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:
cabinw
2026-09-09 13:17:30 +08:00
parent 70b021d4cc
commit e92cccba82
52 changed files with 76449 additions and 15076 deletions
+25 -12
View File
@@ -35,8 +35,8 @@ test('edge exits are brisk and longer routes preserve a bounded readable travel
const edge={status:'exit',steps:[{q:0,r:0,enter:null,exit:0}],end:{q:0,r:-1}};
const short=exitTiming(edge);
const long=exitTiming({...edge,steps:[edge.steps[0],{q:0,r:-1,enter:3,exit:0},{q:0,r:-2,enter:3,exit:0}]});
assert.equal(short.duration,320);
assert.ok(long.duration>short.duration&&long.duration<=980);
assert.ok(short.duration>260&&short.duration<400);
assert.ok(long.duration>short.duration&&long.duration<1200);
for(const timing of [short,long])assert.ok(Math.abs(timing.duration*(1-timing.travelFraction)-180)<.001);
});
@@ -52,15 +52,28 @@ test('motion ramps gently at both ends, never reverses or overshoots',async()=>{
});
test('long routes regain original pacing while edge exits and the finish stay brisk',async()=>{
const {exitTiming}=await import('../src/exit-animation.js');
const route=n=>({status:'exit',steps:Array.from({length:n},(_,i)=>({q:0,r:-i,enter:i?3:null,exit:0}))});
assert.equal(exitTiming(route(1)).duration,320);
for(const [cells,expected] of [[5,975],[10,1400],[17,1700]]){
const timing=exitTiming(route(cells));
assert.equal(timing.duration,expected);
assert.ok(Math.abs(timing.duration*(1-timing.travelFraction)-180)<.001);
test('curved and long routes share the same cruising speed without a duration cap',async()=>{
const {exitTiming,travelProgress}=await import('../src/exit-animation.js');
for(const distance of [1,5,15,30]){
const timing=exitTiming({travelPoints:[{x:0,y:0},{x:distance,y:0}],steps:[]});
assert.equal(timing.travelMs,distance*120+Math.min(80,distance*40));
const t=timing.travelMs*.5,dt=1;
const speed=(travelProgress((t+dt)/timing.travelMs,timing.travelMs)-travelProgress(t/timing.travelMs,timing.travelMs))*distance;
assert.ok(Math.abs(speed-1/120)<1e-9);
assert.equal(travelProgress(0,timing.travelMs),0);assert.equal(travelProgress(1,timing.travelMs),1);
assert.ok(Math.abs(timing.duration-timing.travelMs-180)<1e-9);
}
});
test('showcase boards open with a long curved route and can be cleared completely',async()=>{
const {createStudyLevel}=await import('../src/study-levels.js');
const {available,trace,Game,validateLevel}=await import('../src/core.js');
for(const [radius,minLength] of [[3,5],[5,8],[6,11]]){
const level=createStudyLevel(radius),game=new Game(level);
assert.deepEqual(validateLevel(level),[]);
const routes=available(level,level.tiles).map(t=>trace(level,t,level.tiles));
assert.ok(routes.some(r=>r.steps.length>=minLength&&r.steps.some(s=>s.enter!==null&&(s.enter+3)%6!==s.exit)));
while(game.tiles.length){const tile=available(game.level,game.tiles)[0];assert.ok(tile);game.tap(tile.id);}
assert.ok(game.won);
}
const durations=Array.from({length:17},(_,i)=>exitTiming(route(i+1)).duration);
assert.ok(durations.every((t,i)=>i===0||t>=durations[i-1]));
});