import { World, query, type Entity } from "../../src/index"; import { ActionCard, GameState, Player, Table, setupGame, type ActionKind, } from "./components"; import { ToolEffectStats, formatToolEffectSummaries } from "./stats"; import { beginSelectionPhase, prepareNextRound, selectAction, chantPhase, fetchWaterPhase, exchangePhase, drinkPhase, endOfRoundPhase, } from "./rules"; function mulberry32(seed: number): () => number { let state = seed >>> 0; return () => { state += 0x6d2b79f5; let t = state; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function availableActions(world: World, player: Entity): ActionKind[] { const actions: ActionKind[] = []; for (const card of world.query(query(ActionCard))) { const data = world.get(card, ActionCard); if (data.owner === player && data.zone === "hand") { actions.push(data.kind); } } return actions; } function pickRandom(items: readonly T[], random: () => number): T { return items[Math.floor(random() * items.length)]; } interface GameStats { rounds: number; waterDiff: number; } function runSingleGame(seed: number, stats?: ToolEffectStats): GameStats { const random = mulberry32(seed); const world = new World(); const playerNames = ["慧空", "明心", "了尘", "净远"]; const players = setupGame(world, playerNames, random); let roundsPlayed = 0; const maxRounds = 1000; while (world.getSingleton(GameState).phase !== "gameOver") { if (roundsPlayed >= maxRounds) { throw new Error(`Game did not finish within ${maxRounds} rounds`); } beginSelectionPhase(world); for (const player of players) { const actions = availableActions(world, player); const action = pickRandom(actions, random); selectAction(world, player, action); } chantPhase(world, stats); fetchWaterPhase(world, stats); exchangePhase(world, stats); drinkPhase(world, stats); if (world.getSingleton(GameState).phase === "gameOver") break; endOfRoundPhase(world, stats); if (world.getSingleton(GameState).phase === "gameOver") break; prepareNextRound(world); roundsPlayed++; } const finalRound = world.getSingleton(Table).round; const waterCounts = players.map((p) => world.get(p, Player).water); const maxWater = Math.max(...waterCounts); const minWater = Math.min(...waterCounts); return { rounds: finalRound, waterDiff: maxWater - minWater, }; } export function runSimulation(gameCount = 100, startSeed = 20260701): void { let totalRounds = 0; let totalWaterDiff = 0; const toolStats = new ToolEffectStats(); console.log(`开始模拟 ${gameCount} 局游戏...`); for (let i = 0; i < gameCount; i++) { const seed = startSeed + i; const stats = runSingleGame(seed, toolStats); totalRounds += stats.rounds; totalWaterDiff += stats.waterDiff; } const avgRounds = totalRounds / gameCount; const avgWaterDiff = totalWaterDiff / gameCount; console.log("\n================ 统计结果 ================"); console.log(`模拟总局数: ${gameCount}`); console.log(`结束时平均轮数: ${avgRounds.toFixed(2)} 轮`); console.log(`结束时首尾玩家平均水差距: ${avgWaterDiff.toFixed(2)} 口水`); console.log("=========================================="); console.log("\n道具效果统计(总变化量):"); for (const line of formatToolEffectSummaries(toolStats)) { console.log(line); } } if (process.argv[1]?.endsWith("simulate.ts")) { runSimulation(); }