209 lines
5.5 KiB
TypeScript
209 lines
5.5 KiB
TypeScript
import { query, type Entity, type World } from "../../src/index";
|
||
import {
|
||
ActionCard,
|
||
GameState,
|
||
Player,
|
||
Table,
|
||
Tool,
|
||
WoodenFishMarker,
|
||
getPlayersInSeatOrder,
|
||
getSelectedAction,
|
||
getToolOf,
|
||
type ActionKind,
|
||
type ToolKind,
|
||
} from "./components";
|
||
import type { RoundProposals } from "./rules";
|
||
|
||
export const ACTION_LABELS: Record<ActionKind, string> = {
|
||
fetchWater: "挑水",
|
||
exchange: "交换",
|
||
drink: "喝水",
|
||
chant: "念经",
|
||
rest: "休息",
|
||
};
|
||
|
||
export const TOOL_LABELS: Record<ToolKind, string> = {
|
||
woodenFish: "木鱼",
|
||
bucket: "水桶",
|
||
bottle: "净瓶",
|
||
woodenBucket: "木桶",
|
||
bigBowl: "大碗",
|
||
bigBucket: "大桶",
|
||
mouse: "老鼠",
|
||
shoulderPole: "扁担",
|
||
waterJar: "水缸",
|
||
ladle: "水瓢",
|
||
};
|
||
|
||
export interface PlayerSnapshot {
|
||
entity: Entity;
|
||
name: string;
|
||
seat: number;
|
||
water: number;
|
||
action: ActionKind | null;
|
||
tool: ToolKind;
|
||
toolWater: number;
|
||
hasMarker: boolean;
|
||
}
|
||
|
||
export interface GameSnapshot {
|
||
round: number;
|
||
centralWater: number;
|
||
phase: string;
|
||
winner: Entity | 0;
|
||
players: PlayerSnapshot[];
|
||
}
|
||
|
||
export class GameLog {
|
||
private readonly lines: string[] = [];
|
||
|
||
add(line = ""): void {
|
||
this.lines.push(line);
|
||
}
|
||
|
||
section(title: string): void {
|
||
if (this.lines.length > 0) this.lines.push("");
|
||
this.lines.push(title);
|
||
}
|
||
|
||
entries(): readonly string[] {
|
||
return this.lines;
|
||
}
|
||
|
||
toString(): string {
|
||
return this.lines.join("\n");
|
||
}
|
||
}
|
||
|
||
export function snapshotGame(world: World): GameSnapshot {
|
||
const table = world.getSingleton(Table);
|
||
const marker = world.getSingleton(WoodenFishMarker).holder;
|
||
const state = world.getSingleton(GameState);
|
||
|
||
return {
|
||
round: table.round,
|
||
centralWater: table.centralWater,
|
||
phase: state.phase,
|
||
winner: state.winner,
|
||
players: getPlayersInSeatOrder(world).map((player) => {
|
||
const playerData = world.get(player, Player);
|
||
const toolData = world.get(getToolOf(world, player), Tool);
|
||
return {
|
||
entity: player,
|
||
name: playerData.name,
|
||
seat: playerData.seat,
|
||
water: playerData.water,
|
||
action: getSelectedAction(world, player),
|
||
tool: toolData.kind,
|
||
toolWater: toolData.water,
|
||
hasMarker: player === marker,
|
||
};
|
||
}),
|
||
};
|
||
}
|
||
|
||
export function formatSnapshot(snapshot: GameSnapshot): string[] {
|
||
return [
|
||
`第 ${snapshot.round} 轮 | 中央水=${snapshot.centralWater} | 阶段=${formatPhase(snapshot.phase)}`,
|
||
...snapshot.players.map(formatPlayerSnapshot),
|
||
];
|
||
}
|
||
|
||
export function formatPlayerSnapshot(player: PlayerSnapshot): string {
|
||
const marker = player.hasMarker ? " 🪵" : "";
|
||
const action = player.action ? ` | 行动=${ACTION_LABELS[player.action]}` : "";
|
||
return `${player.name}${marker}: 已喝=${player.water}, 道具=${TOOL_LABELS[player.tool]}(${player.toolWater})${action}`;
|
||
}
|
||
|
||
export function formatProposals(proposals: RoundProposals): string {
|
||
const phases: string[] = [];
|
||
if (proposals.chant) phases.push("念经");
|
||
if (proposals.fetchWater) phases.push("挑水");
|
||
if (proposals.exchange) phases.push("交换");
|
||
if (proposals.drink) phases.push("喝水");
|
||
return phases.length > 0 ? phases.join(" → ") : "无行动阶段";
|
||
}
|
||
|
||
export function formatAvailableActions(world: World, player: Entity): string {
|
||
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);
|
||
}
|
||
return actions.map((action) => ACTION_LABELS[action]).join("、");
|
||
}
|
||
|
||
export function logSnapshot(log: GameLog, world: World): void {
|
||
for (const line of formatSnapshot(snapshotGame(world))) {
|
||
log.add(line);
|
||
}
|
||
}
|
||
|
||
export function logPhaseDelta(
|
||
log: GameLog,
|
||
label: string,
|
||
before: GameSnapshot,
|
||
after: GameSnapshot,
|
||
): void {
|
||
const changes: string[] = [];
|
||
|
||
if (before.centralWater !== after.centralWater) {
|
||
changes.push(`中央水 ${before.centralWater}→${after.centralWater}`);
|
||
}
|
||
|
||
for (const beforePlayer of before.players) {
|
||
const afterPlayer = after.players.find(
|
||
(p) => p.entity === beforePlayer.entity,
|
||
)!;
|
||
const playerChanges: string[] = [];
|
||
|
||
if (beforePlayer.water !== afterPlayer.water) {
|
||
playerChanges.push(`已喝 ${beforePlayer.water}→${afterPlayer.water}`);
|
||
}
|
||
if (
|
||
beforePlayer.tool !== afterPlayer.tool ||
|
||
beforePlayer.toolWater !== afterPlayer.toolWater
|
||
) {
|
||
playerChanges.push(
|
||
`道具 ${TOOL_LABELS[beforePlayer.tool]}(${beforePlayer.toolWater})→${TOOL_LABELS[afterPlayer.tool]}(${afterPlayer.toolWater})`,
|
||
);
|
||
}
|
||
if (beforePlayer.hasMarker !== afterPlayer.hasMarker) {
|
||
playerChanges.push(
|
||
afterPlayer.hasMarker ? "获得木鱼标记" : "失去木鱼标记",
|
||
);
|
||
}
|
||
|
||
if (playerChanges.length > 0) {
|
||
changes.push(`${afterPlayer.name}: ${playerChanges.join(",")}`);
|
||
}
|
||
}
|
||
|
||
if (changes.length === 0) {
|
||
log.add(` ${label}: 无变化`);
|
||
} else {
|
||
log.add(` ${label}: ${changes.join(";")}`);
|
||
}
|
||
}
|
||
|
||
export function formatPhase(phase: string): string {
|
||
switch (phase) {
|
||
case "setup":
|
||
return "设置";
|
||
case "selecting":
|
||
return "出牌";
|
||
case "resolving":
|
||
return "结算";
|
||
case "gameOver":
|
||
return "游戏结束";
|
||
default:
|
||
return phase;
|
||
}
|
||
}
|
||
|
||
export function winnerName(world: World): string | null {
|
||
const winner = world.getSingleton(GameState).winner;
|
||
if (winner === 0) return null;
|
||
return world.get(winner, Player).name;
|
||
}
|