feat: add production campaigns, gameplay labs, and review documentation

This commit is contained in:
cabinw
2026-09-13 12:27:10 +08:00
parent d1dffa9338
commit 4644e975b0
214 changed files with 98994 additions and 134 deletions
+52
View File
@@ -0,0 +1,52 @@
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));