import { defineComponent } from "../../src/component"; import type { Entity, World } from "../../src/index"; import { query } from "../../src/query"; export type ActionKind = "fetchWater" | "exchange" | "drink" | "chant" | "rest"; export type ActionZone = "hand" | "selected" | "cooldown"; export type ToolKind = | "woodenFish" | "bucket" | "bottle" | "woodenBucket" | "bigBowl" | "bigBucket" | "mouse" | "shoulderPole" | "waterJar" | "ladle"; export type Phase = "setup" | "selecting" | "resolving" | "gameOver"; export const ACTION_KINDS: readonly ActionKind[] = [ "fetchWater", "exchange", "drink", "chant", "rest", ] as const; export const TOOL_KINDS: readonly ToolKind[] = [ "woodenFish", "bucket", "bottle", "woodenBucket", "bigBowl", "bigBucket", "mouse", "shoulderPole", "waterJar", "ladle", ] as const; export const CARRYING_TOOLS = new Set([ "bucket", "woodenBucket", "bigBucket", "shoulderPole", "ladle", ]); export const Player = defineComponent("threeMonks.player", { seat: 0, name: "", water: 0, }); export const ActionCard = defineComponent("threeMonks.actionCard", { owner: 0 as Entity, kind: "rest" as ActionKind, zone: "hand" as ActionZone, }); export const Tool = defineComponent("threeMonks.tool", { owner: 0 as Entity, kind: "bucket" as ToolKind, water: 0, }); export const Table = defineComponent("threeMonks.table", { centralWater: 0, round: 1, }); export const WoodenFishMarker = defineComponent("threeMonks.woodenFishMarker", { holder: 0 as Entity, }); export const GameState = defineComponent("threeMonks.gameState", { phase: "setup" as Phase, winner: 0 as Entity | 0, message: "", }); export type RandomFn = () => number; export function setupGame( world: World, playerNames: readonly string[], random: RandomFn = Math.random, ): Entity[] { if (playerNames.length < 3 || playerNames.length > 8) { throw new Error("Three Monks requires 3-8 players"); } const players: Entity[] = []; for (let seat = 0; seat < playerNames.length; seat++) { const player = world.spawn(); world.add(player, Player, { seat, name: playerNames[seat], water: 2 }); players.push(player); for (const kind of ACTION_KINDS) { const card = world.spawn(); world.add(card, ActionCard, { owner: player, kind, zone: "hand" }); } } const tools = dealBalancedTools(players.length, random); for (let i = 0; i < players.length; i++) { const tool = world.spawn(); world.add(tool, Tool, { owner: players[i], kind: tools[i], water: 0 }); } const holder = players[Math.floor(random() * players.length)]; world.addSingleton(Table, { centralWater: 0, round: 1 }); world.addSingleton(WoodenFishMarker, { holder }); world.addSingleton(GameState, { phase: "selecting", winner: 0, message: "Choose an action card.", }); return players; } export function getPlayersInSeatOrder(world: World): Entity[] { return [...world.query(query(Player))].sort( (a, b) => world.get(a, Player).seat - world.get(b, Player).seat, ); } export function getPlayersFromMarker(world: World): Entity[] { const players = getPlayersInSeatOrder(world); if (players.length === 0) return []; const holder = world.getSingleton(WoodenFishMarker).holder; const index = Math.max(0, players.indexOf(holder)); return [...players.slice(index), ...players.slice(0, index)]; } export function getLeftPlayer(world: World, player: Entity): Entity { const players = getPlayersInSeatOrder(world); const index = players.indexOf(player); if (index < 0) throw new Error("Player is not seated"); return players[(index + 1) % players.length]; } export function getRightPlayer(world: World, player: Entity): Entity { const players = getPlayersInSeatOrder(world); const index = players.indexOf(player); if (index < 0) throw new Error("Player is not seated"); return players[(index - 1 + players.length) % players.length]; } export function getToolOf(world: World, player: Entity): Entity { for (const tool of world.query(query(Tool))) { if (world.get(tool, Tool).owner === player) return tool; } throw new Error("Player does not have a tool"); } export function getActionCard( world: World, player: Entity, kind: ActionKind, ): Entity | null { for (const card of world.query(query(ActionCard))) { const data = world.get(card, ActionCard); if (data.owner === player && data.kind === kind) return card; } return null; } export function getSelectedAction( world: World, player: Entity, ): ActionKind | null { for (const card of world.query(query(ActionCard))) { const data = world.get(card, ActionCard); if (data.owner === player && data.zone === "selected") return data.kind; } return null; } export function allPlayersSelected(world: World): boolean { return getPlayersInSeatOrder(world).every( (player) => getSelectedAction(world, player) !== null, ); } export function hasWinner(world: World): boolean { return world.getSingleton(GameState).winner !== 0; } function dealBalancedTools(playerCount: number, random: RandomFn): ToolKind[] { const carrying = shuffle( TOOL_KINDS.filter((kind) => CARRYING_TOOLS.has(kind)), random, ); const nonCarrying = shuffle( TOOL_KINDS.filter((kind) => !CARRYING_TOOLS.has(kind)), random, ); const carryingCount = Math.ceil(playerCount / 2); const tools = [ ...carrying.slice(0, carryingCount), ...nonCarrying.slice(0, playerCount - carryingCount), ]; return shuffle(tools, random); } function shuffle(items: T[], random: RandomFn): T[] { for (let i = items.length - 1; i > 0; i--) { const j = Math.floor(random() * (i + 1)); [items[i], items[j]] = [items[j], items[i]]; } return items; }