import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { mix, playToWin, deviates, decide, DEFAULT_BAND } from './strategy'; // Conformance gate for the ported robot strategy: the client must fold the game seed and // choose its action exactly as backend/internal/robot/strategy.go does, so a local vs_ai // game plays the same. Golden fixtures come from the in-package emitter // (backend/internal/robot/strategyfixture_test.go). interface MixCase { seed: string; salt: string; nums: number[]; value: string; } interface DecisionCase { seed: string; moveCount: number; myScore: number; oppScore: number; candScores: number[]; rackLen: number; bagLen: number; playToWin: boolean; win: boolean; kind: string; index: number; } interface Fixture { playToWinPercent: number; mix: MixCase[]; decisions: DecisionCase[]; } const fx = JSON.parse( readFileSync(new URL('./testdata/strategy.json', import.meta.url), 'utf8'), ) as Fixture; describe('robot strategy parity vs Go', () => { it('mix folds the seed like Go FNV-1a', () => { for (const c of fx.mix) { expect(mix(BigInt(c.seed), c.salt, ...c.nums), `${c.seed}/${c.salt}/${c.nums}`).toBe(BigInt(c.value)); } }); it('chooses the same action as the Go robot', () => { for (const c of fx.decisions) { const seed = BigInt(c.seed); const cands = c.candScores.map((score, index) => ({ score, index })); const rack = new Array(c.rackLen).fill('a'); expect(playToWin(seed), `playToWin ${c.seed}`).toBe(c.playToWin); // the per-turn intent after the occasional off-strategy deviation let win = playToWin(seed); if (deviates(seed, c.moveCount, c.bagLen)) win = !win; expect(win, `win ${c.seed}/${c.moveCount}/${c.bagLen}`).toBe(c.win); const dec = decide(seed, c.moveCount, cands, c.myScore, c.oppScore, rack, c.bagLen, DEFAULT_BAND); expect(dec.kind, `kind ${JSON.stringify(c)}`).toBe(c.kind); if (dec.kind === 'play') { expect(dec.move.index, `index ${JSON.stringify(c)}`).toBe(c.index); } else if (dec.kind === 'exchange') { expect(dec.exchange.length).toBe(c.rackLen); } } }); });