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