// Baseline harness for CB-RES-0001 — boardgame.io v0.50.2, headless client. // Measures: (1) throughput of applying moves to authoritative state, // (2) determinism across two seeded runs, (3) commit/reveal-style stage flow. // Fidelity note: synthetic GROUND-shaped workload (3 players, relationship // map, per-player hands, commit list), not a real GROUND implementation. // Run: node bench.js [numMoves] const { Client } = require('boardgame.io/client'); const { Stage } = require('boardgame.io/core'); const crypto = require('crypto'); const NUM_MOVES = parseInt(process.argv[2] || '100000', 10); const DISABLE_UNDO = process.argv.includes('--disable-undo'); const SINGLE_RUN = process.argv.includes('--single-run'); // isolated memory measurement const NUM_PLAYERS = 3; const SyntheticGround = { name: 'synthetic-ground', setup: ({ ctx }) => ({ round: 0, relationships: {}, // "a->b" -> {kind, strength} hands: Object.fromEntries( Array.from({ length: ctx.numPlayers }, (_, i) => [String(i), Array.from({ length: 7 }, (_, c) => c)]) ), committed: {}, revealed: [], score: Object.fromEntries(Array.from({ length: ctx.numPlayers }, (_, i) => [String(i), 0])), }), moves: { commitAction: ({ G, playerID, random }, targetOffset) => { const target = String((Number(playerID) + targetOffset) % NUM_PLAYERS); const card = G.hands[playerID].length > 0 ? G.hands[playerID][0] : random.D6(); G.committed[playerID] = { target, card }; }, resolveAll: ({ G, random }) => { const order = Object.keys(G.committed).sort(); for (const p of order) { const { target, card } = G.committed[p]; const key = `${p}->${target}`; const rel = G.relationships[key] || { kind: 'rivalry', strength: 0 }; rel.strength = (rel.strength + card + random.D6()) % 10; G.relationships[key] = rel; G.score[p] += rel.strength; G.revealed.push({ p, target, card }); if (G.revealed.length > 50) G.revealed.shift(); } G.committed = {}; G.round += 1; }, }, turn: { activePlayers: { all: Stage.NULL } }, }; function makeClient(seed) { return Client({ game: { ...SyntheticGround, seed, disableUndo: DISABLE_UNDO }, numPlayers: NUM_PLAYERS, playerID: '0', }); } function stateHash(client) { const { G } = client.getState(); return crypto.createHash('sha256').update(JSON.stringify(G)).digest('hex'); } function run(seed, numMoves, collectHash) { const client = makeClient(seed); // Drive all players through commit windows via events/moves. // Each "round" = NUM_PLAYERS commitAction moves + 1 resolveAll. const rounds = Math.floor(numMoves / (NUM_PLAYERS + 1)); const t0 = process.hrtime.bigint(); for (let r = 0; r < rounds; r++) { for (let p = 0; p < NUM_PLAYERS; p++) { client.updatePlayerID(String(p)); client.moves.commitAction(1 + (r % (NUM_PLAYERS - 1))); } client.updatePlayerID('0'); client.moves.resolveAll(); } const t1 = process.hrtime.bigint(); const elapsedMs = Number(t1 - t0) / 1e6; const applied = rounds * (NUM_PLAYERS + 1); return { applied, elapsedMs, movesPerSec: Math.round(applied / (elapsedMs / 1000)), hash: collectHash ? stateHash(client) : null, finalRound: client.getState().G.round, }; } // --- throughput --- if (!SINGLE_RUN) run(42, Math.min(NUM_MOVES, 5000), false); // warmup/JIT const main = run(42, NUM_MOVES, true); const memAfterMain = process.memoryUsage(); // --- determinism: same seed twice, different seed once --- const detA = SINGLE_RUN ? {hash: 'skipped'} : run(7, 4000, true); const detB = SINGLE_RUN ? {hash: 'skipped'} : run(7, 4000, true); const detC = SINGLE_RUN ? {hash: 'x'} : run(8, 4000, true); // --- memory --- const mem = process.memoryUsage(); const result = { library: 'boardgame.io@0.50.2', node: process.version, workload: `synthetic-ground ${NUM_PLAYERS}p commit/reveal`, applied_moves: main.applied, elapsed_ms: Math.round(main.elapsedMs), moves_per_sec: main.movesPerSec, final_round: main.finalRound, determinism_same_seed: detA.hash === detB.hash, determinism_diff_seed_differs: detA.hash !== detC.hash, rss_mb: Math.round(mem.rss / 1048576), heap_mb: Math.round(mem.heapUsed / 1048576), rss_after_main_mb: Math.round(memAfterMain.rss / 1048576), disable_undo: DISABLE_UNDO, single_run: SINGLE_RUN, }; console.log(JSON.stringify(result, null, 2));