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

136 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { World, query, type Entity } from "../../src/index";
import {
ActionCard,
GameState,
Player,
Table,
setupGame,
getPlayersInSeatOrder,
type ActionKind,
} from "./components";
import { ToolEffectStats, formatToolEffectSummaries } from "./stats";
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, 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);
}
const proposals = collectProposals(world);
if (proposals.chant) resolveChant(world, stats);
if (proposals.fetchWater) resolveFetchWater(world, stats);
if (proposals.exchange) resolveExchange(world, stats);
if (proposals.drink) resolveDrink(world, stats);
if (world.getSingleton(GameState).phase === "gameOver") break;
resolveEndOfRoundTools(world, stats);
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;
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();
}