import { readFile, writeFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import assert from 'node:assert/strict'; import { Game, solveRotations, validateLevel } from '../src/core.js'; import { canonicalLevelSignature } from '../src/campaign-evidence.js'; import { inspectCandidate } from '../src/production-generator.js'; import { countBends } from '../src/experience-generator.js'; const root = new URL('../', import.meta.url); const read = async path => JSON.parse(await readFile(new URL(path, root), 'utf8')); const [levels, manifest, analysis] = await Promise.all(['levels/v0.5.json', 'levels/v0.5-recipes.json', 'levels/v0.5-analysis.json'].map(read)); const digest = value => createHash('sha256').update(value).digest('hex'); assert.equal(levels.length, 1000); assert.equal(manifest.recipes.length, 1000); assert.equal(analysis.levels.length, 1000); for (const [path, hash] of Object.entries(manifest.sourceHashes)) assert.equal(digest(await readFile(new URL(path, root))), hash, `Pipeline changed: ${path}`); const signatures = new Set(); let actionCount = 0, replayCount = 0; for (const [index, level] of levels.entries()) { const row = analysis.levels[index], recipe = manifest.recipes[index]; assert.equal(level.id, row.id); assert.equal(level.id, recipe.id); assert.equal(row.level, index + 1); assert.equal(row.policies.length, 20, level.id); assert.deepEqual(validateLevel(level), [], level.id); const hash = digest(canonicalLevelSignature(level)); assert.equal(hash, row.layoutHash); assert.equal(hash, recipe.layoutHash); assert.ok(!signatures.has(hash), level.id); signatures.add(hash); const solution = solveRotations(level); assert.ok(solution.solvable && !solution.exhausted, level.id); assert.equal(solution.rotations, row.difficulty.requiredRotations, level.id); assert.ok(!inspectCandidate(level, recipe.slot).rejected, level.id); const witnesses = [{ actions: solution.actions, paths: row.reference.paths }, { actions: level.experience.actions, paths: row.authored.paths }, ...row.policies]; for (const { actions, paths } of witnesses) { const game = new Game(level); let departed = 0; for (const action of actions) { const result = action.type === 'rotate' ? game.rotate(action.direction) : game.tap(action.id); assert.equal(result.status, action.type === 'rotate' ? 'rotated' : 'exit', `${level.id} action ${JSON.stringify(action)}`); if (action.type === 'tap') { const recorded = paths[departed++]; assert.equal(recorded.id, action.id, level.id); assert.equal(recorded.cells, result.steps.length, level.id); assert.equal(recorded.bends, countBends(result), level.id); } actionCount++; } assert.equal(departed, paths.length, level.id); assert.ok(game.won, level.id); assert.equal(game.moves, 0, level.id); replayCount++; } assert.ok(row.policies.filter(p => p.paths.some(path => path.bends >= 6)).length >= 16, level.id); assert.ok(row.policies.filter(p => p.paths.some(path => path.id === level.experience.feature.id && path.bends >= recipe.slot.minHeroBends)).length >= 10, level.id); } const verification = { pipelineHash: manifest.pipelineHash, levels: levels.length, uniqueLayouts: signatures.size, independentlySolved: levels.length, completeReplays: replayCount, successfulActions: actionCount, result: 'passed' }; await writeFile(new URL('levels/v0.5-validation.json', root), JSON.stringify(verification, null, 2) + '\n'); console.log(JSON.stringify(verification, null, 2));