The demo bot draws from the seed, the round and the seat

This commit is contained in:
Eric Wagoner
2026-09-23 11:01:32 -04:00
parent f1c9b29e66
commit 457a536f58
2 changed files with 13 additions and 6 deletions
+8 -1
View File
@@ -16,10 +16,17 @@ describe('High Card', () => {
expect(game.needsInput(s, 'A')).toBe(false); 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 a = createGame({ A: 'Ann', B: 'Bo' }, 42);
const b = createGame({ A: 'Ann', B: 'Bo' }, 42); const b = createGame({ A: 'Ann', B: 'Bo' }, 42);
expect(game.botInput(a, 'B')).toEqual(game.botInput(b, 'B')); expect(game.botInput(a, 'B')).toEqual(game.botInput(b, 'B'));
const picks = new Set<number>();
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', () => { it('stamps the rules revision', () => {
+5 -5
View File
@@ -33,12 +33,11 @@ export interface State {
export type Input = { pick: number }; export type Input = { pick: number };
/** Mulberry32: a small seeded generator, so a game replays to the same bot picks. */ /** Mulberry32 on a seed: a small generator, so a game replays to the same bot picks. */
function next(state: State): number { function random(seed: number): number {
let t = (state.rng += 0x6d2b79f5) >>> 0; let t = (seed + 0x6d2b79f5) >>> 0;
t = Math.imul(t ^ (t >>> 15), t | 1); t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61); t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
state.rng = state.rng >>> 0;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296; return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
} }
@@ -86,7 +85,8 @@ export const game: GameSpec<State, Input> = {
turn: (state) => state.rounds.length + 1, turn: (state) => state.rounds.length + 1,
// Every finished round is public; nothing is hidden, so the gallery sees what a seat sees. // 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)), 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) => { cleanInput: (raw) => {
const pick = Number((raw as { pick?: unknown })?.pick); const pick = Number((raw as { pick?: unknown })?.pick);
return { pick: Number.isInteger(pick) && pick >= 1 && pick <= HIGHEST ? pick : 1 }; return { pick: Number.isInteger(pick) && pick >= 1 && pick <= HIGHEST ? pick : 1 };