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 | // Golden-vector tests for the host simulation core and the referee decision
// rules — fixtures extracted from src/game/game.ts while the TS was canon.
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 read = (n) => JSON.parse(readFileSync(new URL(`./vectors/${n}`, import.meta.url), "utf8"));
const sim = read("sim.json");
const referee = read("referee.json");
const snapshot = read("snapshot.json");
const scoring = read("scoring.json");
const { GW, GH } = sim;
test("spawn ring matches TS for every roster size", () => {
for (const [n, starts] of Object.entries(sim.spawns)) {
assert.deepEqual(JSON.parse(JSON.stringify(lib.spawnStarts(Number(n)))), starts, `N=${n}`);
}
});
test("tick cadence matches TS (80ms shrinking to 45)", () => {
for (const [round, ms] of Object.entries(sim.tickMs)) {
assert.equal(lib.tickMsForRound(Number(round)), ms, `round ${round}`);
}
});
function mkBike(b) {
return { id: b.id, x: b.x, y: b.y, dir: b.dir, pend: [...b.pend], alive: b.alive, leftGame: b.leftGame ?? false, color: b.color, nx: b.x, ny: b.y, dying: false };
}
// The scenarios' initial conditions are re-declared here 1:1 with the
// generator script (same literals), and the CLJS sim must reproduce the
// recorded per-tick heads + final state.
const SCENARIOS = {
straight: { cells: [], bikes: [mkBike({ id: "A", x: 10, y: 10, dir: 1, pend: [], alive: true, color: 0 })], ticks: 3 },
turns: { cells: [], bikes: [mkBike({ id: "A", x: 10, y: 10, dir: 1, pend: [3, 2], alive: true, color: 0 })], ticks: 3 },
wall: { cells: [], bikes: [mkBike({ id: "A", x: 0, y: 5, dir: 3, pend: [], alive: true, color: 0 })], ticks: 1 },
trail: { cells: [[20, 20, 1]], bikes: [mkBike({ id: "A", x: 30, y: 30, dir: 1, pend: [], alive: true, color: 0 }), mkBike({ id: "B", x: 20, y: 21, dir: 0, pend: [], alive: true, color: 1 })], ticks: 1 },
"headon-cell": { cells: [], bikes: [mkBike({ id: "A", x: 10, y: 10, dir: 1, pend: [], alive: true, color: 0 }), mkBike({ id: "B", x: 12, y: 10, dir: 3, pend: [], alive: true, color: 1 })], ticks: 1 },
swap: { cells: [], bikes: [mkBike({ id: "A", x: 10, y: 10, dir: 1, pend: [], alive: true, color: 0 }), mkBike({ id: "B", x: 11, y: 10, dir: 3, pend: [], alive: true, color: 1 })], ticks: 1 },
leftgame: { cells: [], bikes: [mkBike({ id: "A", x: 10, y: 10, dir: 1, pend: [], alive: true, color: 0, leftGame: true }), mkBike({ id: "B", x: 50, y: 50, dir: 1, pend: [], alive: true, color: 1 })], ticks: 1 },
};
SCENARIOS.leftgame.bikes[0].leftGame = true;
for (const fixture of sim.scenarios) {
test(`tick scenario: ${fixture.name}`, () => {
const sc = SCENARIOS[fixture.name];
assert.ok(sc, `unknown scenario ${fixture.name}`);
const grid = new Uint8Array(GW * GH);
for (const [x, y, v] of sc.cells) grid[y * GW + x] = v;
for (const b of sc.bikes) if (b.alive) grid[b.y * GW + b.x] = b.color + 1;
const seatOf = new Map(sc.bikes.map((b, i) => [b.id, i]));
const heads = [];
for (let t = 0; t < sc.ticks; t++) {
heads.push(JSON.parse(JSON.stringify(lib.tickStep(grid, sc.bikes, seatOf))));
}
assert.deepEqual(heads, fixture.heads, "per-tick heads");
assert.deepEqual(
sc.bikes.map((b) => ({ id: b.id, x: b.x, y: b.y, dir: b.dir, alive: b.alive, color: b.color, pend: b.pend })),
fixture.bikes,
"final bikes",
);
const gridCells = [];
for (let i = 0; i < grid.length; i++) if (grid[i]) gridCells.push([i % GW, (i / GW) | 0, grid[i]]);
assert.deepEqual(gridCells, fixture.gridCells, "final grid");
});
}
test("host input queue: cap 3, mask to 0..3, dead bikes ignored", () => {
for (const c of sim.hostInput) {
const b = { alive: true, pend: [...c.pend] };
lib.pushInput(b, c.d);
if (c.pushed) assert.deepEqual(b.pend, [...c.pend, c.val]);
else assert.deepEqual(b.pend, c.pend);
}
const dead = { alive: false, pend: [] };
lib.pushInput(dead, 1);
assert.deepEqual(dead.pend, []);
});
test("round-over rule matches TS (solo rounds run to zero)", () => {
for (const c of sim.roundEnd) {
assert.equal(Boolean(lib.roundOver(c.left, c.total)), c.over, JSON.stringify(c));
}
});
test("referee: incumbent decision table", () => {
for (const c of referee.incumbent) {
assert.equal(
Boolean(lib.claimDecision(c.myLive, c.theirLive, c.myId, c.from)),
c.iWin,
JSON.stringify(c),
);
}
});
test("referee: bystander adoption table", () => {
for (const c of referee.bystander) {
assert.equal(
Boolean(lib.bystanderClaimWins(c.theirLive, c.myHostLive, c.from, c.hostId)),
c.claimWins,
JSON.stringify(c),
);
}
});
test("referee: host-loss election", () => {
for (const c of referee.election) {
assert.equal(lib.electHost(c.myId, [...c.openPeers], c.gone), c.next, JSON.stringify(c));
}
});
test("snapshot RLE round-trips byte-identically", () => {
for (const s of snapshot.snapshots) {
const grid = new Uint8Array(GW * GH);
for (const [x, y, v] of s.cells) grid[y * GW + x] = v;
assert.deepEqual(JSON.parse(JSON.stringify(lib.rleEncode(grid))), s.runs, `${s.name} encode`);
assert.deepEqual(JSON.parse(JSON.stringify(lib.rleDecodeCells(s.runs))), s.painted, `${s.name} decode`);
}
});
test("free color picks the first unused index", () => {
for (const c of scoring.freeColors) {
assert.equal(lib.freeColorOf([...c.used]), c.out, JSON.stringify(c));
}
});
|