ecs-observable/examples/three-monks/random-playthrough.ts

164 lines
4.3 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,
type ActionKind,
} from "./components";
import {
beginSelectionPhase,
checkWinner,
collectProposals,
prepareNextRound,
resolveChant,
resolveDrink,
resolveEndOfRoundTools,
resolveExchange,
resolveFetchWater,
selectAction,
} from "./rules";
import {
ACTION_LABELS,
GameLog,
formatAvailableActions,
formatProposals,
logPhaseDelta,
logSnapshot,
snapshotGame,
winnerName,
} from "./logging";
export interface RandomPlaythroughOptions {
seed?: number;
playerNames?: readonly string[];
maxRounds?: number;
}
export interface RandomPlaythroughResult {
world: World;
log: string;
roundsPlayed: number;
}
export function generateRandomPlayLog(
options: RandomPlaythroughOptions = {},
): RandomPlaythroughResult {
const seed = options.seed ?? 20260701;
const random = mulberry32(seed);
const playerNames = options.playerNames ?? ["慧空", "明心", "了尘", "净远"];
const maxRounds = options.maxRounds ?? 200;
const world = new World();
const players = setupGame(world, playerNames, random);
const log = new GameLog();
log.section(`三个和尚随机对局(种子=${seed}`);
log.add(`玩家:${playerNames.join("、")}`);
log.add("初始状态:");
logSnapshot(log, world);
let roundsPlayed = 0;
while (world.getSingleton(GameState).phase !== "gameOver") {
if (roundsPlayed >= maxRounds) {
throw new Error(`随机对局未能在 ${maxRounds} 轮内结束`);
}
const round = world.getSingleton(Table).round;
log.section(`${round}`);
beginSelectionPhase(world);
for (const player of players) {
const action = pickRandom(availableActions(world, player), random);
const playerName = world.get(player, Player).name;
log.add(
` ${playerName} 选择 ${ACTION_LABELS[action]}(可选:${formatAvailableActions(world, player)}`,
);
selectAction(world, player, action);
}
const proposals = collectProposals(world);
log.add(` 本轮行动阶段:${formatProposals(proposals)}`);
if (proposals.chant) {
applyLoggedPhase(log, world, "念经", () => resolveChant(world));
}
if (proposals.fetchWater) {
applyLoggedPhase(log, world, "挑水", () => resolveFetchWater(world));
}
if (proposals.exchange) {
applyLoggedPhase(log, world, "交换", () => resolveExchange(world));
}
if (proposals.drink) {
applyLoggedPhase(log, world, "喝水", () => resolveDrink(world));
}
if (world.getSingleton(GameState).phase === "gameOver") break;
applyLoggedPhase(log, world, "轮末道具", () =>
resolveEndOfRoundTools(world),
);
checkWinner(world);
if (world.getSingleton(GameState).phase === "gameOver") break;
prepareNextRound(world);
log.add(" 轮末状态:");
logSnapshot(log, world);
roundsPlayed++;
}
roundsPlayed++;
log.section("游戏结束");
log.add(`胜者:${winnerName(world)}`);
logSnapshot(log, world);
return { world, log: log.toString(), roundsPlayed };
}
function applyLoggedPhase(
log: GameLog,
world: World,
label: string,
phase: () => void,
): void {
const before = snapshotGame(world);
phase();
const after = snapshotGame(world);
logPhaseDelta(log, label, before, after);
}
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);
}
}
if (actions.length === 0) {
throw new Error(`${world.get(player, Player).name} 没有可选行动牌`);
}
return actions;
}
function pickRandom<T>(items: readonly T[], random: () => number): T {
return items[Math.floor(random() * items.length)];
}
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;
};
}
if (process.argv[1]?.endsWith("random-playthrough.ts")) {
const result = generateRandomPlayLog();
console.log(result.log);
}