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

152 lines
4.1 KiB
TypeScript
Raw Normal View History

import { World, query, type Entity } from "../../src/index";
import {
ActionCard,
GameState,
Player,
Table,
setupGame,
type ActionKind,
} from "./components";
import {
beginSelectionPhase,
collectProposals,
prepareNextRound,
selectAction,
chantPhase,
fetchWaterPhase,
exchangePhase,
drinkPhase,
endOfRoundPhase,
} 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)}`);
applyLoggedPhase(log, world, "念经", () => chantPhase(world));
applyLoggedPhase(log, world, "挑水", () => fetchWaterPhase(world));
applyLoggedPhase(log, world, "交换", () => exchangePhase(world));
applyLoggedPhase(log, world, "喝水", () => drinkPhase(world));
if (world.getSingleton(GameState).phase === "gameOver") break;
applyLoggedPhase(log, world, "轮末道具", () => endOfRoundPhase(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);
}