ecs-observable/examples/three-monks/logging.ts

209 lines
5.5 KiB
TypeScript
Raw Permalink Normal View History

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