136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
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();
|
||
}
|