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);
|
|||
|
|
}
|