1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167 | // Golden-vector tests for the board engine: scripted input traces replayed
// against the CLJS port and compared snapshot-for-snapshot with what the
// TypeScript produced, plus the exact event-callback order.
//
// Each trace carries its OWN op script (run-length encoded), so the fixture is
// self-contained: nothing here re-derives the inputs.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as lib from "../test-dist/testlib.js";
const v = JSON.parse(readFileSync(new URL("./vectors/engine.json", import.meta.url), "utf8"));
const { HIDDEN_ROWS } = lib.boardConsts();
const expand = (rleScript) => rleScript.flatMap(([n, op]) => Array.from({ length: n }, () => op));
function mk(seed) {
const events = [];
const g = lib.makeGame(
{
onMove: () => events.push(["onMove"]),
onRotate: () => events.push(["onRotate"]),
onLock: () => events.push(["onLock"]),
onHold: () => events.push(["onHold"]),
onHardDrop: () => events.push(["onHardDrop"]),
onLinesCleared: (c, rows) => events.push(["onLinesCleared", c, [...rows]]),
onLevelUp: (l) => events.push(["onLevelUp", l]),
onGameOver: () => events.push(["onGameOver"]),
onSnakeStart: () => events.push(["onSnakeStart"]),
onSnakeEnd: () => events.push(["onSnakeEnd"]),
},
seed,
);
return { g, events };
}
const flat = (g) => [...g.board.grid].map((r) => [...r].map((c) => (c === 0 ? "." : c)).join("")).join("");
function snap(g, events) {
return {
rev: g.rev,
score: g.score,
lines: g.lines,
level: g.level,
over: g.over,
active: { type: g.active.type, rotation: g.active.rotation, x: g.active.x, y: g.active.y },
hold: g.hold ?? null,
holdUsed: g.holdUsed,
ghostY: lib.ghostY(g),
next: [...lib.nextQueue(g)].join(""),
snake: g.snake
? {
segments: [...g.snake.segments].map((s) => [s[0], s[1]]),
dir: g.snake.dir,
timeLeft: g.snake.timeLeft,
duration: g.snake.duration,
turbo: g.snake.turbo,
}
: null,
grid: flat(g),
bs: JSON.stringify(lib.boardStateOf(g)),
ev: events.length,
};
}
function apply(g, op) {
switch (op[0]) {
case "u": return lib.gameUpdate(g, op[1]);
case "L": return lib.gameMove(g, -1);
case "R": return lib.gameMove(g, 1);
case "cw": return lib.gameRotate(g, true);
case "ccw": return lib.gameRotate(g, false);
case "hd": return lib.gameHardDrop(g);
case "hold": return lib.gameHoldPiece(g);
case "sd1": return lib.gameSetSoftDrop(g, true);
case "sd0": return lib.gameSetSoftDrop(g, false);
case "ss": return lib.gameSoftStep(g);
default: throw new Error("bad op " + op[0]);
}
}
const norm = (ret) => (ret === undefined ? null : ret);
for (const trace of v.traces) {
test(`trace ${trace.name} (seed ${trace.seed}) matches TS step for step`, () => {
const { g, events } = mk(trace.seed);
const script = expand(trace.script);
const expected = trace.steps;
let si = 0;
assert.deepEqual(snap(g, events), stripOp(expected[si]), `${trace.name}: initial state`);
si++;
for (let i = 0; i < script.length; i++) {
const op = script[i];
const ret = apply(g, op);
const last = g.over;
if ((i + 1) % trace.stride === 0 || last) {
const want = expected[si];
assert.ok(want, `${trace.name}: ran past the recorded steps at op ${i}`);
assert.deepEqual(want.op, op, `${trace.name} step ${si}: op`);
assert.deepEqual(norm(ret), want.ret, `${trace.name} step ${si}: return value`);
assert.deepEqual(snap(g, events), stripOp(want), `${trace.name} step ${si} (op ${JSON.stringify(op)})`);
si++;
}
if (last) break;
}
assert.equal(si, expected.length, `${trace.name}: recorded ${expected.length} steps, replayed ${si}`);
assert.deepEqual(events, trace.events, `${trace.name}: event order`);
assert.equal(flat(g), trace.finalGrid);
assert.equal(g.over, trace.finalOver);
assert.equal(g.score, trace.finalScore);
});
}
function stripOp(step) {
const { op, ret, ...rest } = step;
return rest;
}
for (const sc of v.clearScenarios) {
test(`scenario ${sc.name}: scoring, level curve and events`, () => {
const { g, events } = mk(sc.seed);
for (const [y0, y1, skip, cell] of sc.setup.fill ?? []) {
for (let y = y0; y < y1; y++) for (let x = 0; x < 10; x++) if (!skip.includes(x)) g.board.grid[y][x] = cell;
}
for (const [y, x, c] of sc.setup.cells ?? []) g.board.grid[y][x] = c;
if (sc.setup.lines !== undefined) g.lines = sc.setup.lines;
if (sc.setup.level !== undefined) g.level = sc.setup.level;
if (sc.setup.active) g.active = { ...sc.setup.active };
assert.deepEqual(snap(g, events), stripOp(sc.before), `${sc.name}: setup state`);
events.length = 0;
for (const op of expand(sc.setup.ops)) apply(g, op);
assert.deepEqual(snap(g, events), stripOp(sc.after), `${sc.name}: final state`);
assert.deepEqual(events, sc.events, `${sc.name}: event order`);
});
}
for (const ep of v.snakeEpisodes) {
test(`snake episodes (seed ${ep.seed}): seeded schedule, steering and freeze`, () => {
const { g, events } = mk(ep.seed);
const filler = expand(ep.filler);
let fi = 0;
for (const episode of ep.episodes) {
// walk pieces to cycling columns until the seeded schedule offers a snake
while (!g.snake && fi < filler.length) apply(g, filler[fi++]);
assert.ok(g.snake, `${ep.seed}: the seeded schedule never offered a snake`);
assert.deepEqual(snap(g, events), stripOp(episode.spawn), `seed ${ep.seed}: snake spawn state`);
for (const op of expand(episode.script)) apply(g, op);
// the episode was only RECORDED once the snake had actually frozen — the
// replay must reach the same place
assert.equal(g.snake, null, `seed ${ep.seed}: the snake did not freeze`);
assert.deepEqual(snap(g, events), stripOp(episode.afterFreeze), `seed ${ep.seed}: after freeze`);
}
assert.equal(events.filter((e) => e[0] === "onSnakeStart").length, ep.starts);
assert.equal(events.filter((e) => e[0] === "onSnakeEnd").length, ep.ends);
});
}
test("boardStateOf projects snake segments out of the hidden rows", () => {
// a spawned snake's tail sits in the hidden rows, so the wire y goes negative
const withSnake = v.snakeEpisodes.find((e) => e.episodes.length > 0).episodes[0].spawn;
const wire = JSON.parse(withSnake.bs);
assert.deepEqual(
wire.snake,
withSnake.snake.segments.map(([x, y]) => [x, y - HIDDEN_ROWS]),
);
assert.equal(wire.grid.length, 200);
});
|