game1 / client / test / sim.test.mjs
  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));
  }
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/game1.git