ecs-observable/examples/three-monks/simulate.ts

130 lines
3.5 KiB
TypeScript

import { World, query, type Entity } from "../../src/index";
import {
ActionCard,
GameState,
Player,
Table,
setupGame,
getPlayersInSeatOrder,
type ActionKind,
} from "./components";
import {
beginSelectionPhase,
checkWinner,
collectProposals,
prepareNextRound,
resolveChant,
resolveDrink,
resolveEndOfRoundTools,
resolveExchange,
resolveFetchWater,
selectAction,
} 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<T>(items: readonly T[], random: () => number): T {
return items[Math.floor(random() * items.length)];
}
interface GameStats {
rounds: number;
waterDiff: number;
}
function runSingleGame(seed: number): 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);
}
const proposals = collectProposals(world);
if (proposals.chant) resolveChant(world);
if (proposals.fetchWater) resolveFetchWater(world);
if (proposals.exchange) resolveExchange(world);
if (proposals.drink) resolveDrink(world);
if (world.getSingleton(GameState).phase === "gameOver") break;
resolveEndOfRoundTools(world);
checkWinner(world);
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;
console.log(`开始模拟 ${gameCount} 局游戏...`);
for (let i = 0; i < gameCount; i++) {
const seed = startSeed + i;
const stats = runSingleGame(seed);
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("==========================================");
}
if (process.argv[1]?.endsWith("simulate.ts")) {
runSimulation();
}