130 lines
3.5 KiB
TypeScript
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();
|
|
}
|