feat: version campaigns and document original puzzle design
Restore v0.1 and v0.2 archives with separate progress, publish the curved v0.3 campaign, and document the original 24-level design with reproducible evidence. Refine board presentation and movement, and keep video lab tooling within its own directory.
This commit is contained in:
+42
-8
@@ -2,8 +2,8 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { CAMPAIGN, LEVELS } from '../src/levels.js';
|
||||
import { Game, available, rotateBoard, trace, random, validateLevel } from '../src/core.js';
|
||||
import { inspectFlowLevel } from '../src/campaign-generator.js';
|
||||
import { Game, available, trace, random, validateLevel, opposite, key } from '../src/core.js';
|
||||
import { generateFlowLevel, inspectFlowLevel } from '../src/campaign-generator.js';
|
||||
|
||||
const reviewed=LEVELS.map(inspectFlowLevel);
|
||||
|
||||
@@ -12,14 +12,19 @@ test('48 authored levels share one campaign in the browser, JSON export and audi
|
||||
assert.equal(new Set(LEVELS.map(l=>l.id)).size,48);
|
||||
assert.deepEqual(JSON.parse(await readFile(new URL('../levels/campaign.json',import.meta.url),'utf8')),LEVELS);
|
||||
const report=JSON.parse(await readFile(new URL('../levels/campaign-analysis.json',import.meta.url),'utf8'));
|
||||
const manifest=JSON.parse(await readFile(new URL('../levels/campaign-seeds.json',import.meta.url),'utf8'));
|
||||
assert.deepEqual(report.campaign,CAMPAIGN);assert.equal(report.levels.length,48);
|
||||
assert.equal(manifest.seeds.length,48);assert.equal(new Set(manifest.seeds.map(p=>p.seed)).size,48);
|
||||
LEVELS.forEach((l,i)=>{
|
||||
const profile=manifest.seeds[i],generated=generateFlowLevel(profile.seed,profile.options);
|
||||
assert.equal(profile.level,i+1);assert.deepEqual(generated.cells,l.cells);assert.deepEqual(generated.tiles,l.tiles);
|
||||
assert.deepEqual(validateLevel(l),[],l.name);
|
||||
assert.equal(l.chapter,Math.floor(i/6)+1);assert.equal(l.chapterName,CAMPAIGN.chapters[Math.floor(i/6)]);
|
||||
assert.equal(l.moveLimit,l.tiles.length);
|
||||
assert.equal(l.pacing.cognitiveDifficulty,reviewed[i].analysis.score);
|
||||
assert.equal(report.levels[i].difficulty,reviewed[i].analysis.score);
|
||||
assert.equal(report.levels[i].tiles,l.tiles.length);
|
||||
assert.deepEqual(report.levels[i].pathUsage,reviewed[i].analysis.pathUsage);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,20 +39,49 @@ test('every campaign solution finishes, avoids hidden blockers, and needs at mos
|
||||
});
|
||||
});
|
||||
|
||||
test('first level is spacious and every remaining tile can leave after arbitrary free rotations',()=>{
|
||||
const l=LEVELS[0];assert.ok(l.cells.length>=37);assert.ok(l.tiles.length>=12);
|
||||
for(let orientation=0;orientation<6;orientation++)for(const reverse of [false,true]){
|
||||
const g=new Game(rotateBoard(l,orientation)),order=g.tiles.map(t=>t.id);
|
||||
test('first level teaches one visible bend per piece without forced rotation or removal order',()=>{
|
||||
const l=LEVELS[0];assert.equal(l.cells.length,37);assert.equal(l.tiles.length,8);
|
||||
for(const reverse of [false,true]){
|
||||
const g=new Game(l),order=g.tiles.map(t=>t.id);
|
||||
if(reverse)order.reverse();
|
||||
for(const id of order){
|
||||
for(let n=0;n<5;n++)assert.equal(g.rotate(reverse?-1:1).status,'rotated');
|
||||
assert.equal(available(g.level,g.tiles).length,g.tiles.length,'No mysterious final-piece lock or forced removal order');
|
||||
assert.equal(g.moves,g.tiles.length);assert.equal(g.tap(id).status,'exit');
|
||||
assert.equal(g.moves,g.tiles.length);
|
||||
const route=g.tap(id);assert.equal(route.status,'exit');
|
||||
assert.equal(route.steps.filter(s=>s.enter!==null&&opposite(s.enter)!==s.exit).length,1,'The moving arrow must actually turn');
|
||||
}
|
||||
assert.ok(g.won);assert.equal(g.moves,0);
|
||||
}
|
||||
});
|
||||
|
||||
test('all 48 levels use actual bent removal paths, including the release and large-board levels',()=>{
|
||||
for(let i=0;i<LEVELS.length;i++){
|
||||
const p=reviewed[i].analysis.pathUsage;
|
||||
assert.equal(p.removals,LEVELS[i].tiles.length);
|
||||
assert.ok(p.curvedRatio>=.65,`${LEVELS[i].name}: straight exits must remain a minority`);
|
||||
assert.ok(p.curvedRemovals>=3);
|
||||
assert.equal(LEVELS[i].pacing.curvedRatio,p.curvedRatio);
|
||||
}
|
||||
});
|
||||
|
||||
test('the first seven lessons include real turning, crossing, and route-changing situations',()=>{
|
||||
assert.ok(LEVELS.slice(0,7).every(l=>typeof l.lesson==='string'&&l.lesson.length));
|
||||
assert.equal(reviewed[0].analysis.pathUsage.singleBendRemovals,8);
|
||||
assert.equal(reviewed[1].analysis.rotations,1);
|
||||
assert.ok(reviewed[2].analysis.pathUsage.multiBendRemovals>0);
|
||||
const g=new Game(LEVELS[2]);let crossings=0;
|
||||
for(const action of reviewed[2].analysis.actions){
|
||||
if(action.type==='rotate'){g.rotate(action.direction);continue;}
|
||||
for(const step of g.tap(action.id).steps){
|
||||
if(step.enter===null)continue;
|
||||
const cell=g.level.cells.find(c=>key(c.q,c.r)===key(step.q,step.r));
|
||||
const inside=p=>p!==step.enter&&(p-step.enter+6)%6<(step.exit-step.enter+6)%6;
|
||||
for(let p=0;p<6;p++)if(p<cell.tracks[p]&&p!==step.enter&&p!==step.exit&&inside(p)!==inside(cell.tracks[p]))crossings++;
|
||||
}
|
||||
}
|
||||
assert.ok(crossings>0,'The crossing lesson must pass through interleaved track pairs');
|
||||
});
|
||||
|
||||
test('exploring orientations and alternative safe removal orders cannot soft-lock the campaign',()=>{
|
||||
for(const l of LEVELS)for(let run=0;run<4;run++){
|
||||
const rng=random(l.seed+run*997),g=new Game(l);
|
||||
|
||||
Reference in New Issue
Block a user