import { extractCanonicalNames, validateContentCompleteness, validateContentQuality, validateContentStructure, validateNarrativeCoherence, } from "./validation.js"; export function validateNameConsistency(dungeonData) { const canonicalNames = extractCanonicalNames(dungeonData); const fixes = []; const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); canonicalNames.npcs.forEach(canonicalName => { if (dungeonData.flavor) { const original = dungeonData.flavor; dungeonData.flavor = dungeonData.flavor.replace(new RegExp(escapeRe(canonicalName), 'gi'), canonicalName); if (original !== dungeonData.flavor) fixes.push(`Fixed NPC name in flavor text: ${canonicalName}`); } if (dungeonData.hooksRumors) { dungeonData.hooksRumors = dungeonData.hooksRumors.map(hook => { const original = hook; const fixed = hook.replace(new RegExp(escapeRe(canonicalName), 'gi'), canonicalName); if (original !== fixed) fixes.push(`Fixed NPC name in hook: ${canonicalName}`); return fixed; }); } if (dungeonData.encounters) { dungeonData.encounters.forEach(encounter => { if (encounter.details) { const original = encounter.details; encounter.details = encounter.details.replace(new RegExp(escapeRe(canonicalName), 'gi'), canonicalName); if (original !== encounter.details) fixes.push(`Fixed NPC name in encounter: ${canonicalName}`); } }); } if (dungeonData.plotResolutions) { dungeonData.plotResolutions = dungeonData.plotResolutions.map(resolution => { const original = resolution; const fixed = resolution.replace(new RegExp(escapeRe(canonicalName), 'gi'), canonicalName); if (original !== fixed) fixes.push(`Fixed NPC name in plot resolution: ${canonicalName}`); return fixed; }); } }); canonicalNames.rooms.forEach(canonicalRoom => { if (dungeonData.encounters) { dungeonData.encounters.forEach(encounter => { if (encounter.details) { const original = encounter.details; encounter.details = encounter.details.replace(new RegExp(escapeRe(canonicalRoom), 'gi'), canonicalRoom); if (original !== encounter.details) fixes.push(`Fixed room name in encounter: ${canonicalRoom}`); } }); } }); return fixes; } export function standardizeEncounterLocations(encounters, rooms) { if (!encounters || !rooms) return { encounters, fixes: [] }; const roomNames = rooms.map(r => r.name.trim()); const fixes = []; const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const fixedEncounters = encounters.map(encounter => { if (!encounter.details) return encounter; const standardized = roomNames.reduce((details, roomName) => { const roomNameRegex = new RegExp(`^${escapeRe(roomName)}\\s*:?\\s*`, 'i'); if (!roomNameRegex.test(details)) return details; const hasColon = details.match(new RegExp(`^${escapeRe(roomName)}:`, 'i')); if (hasColon) return details; fixes.push(`Standardized location format for encounter: ${encounter.name}`); return details.replace(roomNameRegex, `${roomName}: `); }, encounter.details.trim()); return standardized !== encounter.details ? { ...encounter, details: standardized } : encounter; }); return { encounters: fixedEncounters, fixes }; } export function fixStructureIssues(dungeonData) { const fixes = []; if (dungeonData.rooms) { dungeonData.rooms.forEach((room, i) => { if (!room.name || !room.name.trim()) { const desc = room.description || ''; const nameMatch = desc.match(/^([A-Z][^.!?]{5,30}?)(?:\s|\.|:)/); if (nameMatch) { room.name = nameMatch[1].trim(); fixes.push(`Extracted room name from description: "${room.name}"`); } else { room.name = `Room ${i + 1}`; fixes.push(`Added default name for room ${i + 1}`); } } const words = room.name.split(/\s+/); if (words.length > 6) { const original = room.name; room.name = words.slice(0, 6).join(' '); fixes.push(`Truncated room name: "${original}" -> "${room.name}"`); } }); } if (dungeonData.encounters) { dungeonData.encounters.forEach((encounter, i) => { if (!encounter.name || !encounter.name.trim()) { const details = encounter.details || ''; const nameMatch = details.match(/^([^:]+):\s*(.+)$/); if (nameMatch) { encounter.name = nameMatch[1].trim(); encounter.details = nameMatch[2].trim(); fixes.push(`Extracted encounter name from details: "${encounter.name}"`); } else { encounter.name = `Encounter ${i + 1}`; fixes.push(`Added default name for encounter ${i + 1}`); } } const words = encounter.name.split(/\s+/); if (words.length > 6) { const original = encounter.name; encounter.name = words.slice(0, 6).join(' '); fixes.push(`Truncated encounter name: "${original}" -> "${encounter.name}"`); } }); } if (dungeonData.npcs) { dungeonData.npcs.forEach((npc, i) => { if (!npc.name || !npc.name.trim()) { const trait = npc.trait || ''; const nameMatch = trait.match(/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,2})(?:\s|:)/); if (nameMatch) { npc.name = nameMatch[1].trim(); fixes.push(`Extracted NPC name from trait: "${npc.name}"`); } else { npc.name = `NPC ${i + 1}`; fixes.push(`Added default name for NPC ${i + 1}`); } } const words = npc.name.split(/\s+/); if (words.length > 4) { const original = npc.name; npc.name = words.slice(0, 4).join(' '); fixes.push(`Truncated NPC name: "${original}" -> "${npc.name}"`); } }); } return fixes; } export function fixMissingContent(dungeonData) { const fixes = []; if (!dungeonData.npcs || dungeonData.npcs.length < 4) { if (!dungeonData.npcs) dungeonData.npcs = []; const factionName = dungeonData.coreConcepts?.match(/Primary Faction[:\s]+([^.]+)/i)?.[1]?.trim() || 'the primary faction'; while (dungeonData.npcs.length < 4) { dungeonData.npcs.push({ name: `NPC ${dungeonData.npcs.length + 1}`, trait: `A member of ${factionName.toLowerCase()} with unknown motives.` }); fixes.push(`Added fallback NPC ${dungeonData.npcs.length}`); } } if (!dungeonData.encounters || dungeonData.encounters.length < 6) { if (!dungeonData.encounters) dungeonData.encounters = []; if (dungeonData.encounters.length > 0 && dungeonData.rooms?.length > 0) { const dynamicElement = dungeonData.coreConcepts?.match(/Dynamic Element[:\s]+([^.]+)/i)?.[1]?.trim() || 'strange occurrences'; const conflict = dungeonData.coreConcepts?.match(/Central Conflict[:\s]+([^.]+)/i)?.[1]?.trim() || 'a threat'; while (dungeonData.encounters.length < 6) { const roomIndex = dungeonData.encounters.length % dungeonData.rooms.length; const roomName = dungeonData.rooms[roomIndex]?.name || 'Unknown Location'; const fallbackNames = [ `${roomName} Guardian`, `${roomName} Threat`, `${roomName} Challenge`, `${dynamicElement.split(' ')[0]} Manifestation`, `${conflict.split(' ')[0]} Encounter`, `${roomName} Hazard` ]; dungeonData.encounters.push({ name: fallbackNames[dungeonData.encounters.length % fallbackNames.length], details: `${roomName}: An encounter related to ${dynamicElement.toLowerCase()} occurs here.` }); fixes.push(`Added fallback encounter: "${dungeonData.encounters[dungeonData.encounters.length - 1].name}"`); } } } if (!dungeonData.treasure || dungeonData.treasure.length < 4) { if (!dungeonData.treasure) dungeonData.treasure = []; while (dungeonData.treasure.length < 4) { dungeonData.treasure.push({ name: `Treasure ${dungeonData.treasure.length + 1}`, description: `A mysterious item found in the dungeon.` }); fixes.push(`Added fallback treasure ${dungeonData.treasure.length}`); } } if (!dungeonData.randomEvents || dungeonData.randomEvents.length < 6) { if (!dungeonData.randomEvents) dungeonData.randomEvents = []; if (dungeonData.randomEvents.length > 0 && dungeonData.coreConcepts) { const dynamicElement = dungeonData.coreConcepts.match(/Dynamic Element[:\s]+([^.]+)/i)?.[1]?.trim() || 'strange occurrences'; const conflict = dungeonData.coreConcepts.match(/Central Conflict[:\s]+([^.]+)/i)?.[1]?.trim() || 'a mysterious threat'; const fallbackEvents = [ { name: 'Environmental Shift', description: `The ${dynamicElement.toLowerCase()} causes unexpected changes in the environment.` }, { name: 'Conflict Manifestation', description: `A sign of ${conflict.toLowerCase()} appears, requiring immediate attention.` }, { name: 'Dungeon Shift', description: `The dungeon shifts, revealing a previously hidden passage or danger.` }, { name: 'Faction Messenger', description: `An NPC from the primary faction appears with urgent information.` }, { name: 'Power Fluctuation', description: `The power source fluctuates, creating temporary hazards or opportunities.` }, { name: 'Echoes of the Past', description: `Echoes of past events manifest, providing clues or complications.` } ]; while (dungeonData.randomEvents.length < 6) { dungeonData.randomEvents.push(fallbackEvents[dungeonData.randomEvents.length % fallbackEvents.length]); fixes.push(`Added fallback random event: "${dungeonData.randomEvents[dungeonData.randomEvents.length - 1].name}"`); } } } if (!dungeonData.plotResolutions || dungeonData.plotResolutions.length < 4) { if (!dungeonData.plotResolutions) dungeonData.plotResolutions = []; while (dungeonData.plotResolutions.length < 4) { dungeonData.plotResolutions.push(`The adventurers could resolve the central conflict through decisive action.`); fixes.push(`Added fallback plot resolution ${dungeonData.plotResolutions.length}`); } } return fixes; } export function fixNarrativeCoherence(dungeonData) { const fixes = []; if (dungeonData.encounters && dungeonData.rooms) { const roomNames = dungeonData.rooms.map(r => r.name.trim().toLowerCase()); dungeonData.encounters.forEach(encounter => { if (!encounter.details) return; const locationMatch = encounter.details.match(/^([^:]+):/); if (locationMatch) { const locName = locationMatch[1].trim().toLowerCase(); const matches = roomNames.some(rn => locName === rn || locName.includes(rn) || rn.includes(locName) || locName.split(/\s+/).some(word => rn.includes(word)) ); if (!matches && roomNames.length > 0) { const roomIdx = Math.floor(Math.random() * roomNames.length); const roomName = dungeonData.rooms[roomIdx].name; encounter.details = encounter.details.replace(/^[^:]+:\s*/, `${roomName}: `); fixes.push(`Fixed unknown location in encounter "${encounter.name}" to "${roomName}"`); } } }); } return fixes; } export function validateAndFixContent(dungeonData) { const allFixes = []; const nameFixes = validateNameConsistency(dungeonData); allFixes.push(...nameFixes); const structureFixes = fixStructureIssues(dungeonData); allFixes.push(...structureFixes); if (dungeonData.encounters && dungeonData.rooms) { const roomNames = dungeonData.rooms.map(r => r.name.trim()); dungeonData.encounters.forEach((encounter, idx) => { if (!encounter.details) return; if (!encounter.details.match(/^[^:]+:\s/)) { const roomIdx = idx % roomNames.length; const roomName = roomNames[roomIdx]; encounter.details = `${roomName}: ${encounter.details}`; allFixes.push(`Added location "${roomName}" to encounter "${encounter.name}"`); } }); const locationResult = standardizeEncounterLocations(dungeonData.encounters, dungeonData.rooms); dungeonData.encounters = locationResult.encounters; allFixes.push(...locationResult.fixes); } const coherenceFixes = fixNarrativeCoherence(dungeonData); allFixes.push(...coherenceFixes); const contentFixes = fixMissingContent(dungeonData); allFixes.push(...contentFixes); const allIssues = [ ...validateContentCompleteness(dungeonData), ...validateContentQuality(dungeonData), ...validateContentStructure(dungeonData), ...validateNarrativeCoherence(dungeonData), ]; if (allFixes.length > 0) { console.log("\n[Validation] Applied fixes:"); allFixes.forEach(fix => console.log(` - ${fix}`)); } if (allIssues.length > 0) { console.log("\n[Validation] Content quality issues found (not auto-fixable):"); allIssues.forEach(issue => console.warn(` ⚠ ${issue}`)); } else { console.log("\n[Validation] Content quality checks passed"); } return dungeonData; }