|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * Generate a deterministic fixture transcript for Lost Cities replay testing. |
| 4 | + * |
| 5 | + * Runs a full AI-vs-AI Lost Cities match (3 rounds) with fixed seeds |
| 6 | + * and writes the resulting transcript JSON to |
| 7 | + * tests/fixtures/transcripts/lost-cities/fixture-game.json. |
| 8 | + * |
| 9 | + * Usage: |
| 10 | + * npx tsx scripts/generate-lc-fixture-transcript.ts |
| 11 | + */ |
| 12 | + |
| 13 | +import { |
| 14 | + setupLostCitiesGame, |
| 15 | + executeAction, |
| 16 | + getVisibleState, |
| 17 | +} from '../example-games/lost-cities/LostCitiesGame'; |
| 18 | +import type { LostCitiesSession, PlayerId } from '../example-games/lost-cities/LostCitiesGame'; |
| 19 | +import { LCTranscriptRecorder } from '../example-games/lost-cities/GameTranscript'; |
| 20 | +import { LostCitiesAiPlayer, GreedyStrategy } from '../example-games/lost-cities/AiStrategy'; |
| 21 | +import type { TurnPhase } from '../example-games/lost-cities/LostCitiesRules'; |
| 22 | +import { writeFileSync, mkdirSync } from 'fs'; |
| 23 | +import { dirname, resolve } from 'path'; |
| 24 | + |
| 25 | +// Deterministic RNG (same LCG as tests) |
| 26 | +function createRng(seed: number): () => number { |
| 27 | + let s = seed; |
| 28 | + return () => { |
| 29 | + s = (s * 1664525 + 1013904223) % 4294967296; |
| 30 | + return s / 4294967296; |
| 31 | + }; |
| 32 | +} |
| 33 | + |
| 34 | +const session: LostCitiesSession = setupLostCitiesGame({ |
| 35 | + playerNames: ['Player 1', 'Player 2'], |
| 36 | + isAI: [true, true], |
| 37 | + rng: createRng(42), |
| 38 | +}); |
| 39 | + |
| 40 | +const recorder = new LCTranscriptRecorder(session, ['greedy', 'greedy']); |
| 41 | +const ai0 = new LostCitiesAiPlayer(GreedyStrategy, createRng(100)); |
| 42 | +const ai1 = new LostCitiesAiPlayer(GreedyStrategy, createRng(200)); |
| 43 | + |
| 44 | +let actionCount = 0; |
| 45 | +const maxActions = 2000; // Safety limit (3 rounds × ~40 turns × 2 phases ≈ ~240 actions) |
| 46 | + |
| 47 | +while (session.matchPhase === 'playing' && actionCount < maxActions) { |
| 48 | + const playerIndex: PlayerId = session.round.currentPlayer; |
| 49 | + const phase: TurnPhase = session.round.turnPhase; |
| 50 | + const ai = playerIndex === 0 ? ai0 : ai1; |
| 51 | + const state = getVisibleState(session, playerIndex); |
| 52 | + |
| 53 | + let action; |
| 54 | + if (phase === 'PlayOrDiscard') { |
| 55 | + action = ai.choosePhase1(state); |
| 56 | + } else { |
| 57 | + action = ai.choosePhase2(state); |
| 58 | + } |
| 59 | + |
| 60 | + const result = executeAction(session, action); |
| 61 | + recorder.recordAction(session, result, action, phase); |
| 62 | + actionCount++; |
| 63 | + |
| 64 | + // Reset AI round history when a new round starts |
| 65 | + if (result.roundEnded && !result.matchEnded) { |
| 66 | + ai0.resetRoundHistory(); |
| 67 | + ai1.resetRoundHistory(); |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +if (session.matchPhase !== 'match-over') { |
| 72 | + console.error(`Match did not finish after ${maxActions} actions`); |
| 73 | + process.exit(1); |
| 74 | +} |
| 75 | + |
| 76 | +const transcript = recorder.finalize(session); |
| 77 | + |
| 78 | +// Override timestamps for reproducibility |
| 79 | +transcript.metadata.startedAt = '2026-01-01T00:00:00.000Z'; |
| 80 | +transcript.metadata.endedAt = '2026-01-01T00:15:00.000Z'; |
| 81 | + |
| 82 | +const outPath = resolve('tests/fixtures/transcripts/lost-cities/fixture-game.json'); |
| 83 | +mkdirSync(dirname(outPath), { recursive: true }); |
| 84 | +writeFileSync(outPath, JSON.stringify(transcript, null, 2) + '\n'); |
| 85 | + |
| 86 | +const totalActions = transcript.rounds.reduce( |
| 87 | + (sum, r) => sum + r.actions.length, |
| 88 | + 0, |
| 89 | +); |
| 90 | + |
| 91 | +console.log(`Fixture transcript written to ${outPath}`); |
| 92 | +console.log(` Rounds: ${transcript.rounds.length}`); |
| 93 | +console.log(` Total actions: ${totalActions}`); |
| 94 | +console.log( |
| 95 | + ` Winner: ${transcript.results!.winnerName} (${transcript.results!.finalScores.join('-')})`, |
| 96 | +); |
0 commit comments