Files
scrollsmith/src/validation.js
T
2026-02-21 22:19:21 -05:00

121 lines
4.7 KiB
JavaScript

export function extractCanonicalNames(dungeonData) {
const names = { npcs: [], rooms: [], factions: [] };
if (dungeonData.npcs) {
dungeonData.npcs.forEach(npc => { if (npc.name) names.npcs.push(npc.name.trim()); });
}
if (dungeonData.rooms) {
dungeonData.rooms.forEach(room => { if (room.name) names.rooms.push(room.name.trim()); });
}
if (dungeonData.coreConcepts) {
const factionMatch = dungeonData.coreConcepts.match(/Primary Faction[:\s]+([^.]+)/i);
if (factionMatch) names.factions.push(factionMatch[1].trim());
}
return names;
}
export function validateContentCompleteness(dungeonData) {
const issues = [];
const checks = [
['title', 0, 'Missing title'],
['flavor', 20, 'Flavor text too short'],
['hooksRumors', 4, 'Expected at least 4 hooks'],
['rooms', 5, 'Expected at least 5 rooms'],
['encounters', 6, 'Expected at least 6 encounters'],
['npcs', 4, 'Expected at least 4 NPCs'],
['treasure', 4, 'Expected at least 4 treasures'],
['randomEvents', 6, 'Expected 6 random events'],
['plotResolutions', 4, 'Expected at least 4 plot resolutions']
];
checks.forEach(([key, min, msg]) => {
const val = dungeonData[key];
if (!val || (Array.isArray(val) ? val.length < min : val.trim().length < min)) {
issues.push(`${msg}${Array.isArray(val) ? `, got ${val?.length || 0}` : ''}`);
}
});
dungeonData.rooms?.forEach((r, i) => {
if (!r.description || r.description.trim().length < 20) {
issues.push(`Room ${i + 1} (${r.name}) description too short`);
}
});
dungeonData.encounters?.forEach((e, i) => {
if (!e.details || e.details.trim().length < 30) {
issues.push(`Encounter ${i + 1} (${e.name}) details too short`);
}
});
dungeonData.npcs?.forEach((n, i) => {
if (!n.trait || n.trait.trim().length < 30) {
issues.push(`NPC ${i + 1} (${n.name}) description too short`);
}
});
return issues;
}
export function validateContentQuality(dungeonData) {
const issues = [];
const vagueWords = /\b(some|various|several|many|few|things|stuff|items|objects)\b/gi;
const checkVague = (text, ctx) => {
if (!text) return;
const matches = text.match(vagueWords);
if (matches?.length > 2) {
issues.push(`${ctx} contains vague language: "${matches.slice(0, 3).join('", "')}"`);
}
};
checkVague(dungeonData.flavor, 'Flavor text');
dungeonData.rooms?.forEach(r => checkVague(r.description, `Room "${r.name}"`));
dungeonData.encounters?.forEach(e => checkVague(e.details, `Encounter "${e.name}"`));
dungeonData.npcs?.forEach(n => checkVague(n.trait, `NPC "${n.name}"`));
dungeonData.rooms?.forEach(r => {
if (r.description?.length < 50) {
issues.push(`Room "${r.name}" description too short`);
}
});
return issues;
}
export function validateContentStructure(dungeonData) {
const issues = [];
dungeonData.rooms?.forEach((r, i) => {
if (!r.name?.trim()) issues.push(`Room ${i + 1} missing name`);
if (r.name?.split(/\s+/).length > 6) issues.push(`Room "${r.name}" name too long`);
});
dungeonData.encounters?.forEach((e, i) => {
if (!e.name?.trim()) issues.push(`Encounter ${i + 1} missing name`);
if (e.name?.split(/\s+/).length > 6) issues.push(`Encounter "${e.name}" name too long`);
if (e.details && !e.details.match(/^[^:]+:\s/)) {
issues.push(`Encounter "${e.name}" details missing location prefix`);
}
});
dungeonData.npcs?.forEach((n, i) => {
if (!n.name?.trim()) issues.push(`NPC ${i + 1} missing name`);
if (n.name?.split(/\s+/).length > 4) issues.push(`NPC "${n.name}" name too long`);
});
return issues;
}
export function validateNarrativeCoherence(dungeonData) {
const issues = [];
const factionMatch = dungeonData.coreConcepts?.match(/Primary Faction[:\s]+([^.]+)/i);
const factionName = factionMatch?.[1]?.trim();
if (dungeonData.encounters && dungeonData.rooms) {
const roomNames = dungeonData.rooms.map(r => r.name.trim().toLowerCase());
dungeonData.encounters.forEach(e => {
const locMatch = e.details?.match(/^([^:]+):/);
if (locMatch) {
const locName = locMatch[1].trim().toLowerCase();
if (!roomNames.some(rn => locName.includes(rn) || rn.includes(locName))) {
issues.push(`Encounter "${e.name}" references unknown location "${locMatch[1]}"`);
}
}
});
}
if (factionName) {
const factionLower = factionName.toLowerCase();
const refs = (dungeonData.npcs?.filter(n => n.trait?.toLowerCase().includes(factionLower)).length ?? 0)
+ (dungeonData.encounters?.filter(e => e.details?.toLowerCase().includes(factionLower)).length ?? 0);
if (refs < 2) {
issues.push(`Faction "${factionName}" poorly integrated (${refs} references)`);
}
}
return issues;
}