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

373 lines
10 KiB
TypeScript

import type { Entity, World } from "../../src/index";
import { query } from "../../src/query";
import type { ToolEffectRecorder } from "./stats";
import {
ActionCard,
CARRYING_TOOLS,
GameState,
Player,
Table,
Tool,
WoodenFishMarker,
allPlayersSelected,
getActionCard,
getLeftPlayer,
getPlayersFromMarker,
getPlayersInSeatOrder,
getRightPlayer,
getSelectedAction,
getToolOf,
type ActionKind,
} from "./components";
export interface RoundProposals {
fetchWater: boolean;
exchange: boolean;
drink: boolean;
chant: boolean;
}
export function beginSelectionPhase(world: World): void {
const state = world.getSingleton(GameState);
if (state.phase !== "gameOver") {
state.phase = "selecting";
state.message = `Round ${world.getSingleton(Table).round}: choose an action card.`;
}
}
export function selectAction(
world: World,
player: Entity,
kind: ActionKind,
): void {
const state = world.getSingleton(GameState);
if (state.phase !== "selecting") return;
if (!world.has(player, Player)) throw new Error("Invalid player");
const card = getActionCard(world, player, kind);
if (!card) throw new Error("Player does not have that action card");
const cardData = world.get(card, ActionCard);
if (cardData.zone === "cooldown") {
throw new Error("Cannot select the action card played last round");
}
for (const other of world.query(query(ActionCard))) {
const data = world.get(other, ActionCard);
if (data.owner === player && data.zone === "selected") {
data.zone = "hand";
}
}
cardData.zone = "selected";
if (allPlayersSelected(world)) {
state.message = "All players selected. Resolving round.";
}
}
export function collectProposals(world: World): RoundProposals {
const proposals: RoundProposals = {
fetchWater: false,
exchange: false,
drink: false,
chant: false,
};
for (const player of getPlayersInSeatOrder(world)) {
const action = getSelectedAction(world, player);
if (!action || action === "rest") continue;
proposals[action] = true;
}
return proposals;
}
function isProposed(world: World, kind: ActionKind): boolean {
for (const player of getPlayersInSeatOrder(world)) {
if (getSelectedAction(world, player) === kind) return true;
}
return false;
}
function isGameOver(world: World): boolean {
return world.getSingleton(GameState).phase === "gameOver";
}
// ── Phase wrappers (each checks proposal + gameOver, then delegates) ──
export function chantPhase(world: World, stats?: ToolEffectRecorder): void {
if (isGameOver(world) || !isProposed(world, "chant")) return;
resolveChant(world, stats);
}
export function fetchWaterPhase(
world: World,
stats?: ToolEffectRecorder,
): void {
if (isGameOver(world) || !isProposed(world, "fetchWater")) return;
resolveFetchWater(world, stats);
}
export function exchangePhase(world: World, stats?: ToolEffectRecorder): void {
if (isGameOver(world) || !isProposed(world, "exchange")) return;
resolveExchange(world, stats);
}
export function drinkPhase(world: World, stats?: ToolEffectRecorder): void {
if (isGameOver(world) || !isProposed(world, "drink")) return;
resolveDrink(world, stats);
}
export function endOfRoundPhase(
world: World,
stats?: ToolEffectRecorder,
): void {
if (isGameOver(world)) return;
resolveEndOfRoundTools(world, stats);
checkWinner(world);
}
export function prepareNextRound(world: World): void {
const state = world.getSingleton(GameState);
if (state.phase === "gameOver") return;
for (const card of world.query(query(ActionCard))) {
const data = world.get(card, ActionCard);
if (data.zone === "cooldown") data.zone = "hand";
}
for (const card of world.query(query(ActionCard))) {
const data = world.get(card, ActionCard);
if (data.zone === "selected") data.zone = "cooldown";
}
world.getSingleton(Table).round++;
state.phase = "selecting";
state.message = `Round ${world.getSingleton(Table).round}: choose an action card.`;
}
export function resolveFetchWater(
world: World,
stats?: ToolEffectRecorder,
): void {
const table = world.getSingleton(Table);
const players = getPlayersInSeatOrder(world);
const participants = players.filter((player) => {
const action = getSelectedAction(world, player);
const tool = world.get(getToolOf(world, player), Tool);
return (
action !== null && action !== "rest" && CARRYING_TOOLS.has(tool.kind)
);
});
for (const player of participants) {
const toolEntity = getToolOf(world, player);
const tool = world.get(toolEntity, Tool);
let placed = 2;
if (tool.kind === "bucket") placed = 1;
if (tool.kind === "woodenBucket" && participants.length > 1) placed = 0;
const playerData = world.get(player, Player);
if (playerData.water >= placed) {
playerData.water -= placed;
tool.water += placed;
stats?.record(tool.kind, "挑水", {
playerWaterDelta: -placed,
ownToolWaterDelta: placed,
});
} else {
stats?.record(tool.kind, "挑水", {}, "水不足,未放水");
}
}
table.centralWater += players.length - participants.length;
for (const player of participants) {
const tool = world.get(getToolOf(world, player), Tool);
if (tool.kind === "ladle" && table.centralWater > 0) {
table.centralWater--;
tool.water++;
stats?.record(tool.kind, "挑水", {
ownToolWaterDelta: 1,
centralWaterDelta: -1,
});
}
}
}
export function resolveExchange(
world: World,
stats?: ToolEffectRecorder,
): void {
const participants = getPlayersInSeatOrder(world).filter(
(player) => getSelectedAction(world, player) !== "chant",
);
if (participants.length <= 1) return;
const table = world.getSingleton(Table);
const tools = participants.map((player) => getToolOf(world, player));
for (const toolEntity of tools) {
const tool = world.get(toolEntity, Tool);
if (tool.kind === "shoulderPole" && tool.water > 0) {
const dumped = tool.water;
table.centralWater += dumped;
tool.water = 0;
stats?.record(tool.kind, "交换", {
ownToolWaterDelta: -dumped,
centralWaterDelta: dumped,
});
}
}
for (let i = 0; i < participants.length; i++) {
const giverTool = tools[i];
const receiver = participants[(i + 1) % participants.length];
world.get(giverTool, Tool).owner = receiver;
}
}
export function resolveDrink(world: World, stats?: ToolEffectRecorder): void {
for (const player of getPlayersFromMarker(world)) {
if (!canParticipateInDrink(world, player)) continue;
const table = world.getSingleton(Table);
const playerData = world.get(player, Player);
const tool = world.get(getToolOf(world, player), Tool);
let drankFromTool = 0;
if (table.centralWater > 0) {
table.centralWater--;
playerData.water++;
}
const maxFromTool =
tool.kind === "bigBucket" ? tool.water : Math.min(2, tool.water);
if (maxFromTool > 0) {
tool.water -= maxFromTool;
playerData.water += maxFromTool;
drankFromTool = maxFromTool;
stats?.record(tool.kind, "喝水", {
playerWaterDelta: maxFromTool,
ownToolWaterDelta: -maxFromTool,
});
}
if (tool.kind === "waterJar") {
playerData.water++;
stats?.record(tool.kind, "喝水", {
playerWaterDelta: 1,
});
}
if (
tool.kind === "bigBowl" &&
drankFromTool === 0 &&
table.centralWater > 0
) {
table.centralWater--;
tool.water++;
stats?.record(tool.kind, "喝水", {
ownToolWaterDelta: 1,
centralWaterDelta: -1,
});
}
if (playerData.water >= 10) {
setWinner(world, player);
return;
}
}
}
export function resolveChant(world: World, stats?: ToolEffectRecorder): void {
const chanters = getPlayersFromMarker(world).filter(
(player) => getSelectedAction(world, player) === "chant",
);
if (chanters.length === 0) return;
world.getSingleton(WoodenFishMarker).holder = chanters[chanters.length - 1];
for (const player of chanters) {
const toolEntity = getToolOf(world, player);
const tool = world.get(toolEntity, Tool);
if (tool.kind === "bottle") {
tool.water++;
stats?.record(tool.kind, "念经", {
ownToolWaterDelta: 1,
});
} else if (tool.kind === "mouse") {
const moved = moveOneNeighborWaterTo(world, player, toolEntity);
if (moved) {
stats?.record(tool.kind, "念经", {
ownToolWaterDelta: 1,
otherToolWaterDelta: -1,
});
} else {
stats?.record(tool.kind, "念经", {}, "相邻道具无水可偷");
}
}
}
}
export function resolveEndOfRoundTools(
world: World,
stats?: ToolEffectRecorder,
): void {
for (const toolEntity of world.query(query(Tool))) {
const tool = world.get(toolEntity, Tool);
if (tool.kind === "woodenFish") {
world.getSingleton(WoodenFishMarker).holder = tool.owner;
stats?.record(tool.kind, "轮末", {}, "获得木鱼标记");
return;
}
}
}
export function checkWinner(world: World): Entity | null {
for (const player of getPlayersFromMarker(world)) {
if (world.get(player, Player).water >= 10) {
setWinner(world, player);
return player;
}
}
return null;
}
function canParticipateInDrink(world: World, player: Entity): boolean {
const tool = world.get(getToolOf(world, player), Tool);
if (tool.kind !== "waterJar") return true;
return getSelectedAction(world, player) === "drink";
}
function moveOneNeighborWaterTo(
world: World,
player: Entity,
destinationTool: Entity,
): boolean {
const neighbors = [
getLeftPlayer(world, player),
getRightPlayer(world, player),
];
for (const neighbor of neighbors) {
const neighborToolEntity = getToolOf(world, neighbor);
const neighborTool = world.get(neighborToolEntity, Tool);
if (neighborTool.water > 0) {
neighborTool.water--;
world.get(destinationTool, Tool).water++;
return true;
}
}
return false;
}
function setWinner(world: World, player: Entity): void {
const state = world.getSingleton(GameState);
state.phase = "gameOver";
state.winner = player;
state.message = `${world.get(player, Player).name} wins!`;
}