Skip to content

Commit f8da388

Browse files
committed
CG-0MM0GQFZA1WQKILP: Add fixture transcript, generator script, and placeholder thumbnail
- Create scripts/generate-lc-fixture-transcript.ts: deterministic AI-vs-AI game runner using GreedyStrategy with fixed seeds (42/100/200) - Generate tests/fixtures/transcripts/lost-cities/fixture-game.json: 3-round match, 278 actions, deterministic and reproducible - Add public/assets/games/lost-cities/thumbnail.png: 120x68 placeholder (solid dark teal, to be replaced by replay-generated screenshot)
1 parent 586dcc5 commit f8da388

3 files changed

Lines changed: 87185 additions & 0 deletions

File tree

250 Bytes
Loading
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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

Comments
 (0)