peer-kit / test / tessera.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
// TesseraClient's host-claim comparator, against dist/.
//
// Tessera browsers put `live: 1` on their "ro" while they host a round, so the
// plain lowest-id rule TesseraClient inherited from room.cljs would partition a
// mixed bot/browser room — exactly what room.cljs's header warns about. The
// truth table below is game3's OWN answer (sim/bystander-claim-wins), recorded
// in the fixture, and every row is reached through the wire: the phase is
// driven by real game3 frames (its own mk-st/mk-tk/mk-ov/mk-ma/mk-lo/mk-sn
// output), never by poking _phase, and the claim frame itself is game3's mk-ro.
//
// game3 has a countdown phase and names the roster field `live` (frames.cljs
// mk-ro, game.cljs round-live?), so tessera's override is the neon-grid /
// lampion one, NOT valley-blocks' (`playing`, no countdown) — asserted below
// on the compiled function text of all four.
//
// Fixture provenance: test/vectors/generate-tessera.mjs (imports game3's
// compiled client as ground truth).
import test from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { readFileSync } from "node:fs";
import { TesseraClient } from "../dist/games/tessera.js";
import { NeonGridClient } from "../dist/games/neon-grid.js";
import { LampionClient } from "../dist/games/lampion.js";
import { ValleyBlocksClient } from "../dist/games/valley-blocks.js";
import { GameRoom } from "../dist/games/room.js";

const V = JSON.parse(readFileSync(new URL("./vectors/tessera-client.json", import.meta.url), "utf8"));

class MockPeer extends EventEmitter {
  constructor(id) {
    super();
    this.id = id;
    this.sent = []; // [to, frame]
    this.net = {
      roomPeers: () => new Set(),
      openPeers: () => [],
      relayConnected: () => true,
      roomId: "testroom",
      wake: () => {},
    };
  }
  get myId() {
    return this.id;
  }
  send(to, frame) {
    this.sent.push([to, frame]);
  }
  broadcast(frame) {
    this.sent.push(["*", frame]);
  }
  identityOf() {
    return null;
  }
  close() {
    return Promise.resolve();
  }
}

/** A client seated at V.seat, following `host`. Returns [client, peer]. */
const seated = (host) => {
  const peer = new MockPeer(V.order[V.seat]);
  const client = new TesseraClient(peer, {
    app: "tessera", roomSecret: "s", nick: "bot", keepAliveMs: 0,
  });
  peer.emit("message", host, { t: "ro", host, ps: [[host, "gazda", 1, 0]] });
  assert.equal(client.hostId, host);
  peer.sent.length = 0; // drop the "hi" the roster provoked
  return [client, peer];
};

const feed = (peer, host, frame) => peer.emit("message", host, structuredClone(frame));

/** Walk a fresh client into `row`'s phase through game3's own frames. */
const inPhase = (row) => {
  const [client, peer] = seated(row.host);
  for (const f of V.drive[row.drive] ?? []) feed(peer, row.host, f);
  return client;
};

/* ============================ the claim comparator ======================== */

test("tessera: claimBeats is live-beats-idle, ties to lowest id (game3's own table)", () => {
  assert.equal(V.claims.length, 42);
  for (const row of V.claims) {
    assert.equal(
      inPhase(row).claimBeats(row.from, structuredClone(row.ro)), row.out,
      `phase=${row.phase} theirLive=${row.theirLive} from=${row.from} host=${row.host}`,
    );
  }
});

test("tessera: the fixture's phases are really reached, and every live phase is covered", () => {
  // The table is worthless if the drive frames leave the client in "idle": walk
  // each drive and read the phase the client actually landed in.
  const seen = new Map();
  for (const row of V.claims) seen.set(row.phase, inPhase(row)._phase);
  assert.deepEqual(Object.fromEntries(seen), {
    idle: "idle",
    lobby: "lobby",
    countdown: "countdown",
    playing: "playing",
    roundover: "roundover",
    matchover: "matchover",
    "playing (late join)": "playing",
  });
  // game3's round-live? — playing | countdown | roundover — as the fixture's
  // incumbentLive column, cross-checked against the phase the client reports
  const LIVE = new Set(["playing", "countdown", "roundover"]);
  for (const row of V.claims) {
    assert.equal(row.incumbentLive, LIVE.has(inPhase(row)._phase), `${row.phase}`);
  }
});

test("tessera: an unknown host is always outranked, and the base rule is genuinely overridden", () => {
  const peer = new MockPeer(V.order[V.seat]);
  const client = new TesseraClient(peer, {
    app: "tessera", roomSecret: "s", nick: "bot", keepAliveMs: 0,
  });
  assert.equal(client.hostId, null);
  assert.equal(client.claimBeats("zzz", { t: "ro", host: "zzz", ps: [] }), true);
  assert.equal(client.claimBeats("zzz", { t: "ro", host: "zzz", ps: [], live: 1 }), true);

  // The rows the plain lowest-id default in room.cljs gets WRONG — proof the
  // override is doing work and not just restating the inherited rule.
  const differs = V.claims.filter((r) => r.out !== (r.from < r.host));
  assert.ok(differs.length >= 10, `expected the tables to diverge, got ${differs.length} rows`);
  for (const row of differs) {
    const c = inPhase(row);
    assert.equal(c.claimBeats(row.from, structuredClone(row.ro)), row.out);
    assert.notEqual(c.claimBeats(row.from, structuredClone(row.ro)), row.from < row.host);
  }
});

test("tessera: the override is the neon-grid/lampion one, not valley-blocks'", () => {
  const src = (C) => C.prototype.claimBeats.toString();
  // game3 names the field `live` and HAS a countdown phase, so the comparator is
  // the sibling one verbatim — compiled function text, so a drifted phase set or
  // field name fails here
  assert.equal(src(TesseraClient), src(NeonGridClient));
  assert.equal(src(TesseraClient), src(LampionClient));
  assert.match(src(TesseraClient), /\.live/);
  assert.match(src(TesseraClient), /countdown/);
  // game2's shape, which tessera must NOT have copied
  assert.notEqual(src(TesseraClient), src(ValleyBlocksClient));
  assert.match(src(ValleyBlocksClient), /\.playing/);
  assert.doesNotMatch(src(ValleyBlocksClient), /countdown/);
  // and it is an own override, not the inherited default
  assert.ok(Object.getOwnPropertyNames(TesseraClient.prototype).includes("claimBeats"));
  assert.notEqual(src(TesseraClient), src(GameRoom));
});

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