diff --git a/examples/three-monks/commands.ts b/examples/three-monks/commands.ts deleted file mode 100644 index b45d33e..0000000 --- a/examples/three-monks/commands.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { defineComponent } from "../../src/component"; -import type { Entity, World } from "../../src/index"; -import type { CommandQueue } from "../../src/commands/index"; -import type { ActionKind } from "./components"; -import { selectAction } from "./rules"; -export interface SelectionNotifier { - notifySelectionChanged(): void; -} - -export const SelectActionCard = defineComponent("threeMonks.selectActionCard", { - player: 0 as Entity, - kind: "rest" as ActionKind, -}); - -export function registerCommands( - world: World, - commands: CommandQueue, - flow?: SelectionNotifier, -): void { - commands.handle(SelectActionCard, (command) => { - selectAction(world, command.player, command.kind); - flow?.notifySelectionChanged(); - }); -} - -export function issueSelectAction( - world: World, - player: Entity, - kind: ActionKind, -): Entity { - const command = world.spawn(); - world.add(command, SelectActionCard, { player, kind }); - return command; -} diff --git a/examples/three-monks/design.md b/examples/three-monks/design.md index f438888..81c0509 100644 --- a/examples/three-monks/design.md +++ b/examples/three-monks/design.md @@ -71,7 +71,8 @@ ## 设计实现 - `components.ts`: 游戏数据与状态 -- `commands.ts`: 玩家命令与规则命令实现 - `gameflow.ts`: 规则流程行为树 +- `rules.ts`: 规则引擎(阶段包装器 + 核心结算函数) - `logging.ts`: 日志输出 - `simulate.ts`: 对局模拟与统计 +- `random-playthrough.ts`: 单局详细回放 diff --git a/examples/three-monks/gameflow.ts b/examples/three-monks/gameflow.ts index 3bdd723..03d492d 100644 --- a/examples/three-monks/gameflow.ts +++ b/examples/three-monks/gameflow.ts @@ -9,7 +9,15 @@ import { type TaskRunner, } from "../../src/bt/index"; import { allPlayersSelected, hasWinner } from "./components"; -import { beginSelectionPhase, prepareNextRound, resolveRound } from "./rules"; +import { + beginSelectionPhase, + chantPhase, + exchangePhase, + fetchWaterPhase, + drinkPhase, + endOfRoundPhase, + prepareNextRound, +} from "./rules"; export interface ThreeMonksFlow { runner: TaskRunner; @@ -41,7 +49,11 @@ export function createThreeMonksFlow(world: World): ThreeMonksFlow { control.succeed(); } }), - action((world) => resolveRound(world)), + action((world) => chantPhase(world)), + action((world) => fetchWaterPhase(world)), + action((world) => exchangePhase(world)), + action((world) => drinkPhase(world)), + action((world) => endOfRoundPhase(world)), action((world) => prepareNextRound(world)), ]), ), diff --git a/examples/three-monks/random-playthrough.ts b/examples/three-monks/random-playthrough.ts index 4c65aa2..81a189c 100644 --- a/examples/three-monks/random-playthrough.ts +++ b/examples/three-monks/random-playthrough.ts @@ -9,15 +9,14 @@ import { } from "./components"; import { beginSelectionPhase, - checkWinner, collectProposals, prepareNextRound, - resolveChant, - resolveDrink, - resolveEndOfRoundTools, - resolveExchange, - resolveFetchWater, selectAction, + chantPhase, + fetchWaterPhase, + exchangePhase, + drinkPhase, + endOfRoundPhase, } from "./rules"; import { ACTION_LABELS, @@ -82,24 +81,13 @@ export function generateRandomPlayLog( const proposals = collectProposals(world); log.add(` 本轮行动阶段:${formatProposals(proposals)}`); - if (proposals.chant) { - applyLoggedPhase(log, world, "念经", () => resolveChant(world)); - } - if (proposals.fetchWater) { - applyLoggedPhase(log, world, "挑水", () => resolveFetchWater(world)); - } - if (proposals.exchange) { - applyLoggedPhase(log, world, "交换", () => resolveExchange(world)); - } - if (proposals.drink) { - applyLoggedPhase(log, world, "喝水", () => resolveDrink(world)); - } + applyLoggedPhase(log, world, "念经", () => chantPhase(world)); + applyLoggedPhase(log, world, "挑水", () => fetchWaterPhase(world)); + applyLoggedPhase(log, world, "交换", () => exchangePhase(world)); + applyLoggedPhase(log, world, "喝水", () => drinkPhase(world)); if (world.getSingleton(GameState).phase === "gameOver") break; - applyLoggedPhase(log, world, "轮末道具", () => - resolveEndOfRoundTools(world), - ); - checkWinner(world); + applyLoggedPhase(log, world, "轮末道具", () => endOfRoundPhase(world)); if (world.getSingleton(GameState).phase === "gameOver") break; prepareNextRound(world); diff --git a/examples/three-monks/rules.ts b/examples/three-monks/rules.ts index 9489fdb..9130a08 100644 --- a/examples/three-monks/rules.ts +++ b/examples/three-monks/rules.ts @@ -83,19 +83,47 @@ export function collectProposals(world: World): RoundProposals { return proposals; } -export function resolveRound(world: World, stats?: ToolEffectRecorder): void { - const state = world.getSingleton(GameState); - if (state.phase === "gameOver") return; +function isProposed(world: World, kind: ActionKind): boolean { + for (const player of getPlayersInSeatOrder(world)) { + if (getSelectedAction(world, player) === kind) return true; + } + return false; +} - state.phase = "resolving"; - const proposals = collectProposals(world); +function isGameOver(world: World): boolean { + return world.getSingleton(GameState).phase === "gameOver"; +} - if (proposals.chant) resolveChant(world, stats); - if (proposals.fetchWater) resolveFetchWater(world, stats); - if (proposals.exchange) resolveExchange(world, stats); - if (proposals.drink) resolveDrink(world, stats); - if (world.getSingleton(GameState).phase === "gameOver") return; +// ── Phase wrappers (each checks proposal + gameOver, then delegates) ── +export function chantPhase(world: World, stats?: ToolEffectRecorder): void { + if (isGameOver(world) || !isProposed(world, "chant")) return; + resolveChant(world, stats); +} + +export function fetchWaterPhase( + world: World, + stats?: ToolEffectRecorder, +): void { + if (isGameOver(world) || !isProposed(world, "fetchWater")) return; + resolveFetchWater(world, stats); +} + +export function exchangePhase(world: World, stats?: ToolEffectRecorder): void { + if (isGameOver(world) || !isProposed(world, "exchange")) return; + resolveExchange(world, stats); +} + +export function drinkPhase(world: World, stats?: ToolEffectRecorder): void { + if (isGameOver(world) || !isProposed(world, "drink")) return; + resolveDrink(world, stats); +} + +export function endOfRoundPhase( + world: World, + stats?: ToolEffectRecorder, +): void { + if (isGameOver(world)) return; resolveEndOfRoundTools(world, stats); checkWinner(world); } diff --git a/examples/three-monks/simulate.ts b/examples/three-monks/simulate.ts index e21543b..6af359f 100644 --- a/examples/three-monks/simulate.ts +++ b/examples/three-monks/simulate.ts @@ -5,21 +5,18 @@ import { Player, Table, setupGame, - getPlayersInSeatOrder, type ActionKind, } from "./components"; import { ToolEffectStats, formatToolEffectSummaries } from "./stats"; import { beginSelectionPhase, - checkWinner, - collectProposals, prepareNextRound, - resolveChant, - resolveDrink, - resolveEndOfRoundTools, - resolveExchange, - resolveFetchWater, selectAction, + chantPhase, + fetchWaterPhase, + exchangePhase, + drinkPhase, + endOfRoundPhase, } from "./rules"; function mulberry32(seed: number): () => number { @@ -75,16 +72,13 @@ function runSingleGame(seed: number, stats?: ToolEffectStats): GameStats { selectAction(world, player, action); } - const proposals = collectProposals(world); - - if (proposals.chant) resolveChant(world, stats); - if (proposals.fetchWater) resolveFetchWater(world, stats); - if (proposals.exchange) resolveExchange(world, stats); - if (proposals.drink) resolveDrink(world, stats); + chantPhase(world, stats); + fetchWaterPhase(world, stats); + exchangePhase(world, stats); + drinkPhase(world, stats); if (world.getSingleton(GameState).phase === "gameOver") break; - resolveEndOfRoundTools(world, stats); - checkWinner(world); + endOfRoundPhase(world, stats); if (world.getSingleton(GameState).phase === "gameOver") break; prepareNextRound(world); diff --git a/test/three-monks.test.ts b/test/three-monks.test.ts index 2235629..346f7ba 100644 --- a/test/three-monks.test.ts +++ b/test/three-monks.test.ts @@ -1,6 +1,5 @@ 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, @@ -18,10 +17,6 @@ import { 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, @@ -112,18 +107,17 @@ describe("Three Monks action selection", () => { expect(getSelectedAction(world, players[0])).toBe("rest"); }); - it("command selection notifies gameflow and completes the round once all players select", () => { + it("behavior tree completes a round after 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(); + selectAction(world, player, "rest"); + flow.notifySelectionChanged(); + flow.runner.tick(); } for (const player of players) {