/** * Quick HEAD_TO_HEAD: new AI vs legacy AI — 20 seeds × 2 seat swaps = 40 matches each. * Run with: tsx src/game/ai-h2h-quick.ts */ import { chooseMove } from './ai'; import { chooseMove as chooseMoveOld } from './ai-legacy'; import { CardTracker } from './card-tracker'; import { CardInferenceEngine } from './card-inference'; import { applyMove, teamOf, nextPlayer, createInitialState, getMatchOutcome } from './engine'; import { AIMove, Difficulty, GameState, PlayerIndex } from './types'; // ---- seeded RNG ------------------------------------------------------- function mulberry32(seed: number): () => number { let s = seed >>> 0; return () => { s = (s + 0x6d2b79f5) >>> 0; let t = Math.imul(s ^ (s >>> 15), s | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function seedFromParts(...parts: number[]): number { let h = 2166136261; for (const p of parts) { h ^= p >>> 0; h = Math.imul(h, 16777619); } return h >>> 0; } // ---- simulated timing (same as benchmark) ---------------------------- function simulatedTiming() { let t = 0; return { now: () => t, advance: (ms: number) => { t += ms; return t; }, isSimulated: true as const, }; } // ---- single match ---------------------------------------------------- async function runMatch( difficulty: Difficulty, seed: number, newAITeam: 0 | 1, ): Promise<'new' | 'old' | 'draw'> { const SUITE_KEY = 0xabcd1234; const MAX_ROUNDS = 20; const initialDealer = (seed % 4) as PlayerIndex; let state = createInitialState( initialDealer, mulberry32(seedFromParts(SUITE_KEY, seed, 1, 0)), ); const matchStartingPlayer = state.matchStartingPlayer; const tracker = new CardTracker(); const inference = new CardInferenceEngine(tracker); let rounds = 1; let turn = 0; while (rounds <= MAX_ROUNDS) { while (!state.roundOver) { const playerIdx = state.currentPlayer; const isNew = teamOf(playerIdx) === newAITeam; const timing = simulatedTiming(); const rng = mulberry32(seedFromParts(SUITE_KEY, seed, rounds, turn, playerIdx)); const move: AIMove = isNew ? await chooseMove(state, playerIdx, difficulty, tracker, undefined, { rng, timingSource: timing, inference }) : await chooseMoveOld(state, playerIdx, difficulty, tracker, undefined, { rng, timingSource: timing }); const tableBeforeMove = [...state.table]; const { nextState, capture } = applyMove( state, playerIdx, move.card, move.capture.length > 0 ? move.capture : undefined, ); tracker.trackPlay(move.card); if (capture) tracker.trackCapture(capture.captured); inference.onMove(playerIdx, move, tableBeforeMove); state = nextState; turn++; } const outcome = getMatchOutcome(state.teamScores); if (!outcome.continueMatch) break; if (rounds === MAX_ROUNDS) break; rounds++; const totals: [number, number] = [state.teamScores[0].totalPoints, state.teamScores[1].totalPoints]; tracker.reset(); inference.reset(); state = createInitialState( nextPlayer(state.dealer), mulberry32(seedFromParts(SUITE_KEY, seed, rounds, 0)), ); state.matchStartingPlayer = matchStartingPlayer; state.teamScores[0].totalPoints = totals[0]; state.teamScores[1].totalPoints = totals[1]; state.roundNumber = rounds; } const outcome = getMatchOutcome(state.teamScores); if (outcome.winner === null) return 'draw'; return outcome.winner === newAITeam ? 'new' : 'old'; } // ---- main ------------------------------------------------------------ async function main() { const SEEDS = Array.from({ length: 20 }, (_, i) => 3000 + i); const SWAPS = [0, 1] as const; for (const difficulty of ['master', 'advanced'] as Difficulty[]) { let wins = 0, losses = 0, draws = 0; const total = SEEDS.length * SWAPS.length; console.log(`\nH2H ${difficulty.toUpperCase()} — ${total} matches`); for (const seed of SEEDS) { for (const newAITeam of SWAPS) { const result = await runMatch(difficulty, seed, newAITeam); if (result === 'new') wins++; else if (result === 'old') losses++; else draws++; const done = wins + losses + draws; if (done === 1 || done % 10 === 0 || done === total) { const pct = ((wins / done) * 100).toFixed(1); console.log(` [${done}/${total}] new=${wins} old=${losses} draw=${draws} new-win%=${pct}%`); } } } const target = difficulty === 'master' ? 60 : 55; const winRate = (wins / total) * 100; const pass = winRate >= target; console.log(`RESULT: new AI wins ${winRate.toFixed(1)}% — target ≥${target}% — ${pass ? '✓ PASS' : '✗ FAIL'}`); } } main().catch(console.error);