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