127 lines
4.0 KiB
JavaScript
127 lines
4.0 KiB
JavaScript
const OLLAMA_API_URL = process.env.OLLAMA_API_URL;
|
|
const OLLAMA_API_KEY = process.env.OLLAMA_API_KEY;
|
|
|
|
async function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function callOllama(prompt, model = "gemma3:4b", retries = 3) {
|
|
let attempt = 0;
|
|
while (attempt < retries) {
|
|
try {
|
|
const response = await fetch(OLLAMA_API_URL, {
|
|
method: "POST",
|
|
headers: {
|
|
"Authorization": `Bearer ${OLLAMA_API_KEY}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
model,
|
|
messages: [{ role: "user", content: prompt }],
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Ollama API request failed: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const text = data.choices?.[0]?.message?.content;
|
|
if (!text) throw new Error("No response from Ollama");
|
|
return text;
|
|
|
|
} catch (err) {
|
|
attempt++;
|
|
console.warn(`⚠️ Ollama call failed (attempt ${attempt}/${retries}): ${err.message}`);
|
|
if (attempt >= retries) throw err;
|
|
await sleep(1000 * attempt); // exponential backoff
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Three-pass dungeon generation with full resiliency and JSON retry
|
|
*/
|
|
export async function generateDungeon() {
|
|
console.log("🏗️ Starting dungeon generation...");
|
|
|
|
let draft, refined, jsonText;
|
|
|
|
// Draft Pass
|
|
try {
|
|
console.log("📝 Draft pass...");
|
|
const draftPrompt = `
|
|
Generate a Dungeons & Dragons one-page dungeon concept.
|
|
Include a title, flavor text, hooks, rumors, rooms, encounters, treasure, and NPCs.
|
|
Output in readable text (not JSON). Focus on interesting ideas and adventure hooks.
|
|
`;
|
|
draft = await callOllama(draftPrompt);
|
|
console.log("✅ Draft pass complete.");
|
|
} catch (err) {
|
|
console.error("❌ Draft pass failed:", err);
|
|
throw new Error("Dungeon generation failed at draft step");
|
|
}
|
|
|
|
// Refine Pass
|
|
try {
|
|
console.log("🔧 Refine pass...");
|
|
const refinePrompt = `
|
|
Here is a draft dungeon description:
|
|
|
|
${draft}
|
|
|
|
Please carefully proofread this dungeon and improve it for professionalism and clarity:
|
|
- Fix any spelling, grammar, or phrasing issues
|
|
- Flesh out any vague or unclear descriptions
|
|
- Add richer flavor text to rooms, encounters, and NPCs
|
|
- Ensure all hooks, rumors, and treasures are compelling and well-explained
|
|
- Make the dungeon read as a polished, professional one-page adventure
|
|
|
|
Keep the output as readable text format, not JSON.
|
|
`;
|
|
refined = await callOllama(refinePrompt);
|
|
console.log("✅ Refine pass complete.");
|
|
} catch (err) {
|
|
console.warn("⚠️ Refine pass failed, using draft as fallback:", err.message);
|
|
refined = draft;
|
|
}
|
|
|
|
// JSON Pass with retries
|
|
const jsonPrompt = `
|
|
Convert the following improved dungeon description into strictly valid JSON.
|
|
Use the following fields:
|
|
- title
|
|
- flavor
|
|
- map (just a placeholder string like "map.png")
|
|
- hooks (array of strings)
|
|
- rumors (array of strings)
|
|
- rooms (array of objects with "name" and "description")
|
|
- encounters (array of objects with "name" and "details")
|
|
- treasure (array of strings)
|
|
- npcs (array of objects with "name" and "trait")
|
|
Do not include any commentary, only output valid JSON.
|
|
|
|
Dungeon description:
|
|
${refined}
|
|
`;
|
|
|
|
const maxJsonRetries = 3;
|
|
for (let attempt = 1; attempt <= maxJsonRetries; attempt++) {
|
|
try {
|
|
console.log(`📦 JSON pass (attempt ${attempt}/${maxJsonRetries})...`);
|
|
jsonText = await callOllama(jsonPrompt);
|
|
const cleaned = jsonText.replace(/```json|```/g, "").trim();
|
|
const result = JSON.parse(cleaned);
|
|
console.log("🎉 Dungeon generation complete!");
|
|
return result;
|
|
} catch (err) {
|
|
console.warn(`⚠️ JSON parse failed (attempt ${attempt}/${maxJsonRetries}): ${err.message}`);
|
|
if (attempt === maxJsonRetries) {
|
|
console.error("❌ JSON pass ultimately failed. Raw output:", jsonText);
|
|
throw new Error("Dungeon generation failed at JSON step");
|
|
}
|
|
await sleep(1000 * attempt);
|
|
}
|
|
}
|
|
}
|