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 | // The late-joiner snapshot: the "sn" frame the host sends must be byte-exact
// (ten keys, past js-obj's eight-pair limit) for the recorded host states.
// Fixtures from src/game/game.ts's sendSnap 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 v = JSON.parse(readFileSync(new URL("./vectors/snapshot.json", import.meta.url), "utf8"));
// sendSnap's own arithmetic, re-expressed against the recorded host state: the
// walk flag and remainMs come from inWalk + the clock, and haunt is withheld
// while the walk is still open (revealing it mid-walk would leak the answer).
const snapOf = (h) =>
lib.mkSn(
h.seance,
h.bell,
h.inWalk ? 1 : 0,
h.inWalk ? Math.max(0, h.walkEndAt - h.now) : 0,
h.mediumSeat,
h.order,
h.candles,
h.prevRooms,
h.inWalk ? -1 : h.haunt,
);
for (const c of v.cases) {
test(`snapshot ${c.name} serializes byte-identically to TS`, () => {
assert.equal(JSON.stringify(snapOf(c.host)), JSON.stringify(c.frame));
assert.deepEqual(Object.keys(snapOf(c.host)), Object.keys(c.frame), "key order");
});
}
test("a mid-walk snapshot withholds the haunted room and carries the remaining time", () => {
const walk = v.cases.find((c) => c.name === "mid-walk");
assert.ok(walk, "fixture present");
assert.equal(walk.frame.walk, 1);
assert.equal(walk.frame.haunt, -1, "the answer must not ride in the snapshot");
assert.ok(walk.frame.remainMs > 0 && walk.frame.remainMs < 25000);
});
test("an in-reveal snapshot carries the haunted room and no clock", () => {
const rev = v.cases.find((c) => c.name === "in-reveal");
assert.ok(rev, "fixture present");
assert.equal(rev.frame.walk, 0);
assert.equal(rev.frame.remainMs, 0);
assert.ok(rev.frame.haunt >= 0);
});
test("the snapshot's rooms are prevRooms, so a snuffed seat never reads -1 as a room", () => {
for (const c of v.cases) {
assert.deepEqual(c.frame.rooms, c.host.prevRooms);
for (const r of c.frame.rooms) assert.ok(r >= 0 && r <= 2, `room index ${r}`);
}
});
// The client side of the same fixtures: applySnap's resulting G, and the full
// phase-frame trace, are recorded so the CLJS apply path can be compared to the
// TS one by eye during review and by the browser smoke.
test("the recorded client apply trace covers every host frame type", () => {
const labels = v.trace.map((s) => s.label);
for (const want of [
"after-st", "after-be-i-am-medium", "after-hm", "after-pc", "after-my-pick",
"after-rv-i-was-hit", "after-be-not-medium", "after-rv-safe", "after-ov-i-won",
"after-lo", "after-st-spectating", "after-rv-in-lobby-ignored",
]) {
assert.ok(labels.includes(want), `missing trace step ${want}`);
}
});
test("the apply trace pins the phase machine's transitions", () => {
const at = (l) => v.trace.find((s) => s.label === l);
assert.equal(at("after-st").phase, "begin");
assert.equal(at("after-st").spectator, false);
assert.deepEqual(at("after-st").candles, [3, 3, 3]);
assert.deepEqual(at("after-st").rooms, [0, 1, 2], "seat%3 is where you start");
assert.equal(at("after-be-i-am-medium").phase, "walk");
assert.equal(at("after-be-i-am-medium").haunt, -1);
assert.equal(at("after-hm").mediumNot, 2);
assert.equal(at("after-pc").locked, 2);
assert.equal(at("after-my-pick").myPick, 0);
assert.equal(at("after-rv-i-was-hit").phase, "reveal");
assert.equal(at("after-rv-i-was-hit").haunt, 0);
// a new bell for someone else's medium clears my whisper
assert.equal(at("after-be-not-medium").mediumNot, -1);
assert.equal(at("after-ov-i-won").phase, "seanceover");
assert.equal(at("after-lo").phase, "lobby");
assert.deepEqual(at("after-lo").order, []);
assert.equal(at("after-st-spectating").spectator, true, "absent from the order = spectator");
// applyReveal's phase guard: a stray rv in the lobby changes nothing
assert.equal(at("after-rv-in-lobby-ignored").phase, "lobby");
assert.equal(at("after-rv-in-lobby-ignored").haunt, -1);
});
test("applySnap always seats the late joiner as a spectator", () => {
for (const a of v.applied) {
assert.equal(a.G.spectator, true, a.name);
assert.equal(a.G.myPick, -1, a.name);
assert.equal(a.G.mediumNot, -1, `${a.name}: the whisper is never in a snapshot`);
}
const walk = v.applied.find((a) => a.name === "mid-walk");
assert.equal(walk.G.phase, "walk");
assert.equal(walk.G.durMs, walk.G.durMs, "the remaining window becomes the whole ring");
const rev = v.applied.find((a) => a.name === "in-reveal");
assert.equal(rev.G.phase, "reveal");
});
|