Files
scrollsmith/dungeonGenerator.js
Keli Grubb ed95dd08a2
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
initial commit
2025-08-29 22:56:40 -04:00

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);
}
}
}