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; } export function resolveRound(world: World, stats?: ToolEffectRecorder): void { const state = world.getSingleton(GameState); if (state.phase === "gameOver") return; state.phase = "resolving"; const proposals = collectProposals(world); if (proposals.chant) resolveChant(world, stats); if (proposals.fetchWater) resolveFetchWater(world, stats); if (proposals.exchange) resolveExchange(world, stats); if (proposals.drink) resolveDrink(world, stats); if (world.getSingleton(GameState).phase === "gameOver") 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!`; }