peer-kit / test / vectors / generate-tessera.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
/**
 * Provenance generator for test/vectors/tessera-client.json — the fixture that
 * pins TesseraClient's host-claim comparator (peer-kit 2.0.2).
 *
 * The ground truth is the OTHER side of the wire: game3's ClojureScript client,
 * which is the spec of record for tessera's protocol. Every row of the claim
 * table is game3's own `sim/bystander-claim-wins` answer, and every frame the
 * test replays — the "ro" claim included — is built by game3's own
 * `game/frames.cljs` builders. So the fixture cannot encode a misreading of
 * game3, only game3.
 *
 * The phase column is game3's own state machine, read off game/game.cljs:
 * apply-start -> "countdown", apply-tick -> "playing" (from countdown),
 * apply-over -> "roundover", apply-match -> "matchover", to-lobby -> "lobby",
 * apply-snap -> "playing"; and `round-live?` is
 * playing | countdown | roundover.
 *
 * Point GAME3_TESTLIB at a game3 checkout's compiled test entry
 * (client/test-dist/testlib.js, written by `npm test` in that repo) and run:
 *
 *   GAME3_TESTLIB=<path to game3 client/test-dist/testlib.js> \
 *     node test/vectors/generate-tessera.mjs
 *
 * Nothing is transcribed here: the constants below are the ones game3's
 * config.cljs publishes on the wire in "st"/"sn", and they only have to be
 * plausible for a frame — the comparator never reads them.
 */
import { writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const testlib = process.env.GAME3_TESTLIB;
if (!testlib) {
  console.error("set GAME3_TESTLIB to a game3 checkout's client/test-dist/testlib.js");
  process.exit(1);
}
const lib = await import(testlib);

const OUT = dirname(fileURLToPath(import.meta.url));
const clone = (x) => JSON.parse(JSON.stringify(x));

/* ---------------------------------------------------------------- config ---- */

// game3's config.cljs, as it appears in the frames below.
const GW = 40;
const GH = 30;
const TICK_MS = 125;
const ROUND_TICKS = 360;

const IDS = [
  "12D3KooWAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "12D3KooWBbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "12D3KooWCcccccccccccccccccccccccccccccccccccccccccccc",
];
const SEAT = 0; // the seat the bot under test sits in

/* ----------------------------------------- the drive frames, game3's own ---- */

// The frames that walk a client into each phase, built by game3's own frame
// builders — so the test reaches every row through the wire instead of poking
// `_phase`, and the field names are game3's rather than ours.
const starts = lib.spawnStarts(IDS.length);
const st = clone(lib.mkSt(1, TICK_MS, ROUND_TICKS, IDS.slice(), starts));
const tk = clone(lib.mkTk(1, [[SEAT, starts[SEAT][0], starts[SEAT][1]]], null, null));
const ov = clone(lib.mkOv(null, IDS.map(() => 0), IDS.map((id) => [id, 0])));
const ma = clone(lib.mkMa(IDS[1], IDS.map((id) => [id, 1])));
const lo = clone(lib.mkLo());
// a late join mid-round: game3's send-snap shape (ps = [x y color alive],
// runs = the RLE'd mosaic, bl = [[x y] ...])
const sn = clone(lib.mkSn(
  1, TICK_MS, ROUND_TICKS, 12, IDS.slice(),
  starts.map((s) => [s[0], s[1], 0, 1]),
  lib.rleEncode(new Uint8Array(GW * GH)),
  [[5, 7]],
));

const DRIVE = { st: [st], tk: [st, tk], ov: [ov], ma: [ma], lo: [lo], sn: [sn] };

/* ------------------------------------------------------------------- claims -- */

// The host-election truth table, answered by game3's OWN comparator
// (sim/bystander-claim-wins) — the rule a tessera browser applies to a foreign
// self-claim while it follows another host. `drive` names the DRIVE sequence
// that puts a TesseraClient into that phase; `ro` is the claim frame itself,
// built by game3's mk-ro (which is what puts `live: 1` on the wire).
const PHASES = [
  { phase: "idle", live: false, drive: null },
  { phase: "lobby", live: false, drive: "lo" },
  { phase: "countdown", live: true, drive: "st" },
  { phase: "playing", live: true, drive: "tk" },
  { phase: "roundover", live: true, drive: "ov" },
  { phase: "matchover", live: false, drive: "ma" },
  { phase: "playing (late join)", live: true, drive: "sn" },
];
const claims = [];
for (const p of PHASES) {
  for (const theirLive of [true, false]) {
    for (const [from, host] of [["aaa", "bbb"], ["bbb", "aaa"], ["mmm", "mmm"]]) {
      claims.push({
        phase: p.phase, drive: p.drive, incumbentLive: p.live, theirLive, from, host,
        ro: clone(lib.mkRo(from, [], theirLive)),
        out: Boolean(lib.bystanderClaimWins(theirLive, p.live, from, host)),
      });
    }
  }
}

/* -------------------------------------------------------------------- write -- */

const data = {
  _note:
    "Ground truth imported from game3's compiled CLJS client: " +
    "game/sim.cljs bystanderClaimWins + spawnStarts/rleEncode, and the " +
    "game/frames.cljs builders mkRo/mkSt/mkTk/mkOv/mkMa/mkLo/mkSn. The phase " +
    "column is game3 game/game.cljs's own state machine (apply-start/-tick/" +
    "-over/-match/-snap, to-lobby) over its round-live?. See generate-tessera.mjs.",
  GW, GH, tickMs: TICK_MS, roundTicks: ROUND_TICKS,
  seat: SEAT,
  order: IDS,
  drive: DRIVE,
  claims,
};

writeFileSync(join(OUT, "tessera-client.json"), `${JSON.stringify(data)}\n`);
console.log("wrote tessera-client.json");
console.log("claim rows:", claims.length,
  "| live phases:", [...new Set(claims.filter((r) => r.incumbentLive).map((r) => r.phase))].join(","),
  "| rows the plain lowest-id rule gets wrong:",
  claims.filter((r) => r.out !== (r.from < r.host)).length);

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