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