refactor(three-monks): Replace commands with phase wrappers
This commit is contained in:
parent
6fa1f511bd
commit
b038388deb
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -71,7 +71,8 @@
|
|||
## 设计实现
|
||||
|
||||
- `components.ts`: 游戏数据与状态
|
||||
- `commands.ts`: 玩家命令与规则命令实现
|
||||
- `gameflow.ts`: 规则流程行为树
|
||||
- `rules.ts`: 规则引擎(阶段包装器 + 核心结算函数)
|
||||
- `logging.ts`: 日志输出
|
||||
- `simulate.ts`: 对局模拟与统计
|
||||
- `random-playthrough.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)),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue