90 lines
5.9 KiB
JavaScript
90 lines
5.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { createHash } from 'node:crypto';
|
|
import { Game, solveRotations, validateLevel, trace, rotateBoard, available } from '../src/core.js';
|
|
import { PRODUCTION_CAMPAIGN, PRODUCTION_LEVELS } from '../src/levels-v05.js';
|
|
import { constructProductionCandidate, inspectCandidate } from '../src/production-generator.js';
|
|
import { canonicalLevelSignature } from '../src/campaign-evidence.js';
|
|
import { countBends, pointsInward } from '../src/experience-generator.js';
|
|
|
|
const manifest = JSON.parse(await readFile(new URL('../levels/v0.5-recipes.json', import.meta.url), 'utf8'));
|
|
const report = JSON.parse(await readFile(new URL('../levels/v0.5-analysis.json', import.meta.url), 'utf8'));
|
|
const digest = text => createHash('sha256').update(text).digest('hex');
|
|
|
|
test('1000 published layouts have matching data, unique whole-scene structures and complete formal replays', async () => {
|
|
assert.equal(PRODUCTION_LEVELS.length, 1000);
|
|
assert.deepEqual(PRODUCTION_LEVELS, JSON.parse(await readFile(new URL('../levels/v0.5.json', import.meta.url), 'utf8')));
|
|
assert.equal(PRODUCTION_CAMPAIGN.chapters.length * PRODUCTION_CAMPAIGN.chapterSize, 1000);
|
|
const unique = new Set();
|
|
for (const [index, level] of PRODUCTION_LEVELS.entries()) {
|
|
assert.deepEqual(validateLevel(level), [], level.id);
|
|
const hash = digest(canonicalLevelSignature(level));
|
|
assert.ok(!unique.has(hash), level.id); unique.add(hash);
|
|
assert.equal(hash, manifest.recipes[index].layoutHash);
|
|
assert.equal(level.chapter, Math.floor(index / 50) + 1);
|
|
const solution = solveRotations(level);
|
|
assert.ok(solution.solvable && !solution.exhausted, level.id);
|
|
assert.equal(solution.rotations, level.production.difficulty.requiredRotations, level.id);
|
|
for (const actions of [solution.actions, level.experience.actions, ...report.levels[index].policies.map(policy => policy.actions)]) {
|
|
const game = new Game(level);
|
|
for (const action of actions) assert.equal(action.type === 'rotate' ? game.rotate(action.direction).status : game.tap(action.id).status, action.type === 'rotate' ? 'rotated' : 'exit', level.id);
|
|
assert.ok(game.won, level.id); assert.equal(game.moves, 0, level.id);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('each advertised experience has actual route and release evidence', () => {
|
|
for (const [index, level] of PRODUCTION_LEVELS.entries()) {
|
|
const slot = manifest.recipes[index].slot, row = report.levels[index], hero = level.tiles[0];
|
|
assert.ok(!inspectCandidate(level, slot).rejected, level.id);
|
|
assert.ok(row.authored.won && row.reference.won, level.id);
|
|
assert.equal(row.policies.length, 20);
|
|
assert.ok(row.policies.every(p => p.won), level.id);
|
|
const corePolicies = row.policies.slice(0, 6);
|
|
if (index > 1) assert.ok(corePolicies.filter(p => p.longPaths > 0).length >= 5, level.id);
|
|
const isPrimaryLong = p => p.paths.some(path => path.id === hero.id && path.bends >= slot.minHeroBends);
|
|
assert.ok(corePolicies.filter(isPrimaryLong).length >= 4, level.id);
|
|
assert.ok(row.policies.filter(p => p.longPaths > 0).length >= 16, level.id);
|
|
assert.ok(row.policies.filter(isPrimaryLong).length >= 10, level.id);
|
|
assert.ok(row.shapeUsage.traveledPairs >= 4, level.id);
|
|
assert.ok(row.shapeUsage.heroFeaturedPairs >= Math.min(6, slot.minHeroBends), level.id);
|
|
assert.ok(row.shapeUsage.heroFeaturedRatio >= .6, level.id);
|
|
if (level.experience.kind === 'tour') assert.equal(available(level).length, level.tiles.length, level.id);
|
|
if (level.experience.kind === 'contrast') {
|
|
assert.ok(pointsInward(hero), level.id); assert.equal(trace(level, hero).status, 'blocked', level.id);
|
|
const after = trace(rotateBoard(level), hero); assert.equal(after.status, 'exit', level.id);
|
|
assert.ok(countBends(after) >= slot.minHeroBends, level.id);
|
|
assert.ok(row.difficulty.requiredRotations >= 1, level.id);
|
|
assert.ok(corePolicies.filter(p => p.fulfilledReveals.some(e => e.id === hero.id && e.bends >= slot.minHeroBends)).length >= (slot.number <= 30 ? 4 : 3), level.id);
|
|
assert.ok(row.policies.filter(p => p.fulfilledReveals.some(e => e.id === hero.id && e.bends >= slot.minHeroBends)).length >= 6, level.id);
|
|
}
|
|
if (level.experience.kind === 'gate') {
|
|
assert.ok(corePolicies.filter(p => p.fulfilledUnlocks.some(e => e.id === hero.id && e.onRoute && e.bends >= slot.minHeroBends)).length >= 4, level.id);
|
|
assert.ok(row.policies.filter(p => p.fulfilledUnlocks.some(e => e.id === hero.id && e.onRoute && e.bends >= slot.minHeroBends)).length >= 8, level.id);
|
|
assert.equal(trace(level, level.tiles.find(t => t.id === 't1')).status, 'exit', level.id);
|
|
assert.equal(trace(level, hero, level.tiles.filter(t => t.id !== 't1')).status, 'exit', level.id);
|
|
}
|
|
if (['turn', 'stages'].includes(level.experience.kind)) assert.ok(row.difficulty.requiredRotations > 0, level.id);
|
|
}
|
|
});
|
|
|
|
test('construction recipes reproduce early lessons and samples across every chapter', () => {
|
|
const indices = [...new Set([...Array.from({ length: 10 }, (_, i) => i), ...Array.from({ length: 20 }, (_, i) => i * 50 + 49), 188, 360, 774, 797, 825, 848, 890, 964])];
|
|
for (const index of indices) {
|
|
const recipe = manifest.recipes[index], original = PRODUCTION_LEVELS[index];
|
|
const { level } = constructProductionCandidate(recipe.slot, recipe.attempt);
|
|
assert.ok(level, recipe.id);
|
|
assert.deepEqual(level.cells, original.cells, recipe.id);
|
|
assert.deepEqual(level.tiles, original.tiles, recipe.id);
|
|
assert.deepEqual(level.experience.actions, original.experience.actions, recipe.id);
|
|
}
|
|
});
|
|
|
|
test('the reviewed chapter ending has a woven breather before the layered flower', () => {
|
|
assert.equal(PRODUCTION_LEVELS[848].production.family, 'weave');
|
|
assert.equal(PRODUCTION_LEVELS[848].experience.kind, 'tour');
|
|
assert.equal(PRODUCTION_LEVELS[849].production.family, 'petals');
|
|
assert.equal(PRODUCTION_LEVELS[849].experience.kind, 'stages');
|
|
});
|