boardgame-core/docs/game-module.md

5.0 KiB
Raw Blame History

编写 GameModule

GameModule 是定义游戏逻辑的模块,包含状态定义和命令注册。

GameModule 结构

一个 GameModule 必须导出两个东西:

import { createGameCommandRegistry, createRegion } from 'boardgame-core';

export function createInitialState() {
    return {
        board: createRegion('board', [
            { name: 'x', min: 0, max: 2 },
            { name: 'y', min: 0, max: 2 },
        ]),
        parts: {} as Record<string, Part>,
        currentPlayer: 'X' as PlayerType,
        winner: null as WinnerType,
        turn: 0,
    };
}

export const registry = createGameCommandRegistry<ReturnType<typeof createInitialState>>();

registry.register('setup', async function (game) { /* ... */ });
registry.register('play <player> <row:number> <col:number>', async function (game, cmd) { /* ... */ });

也可用 createGameModule 辅助函数包装:

export const gameModule = createGameModule({
    registry,
    createInitialState,
});

定义游戏状态

建议用 ReturnType 推导状态类型:

export type GameState = ReturnType<typeof createInitialState>;

状态通常包含 Region、partsRecord<string, Part>)以及游戏专属字段(当前玩家、分数等)。

注册命令

使用 registry.register() 注册命令。Schema 字符串定义了命令格式:

registry.register('play <player> <row:number> <col:number>', async function (game, player, row, col) {
    // game 是 IGameContext<TState>,可访问和修改状态
    game.produce(state => {
        // state.parts[...].position = [row, col];
    });

    return { success: true };
});

Schema 语法

语法 含义
name 命令名
<param> 必填参数(字符串)
<param:number> 必填参数(自动转为数字)
[--flag] 可选标志
[-x:number] 可选选项(带类型)

命令处理器函数签名

命令处理器接收 gameIGameContext<TState>)作为第一个参数,后续参数来自命令解析:

registry.register('myCommand <arg>', async function (game, arg) {
    const state = game.value;                          // 读取状态
    game.produce(d => { d.currentPlayer = 'O'; });     // 同步修改状态
    await game.produceAsync(d => { /* ... */ });       // 异步修改(等待动画)

    const result = await game.prompt('confirm <action>', validator, currentPlayer);
    const subResult = await subCommand(game, player);  // 调用子命令
    return { success: true };
});

registry.register() 返回一个可调用函数,可在其他命令中直接调用:

const subCommand = registry.register('sub <player>', async function (game, player) {
    return { score: 10 };
});

// 在另一个命令中使用
registry.register('main', async function (game) {
    const result = await subCommand(game, 'X');
    // result = { success: true, result: { score: 10 } }
});

详见 API 参考

使用 prompt 等待玩家输入

game.prompt() 暂停命令执行,等待外部通过 host.onInput() 提交输入:

const playCmd = await game.prompt(
    'play <player> <row:number> <col:number>',
    (command) => {
        const [player, row, col] = command.params as [PlayerType, number, number];
        if (player !== turnPlayer) throw `Invalid player: ${player}`;
        if (row < 0 || row > 2 || col < 0 || col > 2) throw `Invalid position`;
        if (isCellOccupied(game, row, col)) throw `Cell occupied`;
        return { player, row, col };  // 验证通过,返回所需数据
    },
    game.value.currentPlayer
);

// playCmd = { player, row, col }

验证函数中 throw 字符串会触发重新提示,返回非 null 值表示验证通过并通过该值 resolve Promise。

使用 setup 驱动游戏循环

setup 作为入口点驱动游戏循环,通过调用其他命令函数实现:

// 注册 turn 命令并获取可调用函数
const turnCommand = registry.register('turn <player>', async function (game, player) {
    // ... 执行回合逻辑
    return { winner: null as WinnerType };
});

// 注册 setup 命令
registry.register('setup', async function (game) {
    while (true) {
        const currentPlayer = game.value.currentPlayer;
        const turnOutput = await turnCommand(game, currentPlayer);
        if (!turnOutput.success) throw new Error(turnOutput.error);

        game.produce(state => {
            state.winner = turnOutput.result.winner;
            if (!state.winner) {
                state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
            }
        });
        if (game.value.winner) break;
    }
    return game.value;
});

Part、Region 和 RNG

详见 棋子、区域与 RNG

完整示例

参考以下示例: