import { describe, expect, it } from "vitest"; import { World, query, type Entity } from "../src/index"; import { generateRandomPlayLog } from "../examples/three-monks/random-playthrough"; import { ActionCard, CARRYING_TOOLS, GameState, Player, Table, Tool, WoodenFishMarker, getActionCard, getPlayersInSeatOrder, getSelectedAction, getToolOf, setupGame, type ActionKind, type ToolKind, } from "../examples/three-monks/components"; import { createThreeMonksFlow } from "../examples/three-monks/gameflow"; import { prepareNextRound, resolveChant, resolveEndOfRoundTools, resolveDrink, resolveExchange, resolveFetchWater, selectAction, } from "../examples/three-monks/rules"; function setup(names = ["A", "B", "C"]): { world: World; players: Entity[] } { const world = new World(); const players = setupGame(world, names); return { world, players }; } function setTool( world: World, player: Entity, kind: ToolKind, water = 0, ): Entity { const tool = getToolOf(world, player); world.set(tool, Tool, { owner: player, kind, water }); return tool; } function selectAll(world: World, actions: readonly ActionKind[]): void { const players = getPlayersInSeatOrder(world); for (let i = 0; i < players.length; i++) { selectAction(world, players[i], actions[i]); } } describe("Three Monks random logging", () => { it("generates a complete deterministic random playthrough log", () => { const result = generateRandomPlayLog({ seed: 20260701 }); expect(result.log).toContain("三个和尚随机对局"); expect(result.log).toContain("游戏结束"); expect(result.log).toContain("胜者:慧空"); expect(result.roundsPlayed).toBe(14); }); }); describe("Three Monks setup", () => { it("requires 3-8 players", () => { expect(() => setupGame(new World(), ["A", "B"])).toThrow(); expect(() => setupGame(new World(), ["1", "2", "3", "4", "5", "6", "7", "8", "9"]), ).toThrow(); }); it("creates players, action cards, tools, table state, and wooden fish marker", () => { const { world, players } = setup(); expect(players).toHaveLength(3); expect([...world.query(query(Player))]).toHaveLength(3); expect(players.map((player) => world.get(player, Player).water)).toEqual([ 2, 2, 2, ]); expect([...world.query(query(ActionCard))]).toHaveLength(15); const tools = [...world.query(query(Tool))].map( (tool) => world.get(tool, Tool).kind, ); expect(tools).toHaveLength(3); expect(tools.filter((kind) => CARRYING_TOOLS.has(kind))).toHaveLength(2); expect(world.hasSingleton(Table)).toBe(true); expect(world.hasSingleton(GameState)).toBe(true); expect(players).toContain(world.getSingleton(WoodenFishMarker).holder); }); }); describe("Three Monks action selection", () => { it("selects an available card and prevents selecting cooldown", () => { const { world, players } = setup(); selectAction(world, players[0], "fetchWater"); expect(getSelectedAction(world, players[0])).toBe("fetchWater"); prepareNextRound(world); const card = getActionCard(world, players[0], "fetchWater")!; expect(world.get(card, ActionCard).zone).toBe("cooldown"); expect(() => selectAction(world, players[0], "fetchWater")).toThrow(); selectAction(world, players[0], "rest"); expect(getSelectedAction(world, players[0])).toBe("rest"); }); it("behavior tree completes a round after all players select", () => { const { world, players } = setup(); const flow = createThreeMonksFlow(world); flow.start(); flow.runner.tick(); for (const player of players) { selectAction(world, player, "rest"); flow.notifySelectionChanged(); flow.runner.tick(); } for (const player of players) { const rest = getActionCard(world, player, "rest")!; expect(world.get(rest, ActionCard).zone).toBe("cooldown"); } expect(world.getSingleton(Table).round).toBe(2); }); }); describe("Three Monks phase rules", () => { it("fetch water moves player water to carrying tools and applies modifiers", () => { const { world, players } = setup(); world.get(players[0], Player).water = 1; setTool(world, players[0], "bucket"); setTool(world, players[1], "woodenBucket"); setTool(world, players[2], "ladle"); selectAll(world, ["fetchWater", "fetchWater", "rest"]); resolveFetchWater(world); expect(world.get(players[0], Player).water).toBe(0); expect(world.get(getToolOf(world, players[0]), Tool).water).toBe(1); expect(world.get(getToolOf(world, players[1]), Tool).water).toBe(0); expect(world.get(getToolOf(world, players[2]), Tool).water).toBe(0); expect(world.getSingleton(Table).centralWater).toBe(1); }); it("fetch water does not move water when the player cannot pay", () => { const { world, players } = setup(); world.get(players[0], Player).water = 0; setTool(world, players[0], "bucket"); selectAll(world, ["fetchWater", "rest", "rest"]); resolveFetchWater(world); expect(world.get(players[0], Player).water).toBe(0); expect(world.get(getToolOf(world, players[0]), Tool).water).toBe(0); expect(world.getSingleton(Table).centralWater).toBe(2); }); it("ladle moves one central water after fetching", () => { const { world, players } = setup(); world.get(players[0], Player).water = 2; setTool(world, players[0], "ladle"); setTool(world, players[1], "woodenFish"); setTool(world, players[2], "bottle"); selectAll(world, ["fetchWater", "rest", "rest"]); resolveFetchWater(world); expect(world.get(players[0], Player).water).toBe(0); expect(world.get(getToolOf(world, players[0]), Tool).water).toBe(3); expect(world.getSingleton(Table).centralWater).toBe(1); }); it("exchange excludes chant proposers and shoulder pole dumps water", () => { const { world, players } = setup(); const shoulderPole = setTool(world, players[0], "shoulderPole", 3); const bottle = setTool(world, players[1], "bottle"); const bucket = setTool(world, players[2], "bucket"); selectAll(world, ["exchange", "chant", "exchange"]); resolveExchange(world); expect(world.getSingleton(Table).centralWater).toBe(3); expect(world.get(shoulderPole, Tool).water).toBe(0); expect(world.get(shoulderPole, Tool).owner).toBe(players[2]); expect(world.get(bucket, Tool).owner).toBe(players[0]); expect(world.get(bottle, Tool).owner).toBe(players[1]); }); it("drink resolves in wooden fish order and first player to 10 wins immediately", () => { const { world, players } = setup(); world.setSingleton(WoodenFishMarker, { holder: players[1] }); world.getSingleton(Table).centralWater = 3; world.get(players[1], Player).water = 9; setTool(world, players[0], "bucket"); setTool(world, players[1], "bucket"); setTool(world, players[2], "bucket"); selectAll(world, ["drink", "drink", "drink"]); resolveDrink(world); expect(world.getSingleton(GameState).winner).toBe(players[1]); expect(world.get(players[2], Player).water).toBe(2); }); it("water jar participates in drink only when its owner proposed drink", () => { const { world, players } = setup(); world.getSingleton(Table).centralWater = 3; setTool(world, players[0], "waterJar"); setTool(world, players[1], "bucket"); setTool(world, players[2], "bucket"); selectAll(world, ["rest", "drink", "drink"]); resolveDrink(world); expect(world.get(players[0], Player).water).toBe(2); expect(world.get(players[1], Player).water).toBe(3); }); it("big bowl stores central water if the owner did not drink from it", () => { const { world, players } = setup(); world.setSingleton(WoodenFishMarker, { holder: players[0] }); world.getSingleton(Table).centralWater = 2; setTool(world, players[0], "bigBowl", 0); setTool(world, players[1], "bucket"); setTool(world, players[2], "bucket"); selectAll(world, ["drink", "drink", "drink"]); resolveDrink(world); expect(world.get(players[0], Player).water).toBe(3); expect(world.get(getToolOf(world, players[0]), Tool).water).toBe(1); expect(world.getSingleton(Table).centralWater).toBe(0); }); it("chant gives marker to last chanter and applies bottle/mouse effects", () => { const { world, players } = setup(["A", "B", "C", "D"]); world.setSingleton(WoodenFishMarker, { holder: players[0] }); setTool(world, players[0], "bucket", 1); setTool(world, players[1], "bottle", 0); const mouse = setTool(world, players[2], "mouse", 0); setTool(world, players[3], "bucket", 1); selectAll(world, ["rest", "chant", "chant", "rest"]); resolveChant(world); expect(world.getSingleton(WoodenFishMarker).holder).toBe(players[2]); expect(world.get(getToolOf(world, players[1]), Tool).water).toBe(1); expect(world.get(mouse, Tool).water).toBe(1); }); it("wooden fish tool overrides chant marker at end of round", () => { const { world, players } = setup(); setTool(world, players[0], "woodenFish"); setTool(world, players[1], "bottle"); setTool(world, players[2], "bucket"); selectAll(world, ["rest", "chant", "rest"]); resolveChant(world); expect(world.getSingleton(WoodenFishMarker).holder).toBe(players[1]); resolveEndOfRoundTools(world); expect(world.getSingleton(WoodenFishMarker).holder).toBe(players[0]); }); });