220 lines
5.7 KiB
TypeScript
220 lines
5.7 KiB
TypeScript
|
|
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<ToolKind>([
|
||
|
|
"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<T>(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;
|
||
|
|
}
|