feat: add v2 queued taps and free board rotation
This commit is contained in:
+5
-5
@@ -28,9 +28,8 @@ test('mutual blocking reaches a fixed point; move shortage is separate',()=>{
|
||||
const easy=level([{q:0,r:0},{q:0,r:-1}],[tile('a',0,0),tile('b',0,-1)],1);
|
||||
assert.equal(solve(easy).stuck.length,0);assert.equal(solve(easy).solvable,false);
|
||||
});
|
||||
test('all 24 campaign levels validate, solve and increase in score',()=>{
|
||||
let previous=-1;
|
||||
for(const l of LEVELS){assert.deepEqual(validateLevel(l),[]);const a=analyze(l);assert.ok(a.solvable,l.name);assert.ok(a.rotations>=1);assert.equal(solve(l).solvable,false);assert.ok(a.score>=previous);previous=a.score;const g=new Game(l);for(const action of a.actions){if(action.type==='rotate')assert.equal(g.rotate(action.direction).status,'rotated');else assert.equal(g.tap(action.id).status,'exit');}assert.ok(g.won);assert.ok(g.moves>=0);}
|
||||
test('all 24 campaign levels retain their order, validate and solve with free rotations',()=>{
|
||||
for(const l of LEVELS){assert.deepEqual(validateLevel(l),[]);const a=analyze(l);assert.ok(a.solvable,l.name);assert.ok(a.rotations>=1);assert.equal(solve(l).solvable,false);assert.ok(a.score>=0&&a.score<=100);const g=new Game(l);for(const action of a.actions){if(action.type==='rotate')assert.equal(g.rotate(action.direction).status,'rotated');else assert.equal(g.tap(action.id).status,'exit');}assert.ok(g.won);assert.ok(g.moves>=0);}
|
||||
});
|
||||
test('100 generated seeds are deterministic and solvable',()=>{
|
||||
for(let seed=0;seed<100;seed++){const l=generateLevel(seed,seed%2?2:3,seed%2?15:32,.8);assert.deepEqual(validateLevel(l),[]);assert.ok(solve(l).solvable,`seed ${seed}`);assert.ok(l.tiles.length>0);}
|
||||
@@ -62,7 +61,7 @@ test('six board rotations restore tracks; tokens and arrows stay fixed',()=>{
|
||||
let board=l;for(let i=0;i<6;i++)board=rotateBoard(board);
|
||||
assert.deepEqual(board,l);
|
||||
const g=new Game(l),original=clone(g.tiles),before=clone(g.level.cells);
|
||||
assert.equal(g.rotate(1).status,'rotated');assert.deepEqual(g.tiles,original);assert.notDeepEqual(g.level.cells,before);assert.equal(g.orientation,1);assert.equal(g.moves,l.moveLimit-1);
|
||||
assert.equal(g.rotate(1).status,'rotated');assert.deepEqual(g.tiles,original);assert.notDeepEqual(g.level.cells,before);assert.equal(g.orientation,1);assert.equal(g.moves,l.moveLimit);
|
||||
assert.ok(g.undo());assert.equal(g.orientation,0);assert.deepEqual(g.level.cells,before);assert.equal(g.moves,l.moveLimit);
|
||||
});
|
||||
test('a stuck orientation can be rescued by rotating and must not immediately lose',()=>{
|
||||
@@ -83,8 +82,9 @@ test('swept segment detection cannot tunnel through a collider between frames',(
|
||||
const hit=segmentHexHit({x:-10,y:0},{x:10,y:0},{x:0,y:0});assert.ok(hit>0&&hit<1);
|
||||
assert.equal(segmentHexHit({x:-10,y:3},{x:10,y:3},{x:0,y:0}),null);
|
||||
});
|
||||
test('rotation step cost is included in the minimum move budget',()=>{
|
||||
test('free rotations are excluded from the minimum move budget',()=>{
|
||||
const l=clone(LEVELS[0]),a=solveRotations(l);assert.ok(a.rotations>0);
|
||||
assert.equal(a.minimumMoves,l.tiles.length);l.moveLimit=l.tiles.length;assert.ok(solveRotations(l).solvable);const g=new Game(l);for(const action of a.actions){if(action.type==='rotate'){const before=g.moves;g.rotate(action.direction);assert.equal(g.moves,before);}else g.tap(action.id);}assert.ok(g.won);assert.equal(g.moves,0);
|
||||
l.moveLimit=a.minimumMoves-1;const tooShort=solveRotations(l);assert.equal(tooShort.reachable,true);assert.equal(tooShort.solvable,false);
|
||||
});
|
||||
test('irregular boards retain classic play and do not rotate tiles off the base',()=>{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { TapQueue } from '../src/tap-queue.js';
|
||||
import { Game, available } from '../src/core.js';
|
||||
import { LEVELS } from '../src/levels.js';
|
||||
test('rapid taps retain order, ignore duplicate/active pieces, and discard stale IDs',()=>{
|
||||
const queue=new TapQueue();
|
||||
assert.equal(queue.add('a','a'),false);
|
||||
queue.add('b');queue.add('gone');queue.add('c');assert.equal(queue.add('b'),false);
|
||||
const tiles=[{id:'c'},{id:'b'}];
|
||||
assert.equal(queue.take(tiles).id,'b');assert.equal(queue.take(tiles).id,'c');assert.equal(queue.take(tiles),null);
|
||||
queue.add('b');queue.clear();assert.equal(queue.take(tiles),null);
|
||||
});
|
||||
test('buffered removals are evaluated in order against the updated game, with undo intact',()=>{
|
||||
const game=new Game(LEVELS[0]),queue=new TapQueue();
|
||||
const free=available(game.level,game.tiles).slice(0,3);assert.ok(free.length>=2);
|
||||
for(const tile of free)queue.add(tile.id);
|
||||
const start=game.moves;
|
||||
while(queue.ids.length){const tile=queue.take(game.tiles);assert.equal(game.tap(tile.id).status,'exit');}
|
||||
assert.equal(game.moves,start-free.length);
|
||||
assert.ok(game.undo());assert.equal(game.moves,start-free.length+1);
|
||||
});
|
||||
Reference in New Issue
Block a user