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`: 游戏数据与状态
|
- `components.ts`: 游戏数据与状态
|
||||||
- `commands.ts`: 玩家命令与规则命令实现
|
|
||||||
- `gameflow.ts`: 规则流程行为树
|
- `gameflow.ts`: 规则流程行为树
|
||||||
|
- `rules.ts`: 规则引擎(阶段包装器 + 核心结算函数)
|
||||||
- `logging.ts`: 日志输出
|
- `logging.ts`: 日志输出
|
||||||
- `simulate.ts`: 对局模拟与统计
|
- `simulate.ts`: 对局模拟与统计
|
||||||
|
- `random-playthrough.ts`: 单局详细回放
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,15 @@ import {
|
||||||
type TaskRunner,
|
type TaskRunner,
|
||||||
} from "../../src/bt/index";
|
} from "../../src/bt/index";
|
||||||
import { allPlayersSelected, hasWinner } from "./components";
|
import { allPlayersSelected, hasWinner } from "./components";
|
||||||
import { beginSelectionPhase, prepareNextRound, resolveRound } from "./rules";
|
import {
|
||||||
|
beginSelectionPhase,
|
||||||
|
chantPhase,
|
||||||
|
exchangePhase,
|
||||||
|
fetchWaterPhase,
|
||||||
|
drinkPhase,
|
||||||
|
endOfRoundPhase,
|
||||||
|
prepareNextRound,
|
||||||
|
} from "./rules";
|
||||||
|
|
||||||
export interface ThreeMonksFlow {
|
export interface ThreeMonksFlow {
|
||||||
runner: TaskRunner;
|
runner: TaskRunner;
|
||||||
|
|
@ -41,7 +49,11 @@ export function createThreeMonksFlow(world: World): ThreeMonksFlow {
|
||||||
control.succeed();
|
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)),
|
action((world) => prepareNextRound(world)),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,14 @@ import {
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import {
|
import {
|
||||||
beginSelectionPhase,
|
beginSelectionPhase,
|
||||||
checkWinner,
|
|
||||||
collectProposals,
|
collectProposals,
|
||||||
prepareNextRound,
|
prepareNextRound,
|
||||||
resolveChant,
|
|
||||||
resolveDrink,
|
|
||||||
resolveEndOfRoundTools,
|
|
||||||
resolveExchange,
|
|
||||||
resolveFetchWater,
|
|
||||||
selectAction,
|
selectAction,
|
||||||
|
chantPhase,
|
||||||
|
fetchWaterPhase,
|
||||||
|
exchangePhase,
|
||||||
|
drinkPhase,
|
||||||
|
endOfRoundPhase,
|
||||||
} from "./rules";
|
} from "./rules";
|
||||||
import {
|
import {
|
||||||
ACTION_LABELS,
|
ACTION_LABELS,
|
||||||
|
|
@ -82,24 +81,13 @@ export function generateRandomPlayLog(
|
||||||
const proposals = collectProposals(world);
|
const proposals = collectProposals(world);
|
||||||
log.add(` 本轮行动阶段:${formatProposals(proposals)}`);
|
log.add(` 本轮行动阶段:${formatProposals(proposals)}`);
|
||||||
|
|
||||||
if (proposals.chant) {
|
applyLoggedPhase(log, world, "念经", () => chantPhase(world));
|
||||||
applyLoggedPhase(log, world, "念经", () => resolveChant(world));
|
applyLoggedPhase(log, world, "挑水", () => fetchWaterPhase(world));
|
||||||
}
|
applyLoggedPhase(log, world, "交换", () => exchangePhase(world));
|
||||||
if (proposals.fetchWater) {
|
applyLoggedPhase(log, world, "喝水", () => drinkPhase(world));
|
||||||
applyLoggedPhase(log, world, "挑水", () => resolveFetchWater(world));
|
|
||||||
}
|
|
||||||
if (proposals.exchange) {
|
|
||||||
applyLoggedPhase(log, world, "交换", () => resolveExchange(world));
|
|
||||||
}
|
|
||||||
if (proposals.drink) {
|
|
||||||
applyLoggedPhase(log, world, "喝水", () => resolveDrink(world));
|
|
||||||
}
|
|
||||||
if (world.getSingleton(GameState).phase === "gameOver") break;
|
if (world.getSingleton(GameState).phase === "gameOver") break;
|
||||||
|
|
||||||
applyLoggedPhase(log, world, "轮末道具", () =>
|
applyLoggedPhase(log, world, "轮末道具", () => endOfRoundPhase(world));
|
||||||
resolveEndOfRoundTools(world),
|
|
||||||
);
|
|
||||||
checkWinner(world);
|
|
||||||
if (world.getSingleton(GameState).phase === "gameOver") break;
|
if (world.getSingleton(GameState).phase === "gameOver") break;
|
||||||
|
|
||||||
prepareNextRound(world);
|
prepareNextRound(world);
|
||||||
|
|
|
||||||
|
|
@ -83,19 +83,47 @@ export function collectProposals(world: World): RoundProposals {
|
||||||
return proposals;
|
return proposals;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveRound(world: World, stats?: ToolEffectRecorder): void {
|
function isProposed(world: World, kind: ActionKind): boolean {
|
||||||
const state = world.getSingleton(GameState);
|
for (const player of getPlayersInSeatOrder(world)) {
|
||||||
if (state.phase === "gameOver") return;
|
if (getSelectedAction(world, player) === kind) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
state.phase = "resolving";
|
function isGameOver(world: World): boolean {
|
||||||
const proposals = collectProposals(world);
|
return world.getSingleton(GameState).phase === "gameOver";
|
||||||
|
}
|
||||||
|
|
||||||
if (proposals.chant) resolveChant(world, stats);
|
// ── Phase wrappers (each checks proposal + gameOver, then delegates) ──
|
||||||
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;
|
|
||||||
|
|
||||||
|
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);
|
resolveEndOfRoundTools(world, stats);
|
||||||
checkWinner(world);
|
checkWinner(world);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,18 @@ import {
|
||||||
Player,
|
Player,
|
||||||
Table,
|
Table,
|
||||||
setupGame,
|
setupGame,
|
||||||
getPlayersInSeatOrder,
|
|
||||||
type ActionKind,
|
type ActionKind,
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import { ToolEffectStats, formatToolEffectSummaries } from "./stats";
|
import { ToolEffectStats, formatToolEffectSummaries } from "./stats";
|
||||||
import {
|
import {
|
||||||
beginSelectionPhase,
|
beginSelectionPhase,
|
||||||
checkWinner,
|
|
||||||
collectProposals,
|
|
||||||
prepareNextRound,
|
prepareNextRound,
|
||||||
resolveChant,
|
|
||||||
resolveDrink,
|
|
||||||
resolveEndOfRoundTools,
|
|
||||||
resolveExchange,
|
|
||||||
resolveFetchWater,
|
|
||||||
selectAction,
|
selectAction,
|
||||||
|
chantPhase,
|
||||||
|
fetchWaterPhase,
|
||||||
|
exchangePhase,
|
||||||
|
drinkPhase,
|
||||||
|
endOfRoundPhase,
|
||||||
} from "./rules";
|
} from "./rules";
|
||||||
|
|
||||||
function mulberry32(seed: number): () => number {
|
function mulberry32(seed: number): () => number {
|
||||||
|
|
@ -75,16 +72,13 @@ function runSingleGame(seed: number, stats?: ToolEffectStats): GameStats {
|
||||||
selectAction(world, player, action);
|
selectAction(world, player, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
const proposals = collectProposals(world);
|
chantPhase(world, stats);
|
||||||
|
fetchWaterPhase(world, stats);
|
||||||
if (proposals.chant) resolveChant(world, stats);
|
exchangePhase(world, stats);
|
||||||
if (proposals.fetchWater) resolveFetchWater(world, stats);
|
drinkPhase(world, stats);
|
||||||
if (proposals.exchange) resolveExchange(world, stats);
|
|
||||||
if (proposals.drink) resolveDrink(world, stats);
|
|
||||||
if (world.getSingleton(GameState).phase === "gameOver") break;
|
if (world.getSingleton(GameState).phase === "gameOver") break;
|
||||||
|
|
||||||
resolveEndOfRoundTools(world, stats);
|
endOfRoundPhase(world, stats);
|
||||||
checkWinner(world);
|
|
||||||
if (world.getSingleton(GameState).phase === "gameOver") break;
|
if (world.getSingleton(GameState).phase === "gameOver") break;
|
||||||
|
|
||||||
prepareNextRound(world);
|
prepareNextRound(world);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { World, query, type Entity } from "../src/index";
|
import { World, query, type Entity } from "../src/index";
|
||||||
import { CommandQueue } from "../src/commands/index";
|
|
||||||
import { generateRandomPlayLog } from "../examples/three-monks/random-playthrough";
|
import { generateRandomPlayLog } from "../examples/three-monks/random-playthrough";
|
||||||
import {
|
import {
|
||||||
ActionCard,
|
ActionCard,
|
||||||
|
|
@ -18,10 +17,6 @@ import {
|
||||||
type ActionKind,
|
type ActionKind,
|
||||||
type ToolKind,
|
type ToolKind,
|
||||||
} from "../examples/three-monks/components";
|
} from "../examples/three-monks/components";
|
||||||
import {
|
|
||||||
issueSelectAction,
|
|
||||||
registerCommands,
|
|
||||||
} from "../examples/three-monks/commands";
|
|
||||||
import { createThreeMonksFlow } from "../examples/three-monks/gameflow";
|
import { createThreeMonksFlow } from "../examples/three-monks/gameflow";
|
||||||
import {
|
import {
|
||||||
prepareNextRound,
|
prepareNextRound,
|
||||||
|
|
@ -112,18 +107,17 @@ describe("Three Monks action selection", () => {
|
||||||
expect(getSelectedAction(world, players[0])).toBe("rest");
|
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 { world, players } = setup();
|
||||||
const commands = new CommandQueue(world);
|
|
||||||
const flow = createThreeMonksFlow(world);
|
const flow = createThreeMonksFlow(world);
|
||||||
registerCommands(world, commands, flow);
|
|
||||||
|
|
||||||
flow.start();
|
flow.start();
|
||||||
flow.runner.tick();
|
flow.runner.tick();
|
||||||
|
|
||||||
for (const player of players) {
|
for (const player of players) {
|
||||||
issueSelectAction(world, player, "rest");
|
selectAction(world, player, "rest");
|
||||||
commands.execute();
|
flow.notifySelectionChanged();
|
||||||
|
flow.runner.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const player of players) {
|
for (const player of players) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue