From 271b9836186bc90b63ead5fadc8ffe7188fc98f4 Mon Sep 17 00:00:00 2001 From: keligrubb Date: Sat, 30 Aug 2025 21:46:57 -0400 Subject: [PATCH] try to make prompts more lightweight --- dungeonGenerator.js | 215 +++++++++++++++++++++++++------------------- 1 file changed, 124 insertions(+), 91 deletions(-) diff --git a/dungeonGenerator.js b/dungeonGenerator.js index a89c1a9..9f53b54 100644 --- a/dungeonGenerator.js +++ b/dungeonGenerator.js @@ -5,9 +5,12 @@ async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } -async function callOllama(prompt, model = "gemma3n:e4b", retries = 6) { +async function callOllama(prompt, model = "gemma3n:e4b", retries = 6, stepName = "unknown") { for (let attempt = 1; attempt <= retries; attempt++) { try { + console.log(`\nšŸ“¤ [${stepName}] Sending prompt (attempt ${attempt}/${retries})...`); + console.log(` Prompt length: ${prompt.length} chars, ~${prompt.split(/\s+/).length} words`); + const response = await fetch(OLLAMA_API_URL, { method: "POST", headers: { @@ -27,103 +30,133 @@ async function callOllama(prompt, model = "gemma3n:e4b", retries = 6) { const data = await response.json(); const text = data.choices?.[0]?.message?.content; if (!text) throw new Error("No response from Ollama"); - return text; + + console.log(`āœ… [${stepName}] Success — received ${text.length} chars`); + return text.trim(); } catch (err) { - console.warn(`āš ļø Ollama call failed (attempt ${attempt}/${retries}): ${err.message}`); + console.warn(`āš ļø [${stepName}] Failed (attempt ${attempt}/${retries}): ${err.message}`); if (attempt === retries) throw err; - - // Exponential backoff with jitter - const delay = Math.pow(2, attempt) * 1000; // 2^attempt seconds - const jitter = Math.random() * 1000; // up to 1 second extra - await sleep(delay + jitter); + const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000; + console.log(` Retrying in ${Math.round(delay / 1000)}s...`); + await sleep(delay); } } } +function parseList(raw) { + return raw + .split("\n") + .map(line => line.replace(/^\d+[\).\s-]*/, "").trim()) + .filter(line => line.length > 0); +} -/** - * 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 = 5; - for (let attempt = 1; attempt <= maxJsonRetries; attempt++) { - try { - console.log(`šŸ“¦ JSON pass (attempt ${attempt}/${maxJsonRetries})...`); - jsonText = await callOllama(jsonPrompt, "gemma3n:e4b", 6); - 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"); +function parseObjects(raw, type = "rooms") { + return raw + .split("\n") + .map(line => line.replace(/^\d+[\).\s-]*/, "").trim()) + .filter(line => line.length > 0) + .map(entry => { + if (type === "rooms") { + const [name, ...descParts] = entry.split(/[-–—:]/); + return { name: name.trim(), description: descParts.join(" ").trim() }; } - await sleep(1000 * attempt); - } - } + if (type === "encounters") { + const [name, ...detailParts] = entry.split(/[-–—:]/); + return { name: name.trim(), details: detailParts.join(" ").trim() }; + } + if (type === "npcs") { + const [name, ...traitParts] = entry.split(/[-–—:]/); + return { name: name.trim(), trait: traitParts.join(" ").trim() }; + } + return entry; + }); +} + +export async function generateDungeon() { + console.log("šŸ—ļø Starting staged dungeon generation...\n"); + + // Step 1: Generate 10 titles + const titlesRaw = await callOllama( + "Generate 10 unique creative dungeon titles. Output as a numbered list only.", + "gemma3n:e4b", + 6, + "Step 1: Titles (10)" + ); + const titles10 = parseList(titlesRaw); + + // Step 2: Pick top 3, add 2 new + const titles3plusRaw = await callOllama( + `Here are 10 dungeon titles:\n${titles10.join("\n")}\n\nSelect the 3 strongest titles and add 2 new original ones. Output 5 titles as a numbered list.`, + "gemma3n:e4b", + 6, + "Step 2: Narrow (5)" + ); + const titles5 = parseList(titles3plusRaw); + + // Step 3: Pick single best + const bestTitleRaw = await callOllama( + `Here are 5 dungeon titles:\n${titles5.join("\n")}\n\nPick the single strongest title. Output only the title.`, + "gemma3n:e4b", + 6, + "Step 3: Final Title" + ); + const title = bestTitleRaw.split("\n")[0].trim(); + + // Step 4: Flavor text + const flavor = await callOllama( + `Write one evocative paragraph of flavor text for a dungeon titled "${title}". Do not include hooks, NPCs, or treasure.`, + "gemma3n:e4b", + 6, + "Step 4: Flavor" + ); + + // Step 5: Hooks & rumors + const hooksRumorsRaw = await callOllama( + `Based only on this dungeon flavor:\n${flavor}\n\nGenerate 3 adventure hooks and 3 rumors. Output as two numbered lists: first "Hooks", then "Rumors".`, + "gemma3n:e4b", + 6, + "Step 5: Hooks & Rumors" + ); + const [hooksSection, rumorsSection] = hooksRumorsRaw.split(/Rumors[:\n]/i); + const hooks = parseList(hooksSection.replace(/Hooks[:\n]*/i, "")); + const rumors = parseList(rumorsSection || ""); + + // Step 6: Rooms & encounters + const roomsEncountersRaw = await callOllama( + `Based on this flavor:\n${flavor}\n\nAnd these hooks and rumors:\n${hooks.join("\n")}\n${rumors.join("\n")}\n\nGenerate:\n- 5 rooms with names and short descriptions\n- 3 encounters with names and details.\nOutput as two numbered lists labeled "Rooms" and "Encounters".`, + "gemma3n:e4b", + 6, + "Step 6: Rooms & Encounters" + ); + const [roomsSection, encountersSection] = roomsEncountersRaw.split(/Encounters[:\n]/i); + const rooms = parseObjects(roomsSection.replace(/Rooms[:\n]*/i, ""), "rooms"); + const encounters = parseObjects(encountersSection || "", "encounters"); + + // Step 7: Treasure & NPCs + const treasureNpcsRaw = await callOllama( + `Based only on these rooms and encounters:\n${JSON.stringify({ rooms, encounters }, null, 2)}\n\nGenerate:\n- 3 treasures (list)\n- 3 NPCs (name + trait).\nOutput as two numbered lists labeled "Treasure" and "NPCs".`, + "gemma3n:e4b", + 6, + "Step 7: Treasure & NPCs" + ); + const [treasureSection, npcsSection] = treasureNpcsRaw.split(/NPCs[:\n]/i); + const treasure = parseList(treasureSection.replace(/Treasure[:\n]*/i, "")); + const npcs = parseObjects(npcsSection || "", "npcs"); + + // Step 8: Assemble JSON in code + const dungeon = { + title, + flavor: flavor.trim(), + map: "map.png", + hooks, + rumors, + rooms, + encounters, + treasure, + npcs + }; + + console.log("\nšŸŽ‰ Dungeon generation complete!"); + return dungeon; }