/* EMOJESIS data validator + teaching-chain simulation. Run: node validate.js */ 'use strict'; const { ELEMENTS, RECIPES, CATEGORIES, STARTERS } = require('./js/data.js'); let failures = 0; const fail = (msg) => { failures++; console.error(' FAIL: ' + msg); }; const ok = (msg) => console.log(' ok: ' + msg); const pairKey = (a, b, ordered) => ordered ? a + '>' + b : [a, b].sort().join('|'); /* 1. Referential integrity + element fields */ console.log('\n[1] Referential integrity & element fields'); for (const id of Object.keys(ELEMENTS)) { const e = ELEMENTS[id]; if (e.id !== id) fail(`element key "${id}" mismatches e.id "${e.id}"`); for (const f of ['id', 'emoji', 'name', 'category', 'description']) { if (!e[f] || typeof e[f] !== 'string') fail(`element "${id}" missing field "${f}"`); } if (e.category && !CATEGORIES.includes(e.category)) { fail(`element "${id}" has unknown category "${e.category}"`); } } for (const id of STARTERS) { if (!ELEMENTS[id]) fail(`starter "${id}" missing from ELEMENTS`); } RECIPES.forEach((r, i) => { const tag = `recipe #${i} (${r.a}+${r.b}=${r.result})`; if (!ELEMENTS[r.a]) fail(`${tag}: input "${r.a}" does not exist`); if (!ELEMENTS[r.b]) fail(`${tag}: input "${r.b}" does not exist`); if (!ELEMENTS[r.result]) fail(`${tag}: result "${r.result}" does not exist`); if (!r.msg || typeof r.msg !== 'string') fail(`${tag}: missing msg`); }); if (failures === 0) ok('all references and fields valid'); /* 2. Duplicate recipes */ console.log('\n[2] Duplicate recipes'); { const seen = new Map(); RECIPES.forEach((r, i) => { const k = pairKey(r.a, r.b, r.ordered); if (seen.has(k)) { fail(`duplicate recipe pair "${k}" at #${seen.get(k)} and #${i}`); } else { seen.set(k, i); } }); if (failures === 0) ok('no duplicate pairs'); } /* 3. Reachability BFS to fixpoint (nightOnly ignored — craftable at some hour) */ console.log('\n[3] Reachability from starters'); { const reachable = new Set(STARTERS); let changed = true; while (changed) { changed = false; for (const r of RECIPES) { if (!reachable.has(r.result) && reachable.has(r.a) && reachable.has(r.b)) { reachable.add(r.result); changed = true; } } } const unreachable = Object.keys(ELEMENTS).filter((id) => !reachable.has(id)); for (const id of unreachable) fail(`element "${id}" is unreachable from starters`); if (unreachable.length === 0) ok(`all ${Object.keys(ELEMENTS).length} elements reachable`); } /* 4. Counts */ console.log('\n[4] Content counts'); { const nEl = Object.keys(ELEMENTS).length; const nRe = RECIPES.length; const cats = new Set(Object.values(ELEMENTS).map((e) => e.category)); const secrets = new Set(); for (const e of Object.values(ELEMENTS)) if (e.secret) secrets.add(e.id); for (const r of RECIPES) if (r.secret) secrets.add(r.result); console.log(` elements: ${nEl}`); console.log(` recipes: ${nRe}`); console.log(` categories: ${cats.size} (${[...cats].join(', ')})`); console.log(` secrets: ${secrets.size}`); if (nEl < 100) fail(`only ${nEl} elements (need >= 100)`); if (nRe < 160 || nRe > 220) fail(`recipe count ${nRe} outside 160-220`); if (cats.size < 8) fail(`only ${cats.size} categories (need >= 8)`); if (secrets.size < 6) fail(`only ${secrets.size} secrets (need >= 6)`); } /* 5. Teaching chain — exact combos */ console.log('\n[5] Teaching chain (exact combos)'); { const chain = [ ['fire', 'water', 'steam'], ['water', 'earth', 'mud'], ['air', 'water', 'cloud'], ['cloud', 'water', 'rain'], ['rain', 'earth', 'life'], ['life', 'water', 'plant'], ['plant', 'time', 'tree'], ['tree', 'fire', 'wood'], ['earth', 'earth', 'mountain'], ['human', 'earth', 'tool'] ]; for (const [a, b, result] of chain) { const hit = RECIPES.find( (r) => !r.ordered && r.result === result && ((r.a === a && r.b === b) || (r.a === b && r.b === a)) ); if (!hit) fail(`teaching chain combo ${a}+${b}=${result} not found`); } /* Signature combos */ const sig = [ ['brain', 'lightning', 'idea'], ['book', 'fire', 'forbiddenBook'], ['computer', 'coffee', 'developer'], ['internet', 'house', 'personalWebsite'], ['skull', 'love', 'undead'], ['cat', 'computer', 'internetCat'], ['robot', 'love', 'syntheticHeart'], ['moon', 'key', 'secret'], ['statue', 'phone', 'ancientInfluencer'], ['personalWebsite', 'writing', 'blog'], ['blog', 'antenna', 'rss'] ]; for (const [a, b, result] of sig) { const hit = RECIPES.find( (r) => r.result === result && ((r.a === a && r.b === b) || (!r.ordered && r.a === b && r.b === a)) ); if (!hit) fail(`signature combo ${a}+${b}=${result} not found`); } if (failures === 0) ok('teaching chain + signature combos present'); } /* 6. Daily-challenge target pool for a fresh save */ console.log('\n[6] Daily challenge pool (fresh save)'); { const nonStarters = Object.keys(ELEMENTS).filter((id) => !STARTERS.includes(id)); if (nonStarters.length === 0) fail('no non-starter elements available as daily goals'); else ok(`${nonStarters.length} candidate goals for a fresh save`); } /* 7. Simulation: craft the teaching chain step by step from a fresh save */ console.log('\n[7] Simulated playthrough of the teaching chain'); { const owned = new Set(STARTERS); const combine = (a, b) => { const r = RECIPES.find( (x) => !x.ordered && ((x.a === a && x.b === b) || (x.a === b && x.b === a)) ); return r ? r.result : null; }; const steps = [ ['fire', 'water', 'steam'], ['water', 'earth', 'mud'], ['air', 'water', 'cloud'], ['cloud', 'water', 'rain'], ['rain', 'earth', 'life'], ['life', 'water', 'plant'], ['steam', 'earth', 'time'], ['plant', 'time', 'tree'], ['tree', 'fire', 'wood'], ['earth', 'earth', 'mountain'], ['life', 'mud', 'human'], ['human', 'earth', 'tool'] ]; for (const [a, b, expect] of steps) { if (!owned.has(a) || !owned.has(b)) { fail(`sim: "${a}" or "${b}" not yet discovered when needed`); continue; } const got = combine(a, b); if (got !== expect) fail(`sim: ${a}+${b} gave "${got}", expected "${expect}"`); else owned.add(got); } if (failures === 0) ok(`simulation complete — ${owned.size} elements discovered`); } /* Summary */ console.log(''); if (failures > 0) { console.error(`VALIDATION FAILED: ${failures} problem(s).`); process.exit(1); } else { console.log('VALIDATION PASSED.'); }