peer-kit / test / seance-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
// Séance host sim — rule fidelity against game4's browser hostTick chain.
import test from "node:test";
import assert from "node:assert/strict";
import { SeanceSim, walkMs, CANDLES } from "../dist/games/host/seance-sim.js";
import { mulberry32 } from "../dist/games/host/rng.js";

function makeSim(ids, { seed = 7 } = {}) {
  const wins = new Map(ids.map((id) => [id, 0]));
  const sim = new SeanceSim({
    rng: mulberry32(seed),
    now: () => 0,
    roster: () => ids.map((id, i) => [id, id, i, wins.get(id)]),
    addWin: (id) => wins.set(id, wins.get(id) + 1),
  });
  return { sim, wins };
}

const IDS = ["pa", "pb", "pc"];

test("séance runs to a single survivor and credits the win", () => {
  const { sim, wins } = makeSim(IDS);
  const start = sim.start(1, IDS);
  assert.equal(start.frames[0].f.t, "st");
  assert.deepEqual(start.frames[0].f.order, IDS);
  assert.equal(start.frames[0].f.candles, CANDLES);
  assert.deepEqual(start.next, { step: "bell", inMs: 2600 });

  let bells = 0;
  let emit = sim.step("bell");
  for (;;) {
    bells++;
    // bell frame + targeted medium hint
    const be = emit.frames.find((x) => x.f.t === "be").f;
    assert.equal(be.bell, bells);
    assert.equal(be.durMs, walkMs(bells));
    assert.deepEqual(emit.next, { step: "resolve", inMs: walkMs(bells) + 500 });
    const hm = emit.frames.find((x) => x.f.t === "hm");
    if (be.medium >= 0) {
      assert.ok(hm, "alive medium gets a hint");
      assert.equal(hm.to, IDS[be.medium]);
      assert.notEqual(hm.f.not, sim.haunt, "hint never names the haunted room");
      assert.ok(sim.candles[be.medium] > 0, "medium is an alive seat");
    }
    // pa dodges the ghost; pb and pc walk straight into it
    const haunt = sim.haunt;
    const safe = (haunt + 1) % 3;
    const pc1 = sim.pick("pa", safe);
    assert.equal(pc1.frames[0].f.t, "pc");
    sim.pick("pb", haunt);
    sim.pick("pc", haunt);

    const resolved = sim.step("resolve");
    const rv = resolved.frames[0].f;
    assert.equal(rv.t, "rv");
    assert.equal(rv.haunt, haunt);
    assert.equal(rv.candles[0], CANDLES, "pa never loses a candle");
    assert.equal(resolved.next.inMs, 6500);
    if (resolved.next.step === "end") {
      const end = sim.step("end");
      const ov = end.frames[0].f;
      assert.equal(ov.t, "ov");
      assert.equal(ov.w, "pa");
      assert.equal(ov.bells, bells);
      assert.deepEqual(end.next, { step: "lobby", inMs: 6500 });
      assert.deepEqual(end.matchEnd, [
        ["pa", "pa", 1],
        ["pb", "pb", 0],
        ["pc", "pc", 0],
      ]);
      assert.equal(wins.get("pa"), 1);
      break;
    }
    emit = sim.step("bell");
  }
  assert.equal(bells, CANDLES, "two guests losing every bell end it in exactly 3 bells");
  const lobby = sim.step("lobby");
  assert.equal(lobby.frames[0].f.t, "lo");
  assert.equal(lobby.next, null);
  assert.equal(sim.running, false);
});

test("no tap means you stay where you stood (seat%3 on the first bell)", () => {
  const { sim } = makeSim(IDS);
  sim.start(1, IDS);
  sim.step("bell");
  const rv = sim.step("resolve").frames[0].f;
  assert.deepEqual(rv.rooms, [0, 1, 2]);
});

test("solo séance ends with no winner when the last candle goes out", () => {
  const { sim, wins } = makeSim(["pa"]);
  sim.start(1, ["pa"]);
  for (let bell = 1; bell <= CANDLES; bell++) {
    sim.step("bell");
    sim.pick("pa", sim.haunt); // walk into the ghost every bell
    const resolved = sim.step("resolve");
    if (bell < CANDLES) assert.equal(resolved.next.step, "bell", "solo séance continues while candles remain");
    else {
      assert.equal(resolved.next.step, "end");
      const end = sim.step("end");
      assert.equal(end.frames[0].f.w, null);
      assert.equal(end.matchEnd, undefined, "no winner, no receipt");
    }
  }
  assert.equal(wins.get("pa"), 0);
});

test("mid-walk departures snuff the candle and can end the séance at once", () => {
  const { sim } = makeSim(IDS);
  sim.start(1, IDS);
  sim.step("bell");
  assert.deepEqual(sim.peerGone("pb"), { frames: [] }, "two still standing — séance continues");
  const end = sim.peerGone("pc");
  const ov = end.frames[0].f;
  assert.equal(ov.t, "ov");
  assert.equal(ov.w, "pa");
  assert.ok(end.matchEnd, "abandon-win still fires the receipt hook");
  assert.equal(sim.running, false);
});

test("picks are rejected outside the walk window and from dead or unknown seats", () => {
  const { sim } = makeSim(IDS);
  sim.start(1, IDS);
  assert.deepEqual(sim.pick("pa", 1), { frames: [] }, "no pick before the first bell");
  sim.step("bell");
  assert.deepEqual(sim.pick("zz", 1), { frames: [] }, "unknown guest");
  assert.deepEqual(sim.pick("pa", 3), { frames: [] }, "no fourth room");
  sim.step("resolve");
  assert.deepEqual(sim.pick("pa", 1), { frames: [] }, "no pick during the reveal");
});

test("snapshot carries the walk state and hides the haunt mid-walk", () => {
  const { sim } = makeSim(IDS);
  sim.start(1, IDS);
  sim.step("bell");
  const sn = sim.snapshot("late").frames[0];
  assert.equal(sn.to, "late");
  assert.equal(sn.f.walk, 1);
  assert.equal(sn.f.haunt, -1, "the ghost is a secret while guests walk");
  assert.equal(sn.f.remainMs, walkMs(1), "injected clock at 0 leaves the full window");
  sim.step("resolve");
  const sn2 = sim.snapshot("late").frames[0].f;
  assert.equal(sn2.walk, 0);
  assert.equal(sn2.haunt, sim.haunt);
});

test("seeded runs are byte-identical (gym determinism contract)", () => {
  const transcript = (seed) => {
    const { sim } = makeSim(IDS, { seed });
    const out = [sim.start(1, IDS)];
    for (let i = 0; i < 3; i++) {
      out.push(sim.step("bell"));
      sim.pick("pb", sim.haunt);
      out.push(sim.step("resolve"));
    }
    return JSON.stringify(out);
  };
  assert.equal(transcript(42), transcript(42));
  assert.notEqual(transcript(42), transcript(43));
});

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