ecs-observable/test/three-monks.test.ts

269 lines
9.3 KiB
TypeScript

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