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

209 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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