164 lines
4.3 KiB
TypeScript
164 lines
4.3 KiB
TypeScript
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);
|
||
}
|