feat(examples): Add Three Monks game example
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { World, query, type Entity } from "../src/index";
|
||||
import { CommandQueue } from "../src/commands/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 {
|
||||
issueSelectAction,
|
||||
registerCommands,
|
||||
} from "../examples/three-monks/commands";
|
||||
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("command selection notifies gameflow and completes the round once all players select", () => {
|
||||
const { world, players } = setup();
|
||||
const commands = new CommandQueue(world);
|
||||
const flow = createThreeMonksFlow(world);
|
||||
registerCommands(world, commands, flow);
|
||||
|
||||
flow.start();
|
||||
flow.runner.tick();
|
||||
|
||||
for (const player of players) {
|
||||
issueSelectAction(world, player, "rest");
|
||||
commands.execute();
|
||||
}
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user