peer-kit / test / neon-grid-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
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
// Neon-grid host sim — rule fidelity against game1's browser hostTick.
import test from "node:test";
import assert from "node:assert/strict";
import { NeonGridSim, GW, GH, WIN_SCORE } from "../dist/games/host/neon-grid-sim.js";

function makeSim(ids) {
  const scores = new Map(ids.map((id) => [id, 0]));
  const sim = new NeonGridSim({
    roster: () => ids.map((id, i) => [id, id, i, scores.get(id)]),
    addScore: (id) => scores.set(id, scores.get(id) + 1),
    resetScores: () => ids.forEach((id) => scores.set(id, 0)),
  });
  return { sim, scores };
}

const riders = (ids) => ids.map((id, i) => ({ id, color: i }));

test("two spawns face each other and meet head-on in a draw", () => {
  const { sim } = makeSim(["pa", "pb"]);
  const start = sim.startRound(riders(["pa", "pb"]));
  const st = start.frames[0].f;
  assert.equal(st.t, "st");
  assert.equal(st.tickMs, 80);
  assert.deepEqual(st.starts[0], [64, 12, 2], "seat 0 spawns top, riding down");
  assert.deepEqual(st.starts[1], [64, 84, 0], "seat 1 spawns bottom, riding up");
  assert.deepEqual(start.next, { step: "tick", inMs: 3000 });

  let emit;
  for (let t = 0; t < 40; t++) {
    emit = sim.step("tick");
    const over = emit.frames.find((x) => x.f.t === "ov");
    if (over) {
      assert.equal(t + 1, 36, "72 cells apart, 2 cells closed per tick");
      assert.equal(over.f.w, null, "same-cell head-on kills both");
      assert.deepEqual(emit.next, { step: "round", inMs: 3200 });
      return;
    }
  }
  assert.fail("round never ended");
});

test("walls kill, the survivor scores, and dead trails are cleared", () => {
  const { sim, scores } = makeSim(["pa", "pb"]);
  sim.startRound(riders(["pa", "pb"]));
  sim.input("pa", 3); // pa turns left toward the near wall (x: 64 → 0)
  let over;
  let ticks = 0;
  while (!over) {
    ticks++;
    const emit = sim.step("tick");
    over = emit.frames.find((x) => x.f.t === "ov")?.f;
    assert.ok(ticks < 100, "round must end");
  }
  assert.equal(ticks, 65, "pa reaches x<0 in 65 ticks");
  assert.equal(over.w, "pb");
  assert.deepEqual(over.scores, [["pa", 0], ["pb", 1]]);
  assert.equal(scores.get("pb"), 1);
  // pa's trail (color 0 → value 1) is wiped; pb's (value 2) survives
  assert.ok(!sim.grid.includes(1), "dead trail cleared");
  assert.ok(sim.grid.includes(2), "survivor trail kept");
});

test("180° reversals are discarded, later queued turns still apply", () => {
  const { sim } = makeSim(["pa", "pb"]);
  sim.startRound(riders(["pa", "pb"]));
  sim.input("pa", 0); // reverse of dir 2 — discarded
  sim.input("pa", 1); // valid right turn — applies the same tick
  const tk = sim.step("tick").frames[0].f;
  const paHead = tk.h.find(([s]) => s === 0);
  assert.deepEqual(paHead, [0, 65, 12, 1], "moved right, not up");
});

test("a queue longer than 3 drops the extras", () => {
  const { sim } = makeSim(["pa", "pb"]);
  sim.startRound(riders(["pa", "pb"]));
  sim.input("pa", 1);
  sim.input("pa", 2);
  sim.input("pa", 1);
  sim.input("pa", 3); // 4th — dropped
  sim.step("tick"); // applies 1
  sim.step("tick"); // applies 2
  sim.step("tick"); // applies 1
  const tk = sim.step("tick").frames[0].f; // queue empty — keeps riding right
  const paHead = tk.h.find(([s]) => s === 0);
  assert.equal(paHead[1], 67, "x advanced on the last two right-facing ticks");
});

test("a departed rider dies on the next tick", () => {
  const { sim } = makeSim(["pa", "pb"]);
  sim.startRound(riders(["pa", "pb"]));
  sim.peerGone("pa");
  const tk = sim.step("tick").frames[0].f;
  assert.deepEqual(tk.h.find(([s]) => s === 0)[3], 0, "left the game, died in place");
});

test("tick speed ramps 80 → 45 across rounds", () => {
  const { sim } = makeSim(["pa", "pb"]);
  const seen = [];
  for (let r = 0; r < 10; r++) {
    seen.push(sim.startRound(riders(["pa", "pb"])).frames[0].f.tickMs);
    sim.abort();
  }
  assert.deepEqual(seen, [80, 76, 72, 68, 64, 60, 56, 52, 48, 45]);
});

test("first to WIN_SCORE is champion: ov carries matchEnd, then ma, then lo", () => {
  const { sim, scores } = makeSim(["pa", "pb"]);
  let final;
  for (let round = 1; round <= WIN_SCORE; round++) {
    sim.startRound(riders(["pa", "pb"]));
    sim.input("pa", 3); // pa suicides into the near wall every round
    let emit;
    do {
      emit = sim.step("tick");
    } while (!emit.frames.some((x) => x.f.t === "ov"));
    final = emit;
  }
  assert.equal(scores.get("pb"), WIN_SCORE);
  assert.deepEqual(final.matchEnd, [
    ["pa", "pa", 0],
    ["pb", "pb", WIN_SCORE],
  ]);
  assert.deepEqual(final.next, { step: "match", inMs: 2200 });
  const ma = sim.step("match");
  assert.equal(ma.frames[0].f.t, "ma");
  assert.equal(ma.frames[0].f.w, "pb");
  assert.deepEqual(ma.next, { step: "lobby", inMs: 6000 });
  const lo = sim.step("lobby");
  assert.equal(lo.frames[0].f.t, "lo");
  assert.equal(scores.get("pb"), 0, "scores reset on the way back to the lobby");
});

test("snapshot RLE round-trips the grid", () => {
  const { sim } = makeSim(["pa", "pb"]);
  sim.startRound(riders(["pa", "pb"]));
  for (let i = 0; i < 10; i++) sim.step("tick");
  const sn = sim.snapshot("late").frames[0];
  assert.equal(sn.to, "late");
  const decoded = new Uint8Array(GW * GH);
  let idx = 0;
  for (let r = 0; r < sn.f.runs.length; r += 2) {
    decoded.fill(sn.f.runs[r], idx, idx + sn.f.runs[r + 1]);
    idx += sn.f.runs[r + 1];
  }
  assert.equal(idx, GW * GH);
  assert.deepEqual(decoded, sim.grid);
});

test("identical input scripts produce byte-identical transcripts", () => {
  const run = () => {
    const { sim } = makeSim(["pa", "pb", "pc"]);
    const out = [sim.startRound(riders(["pa", "pb", "pc"]))];
    for (let t = 0; t < 30; t++) {
      if (t % 7 === 0) sim.input("pa", t % 4);
      if (t % 5 === 0) sim.input("pb", (t + 1) % 4);
      out.push(sim.step("tick"));
    }
    return JSON.stringify(out, (_, v) => (v instanceof Uint8Array ? [...v] : v));
  };
  assert.equal(run(), run());
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/peer-kit.git