ecs-observable/examples/three-monks/components.ts

220 lines
5.7 KiB
TypeScript
Raw Normal View History

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