game5 / client / test / frames.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
// Golden-vector tests: wire frames must serialize byte-identically to the TS
// implementation's (fixtures recorded off the real hostTick/sendSnap/
// broadcastRoster code paths while the TS was canon). These are also the
// frames peer-kit's LampionClient parses, so a key-order or omitted-field
// regression here desyncs the bot fleet as well as the deployed clients.
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 vectors = JSON.parse(readFileSync(new URL("./vectors/frames.json", import.meta.url), "utf8"));
const byName = Object.fromEntries(vectors.map((v) => [v.name, v]));

const lvl = (l) => lib.mkLevel(l.stairs, l.rots, l.auto);
// Each builder is called with the values the fixture recorded, so a key-order
// or omitted-field regression shows up as a byte diff.
const built = {
  "ro-idle": (f) => lib.mkRo(f.host, f.ps, false),
  "ro-live": (f) => lib.mkRo(f.host, f.ps, true),
  hi: (f) => lib.mkHi(f.nick, f.score),
  in: (f) => lib.mkIn(f.a),
  fu: () => lib.mkFu(),
  st: (f) => lib.mkSt(f.round, f.tickMs, f.ticks, f.order, lvl(f.level), f.starts),
  "tk-bare": (f) => lib.mkTk(f.n, null, null, null),
  "tk-mv": (f) => lib.mkTk(f.n, f.mv, null, null),
  "tk-rt-auto": (f) => lib.mkTk(f.n, null, f.rt, null),
  "tk-rt-player": (f) => lib.mkTk(f.n, null, f.rt, null),
  "tk-fp": (f) => lib.mkTk(f.n, null, null, f.fp),
  "tk-mv-rt-fp": (f) => lib.mkTk(f.n, f.mv, f.rt, f.fp),
  ov: (f) => lib.mkOv(f.w, f.tops, f.scores),
  "ov-draw": (f) => lib.mkOv(f.w, f.tops, f.scores),
  ma: (f) => lib.mkMa(f.w, f.scores),
  lo: () => lib.mkLo(),
  sn: (f) => lib.mkSn(f.round, f.tickMs, f.ticks, f.n, f.order, lvl(f.level), f.rots, f.ps),
};

test("every recorded frame type has a builder under test", () => {
  assert.deepEqual(
    Object.keys(built).sort(),
    vectors.map((v) => v.name).sort(),
  );
});

for (const [name, make] of Object.entries(built)) {
  test(`frame ${name} serializes byte-identically to TS`, () => {
    assert.equal(JSON.stringify(make(byName[name].frame)), byName[name].json);
  });
}

// The trap CLJS.md warns about, pinned here as well as in the byte vector:
// "sn" carries NINE pairs, one past js-obj's ordered threshold.
test("sn key order is the wire order, all nine fields", () => {
  const f = lib.mkSn(1, 125, 720, 7, ["a"], lib.mkLevel([[0]], [], [[0, 1]]), [3], [[0, 0, 1]]);
  assert.deepEqual(Object.keys(f), [
    "t", "round", "tickMs", "ticks", "n", "order", "level", "rots", "ps",
  ]);
  assert.equal(
    JSON.stringify(f),
    '{"t":"sn","round":1,"tickMs":125,"ticks":720,"n":7,"order":["a"],' +
      '"level":{"stairs":[[0]],"rots":[],"auto":[[0,1]]},"rots":[3],"ps":[[0,0,1]]}',
  );
});

test("st key order is the wire order", () => {
  assert.deepEqual(Object.keys(lib.mkSt(1, 125, 720, [], lib.mkLevel([], [], []), [])), [
    "t", "round", "tickMs", "ticks", "order", "level", "starts",
  ]);
});

test("tk omits mv, rt and fp when empty, exactly as the TS spreads did", () => {
  assert.equal(JSON.stringify(lib.mkTk(4, [], [], null)), '{"t":"tk","n":4}');
  assert.equal(JSON.stringify(lib.mkTk(4, null, null, null)), '{"t":"tk","n":4}');
  // ...and carries them, in wire order t,n,mv,rt,fp, when present
  assert.equal(
    JSON.stringify(lib.mkTk(4, [[0, 1, 2]], [[1, -1, 0]], [[1, 2, 1]])),
    '{"t":"tk","n":4,"mv":[[0,1,2]],"rt":[[1,-1,0]],"fp":[[1,2,1]]}',
  );
  // an fp of an empty roster is still SENT (the TS tested `fp ?` truthiness,
  // not its length) — only mv/rt were length-gated
  assert.equal(JSON.stringify(lib.mkTk(16, [], [], [])), '{"t":"tk","n":16,"fp":[]}');
});

test("ro omits live unless the sender is host mid-round", () => {
  assert.ok(!JSON.stringify(lib.mkRo("h", [], false)).includes("live"));
  assert.equal(JSON.stringify(lib.mkRo("h", [], true)), '{"t":"ro","host":"h","ps":[],"live":1}');
});

test("ov carries a null winner for a shared night", () => {
  assert.equal(JSON.stringify(lib.mkOv(null, [0, 0], [])), '{"t":"ov","w":null,"tops":[0,0],"scores":[]}');
});

test("isGameFrame accepts frames, rejects junk", () => {
  for (const v of vectors) assert.equal(lib.isGameFrame(v.frame), true, v.name);
  for (const junk of [null, undefined, 42, "hi", [], {}, { t: 7 }]) {
    assert.equal(Boolean(lib.isGameFrame(junk)), false, `junk: ${JSON.stringify(junk)}`);
  }
});

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