From 457a536f58209be583be0f68ec1c29f217c9f269 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Wed, 23 Sep 2026 11:01:32 -0400 Subject: [PATCH] The demo bot draws from the seed, the round and the seat --- template/src/lib/game/index.test.ts | 9 ++++++++- template/src/lib/game/index.ts | 10 +++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/template/src/lib/game/index.test.ts b/template/src/lib/game/index.test.ts index edca79d..6adefe2 100644 --- a/template/src/lib/game/index.test.ts +++ b/template/src/lib/game/index.test.ts @@ -16,10 +16,17 @@ describe('High Card', () => { expect(game.needsInput(s, 'A')).toBe(false); }); - it('replays to the same bot picks from the same seed', () => { + it('draws the same bot picks from the same seed, and different ones as rounds pass', () => { const a = createGame({ A: 'Ann', B: 'Bo' }, 42); const b = createGame({ A: 'Ann', B: 'Bo' }, 42); expect(game.botInput(a, 'B')).toEqual(game.botInput(b, 'B')); + const picks = new Set(); + let s = a; + for (let i = 0; i < 12; i++) { + picks.add(game.botInput(s, 'B').pick); + s = resolveRound(s, { A: { pick: 1 }, B: game.botInput(s, 'B') }); + } + expect(picks.size).toBeGreaterThan(1); }); it('stamps the rules revision', () => { diff --git a/template/src/lib/game/index.ts b/template/src/lib/game/index.ts index 6a507c0..1b00c8c 100644 --- a/template/src/lib/game/index.ts +++ b/template/src/lib/game/index.ts @@ -33,12 +33,11 @@ export interface State { export type Input = { pick: number }; -/** Mulberry32: a small seeded generator, so a game replays to the same bot picks. */ -function next(state: State): number { - let t = (state.rng += 0x6d2b79f5) >>> 0; +/** Mulberry32 on a seed: a small generator, so a game replays to the same bot picks. */ +function random(seed: number): number { + let t = (seed + 0x6d2b79f5) >>> 0; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); - state.rng = state.rng >>> 0; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; } @@ -86,7 +85,8 @@ export const game: GameSpec = { turn: (state) => state.rounds.length + 1, // Every finished round is public; nothing is hidden, so the gallery sees what a seat sees. view: (state, viewer) => (viewer === SPECTATOR ? structuredClone(state) : structuredClone(state)), - botInput: (state) => ({ pick: 1 + Math.floor(next(structuredClone(state)) * HIGHEST) }), + // A function of the state alone: the seed, the round and the seat, so a replay draws the same pick. + botInput: (state, seat) => ({ pick: 1 + Math.floor(random(state.rng + state.rounds.length * 7919 + state.seats.indexOf(seat)) * HIGHEST) }), cleanInput: (raw) => { const pick = Number((raw as { pick?: unknown })?.pick); return { pick: Number.isInteger(pick) && pick >= 1 && pick <= HIGHEST ? pick : 1 };