boardgame-core/.qwen/skills/create-game-module/references/api.md

17 KiB
Raw Permalink Blame History

API 参考

本文档记录游戏模组开发者需要使用的公共 API。

核心接口

IGameContext<TState>

游戏运行的核心上下文,提供状态访问、随机数、命令执行和提示系统。

interface IGameContext<TState extends Record<string, unknown> = {}> {
    readonly value: TState;                           // 当前游戏状态(只读)
    readonly rng: ReadonlyRNG;                        // 随机数生成器(只读)

    // 状态更新
    produce(fn: (draft: TState) => void): void;       // 同步变更状态(基于 mutative
    produceAsync(fn: (draft: TState) => void): Promise<void>;  // 异步变更状态(等待中断)

    // 命令执行
    run<T>(input: string): Promise<CommandResult<T>>;           // 执行命令字符串
    runParsed<T>(command: Command): Promise<CommandResult<T>>; // 执行已解析的命令

    // 提示系统
    prompt<TResult, TArgs>(
        def: PromptDef<TArgs>,
        validator: PromptValidator<TResult, TArgs>,
        currentPlayer?: string | null
    ): Promise<TResult>;
}

使用示例:

// 读取状态
const currentPlayer = game.value.currentPlayer;

// 同步更新状态
game.produce((state) => {
    state.score += 10;
});

// 异步更新状态(等待动画完成)
await game.produceAsync((state) => {
    state.phase = 'next';
});

// 等待玩家输入
const result = await game.prompt(
    prompts.move,
    (from, to) => {
        if (!isValidMove(from, to)) {
            throw '无效移动';
        }
        return { from, to };
    }
);

GameModule<TState, TResult>

游戏模块的类型定义,这是开发者创建游戏时需要导出的核心结构。

type GameModule<TState extends Record<string, unknown>, TResult = unknown> = {
    registry?: CommandRegistry<IGameContext<TState>>;  // 可选的命令注册表
    createInitialState: () => TState;                   // 创建初始状态
    start: (ctx: IGameContext<TState>) => Promise<TResult>;  // 游戏主循环
}

createGameCommandRegistry<TState>()

创建游戏命令注册表,游戏模组用它来注册自定义命令。

function createGameCommandRegistry<TState extends Record<string, unknown> = {}>(): CommandRegistry<IGameContext<TState>>

使用示例:

import { createGameCommandRegistry, IGameContext } from '@/index';

export type GameState = { score: number };
export const registry = createGameCommandRegistry<GameState>();

// 注册命令
registry.register('addScore <amount:number>', async function(ctx, amount) {
    ctx.produce((state) => {
        state.score += amount;
    });
    return { success: true, result: undefined };
});

提示系统

createPromptDef<TArgs>(schema, hintText?)

从字符串模式创建 PromptDef

function createPromptDef<TArgs>(
    schema: CommandSchema | string,
    hintText?: string
): PromptDef<TArgs>

使用示例:

import { createPromptDef } from '@/index';

export const prompts = {
    // 必需参数
    play: createPromptDef<[PlayerType, number, number]>(
        'play <player> <row:number> <col:number>',
        '选择下子位置'
    ),

    // 可选参数
    draw: createPromptDef<[number?]>(
        'draw [count:number]',
        '抽牌'
    ),

    // 带选项
    trade: createPromptDef<[string, string]>(
        'trade <give> <receive> [--force]',
        '交易'
    ),
};

PromptDef<TArgs>

提示定义,用于 context.prompt() 方法。

type PromptDef<TArgs extends any[] = any[]> = {
    schema: CommandSchema;    // 命令模式定义
    hintText?: string;        // 可选的提示文本
}

PromptValidator<TResult, TArgs>

提示验证函数类型。验证器函数接收解析后的参数,应返回结果或抛出字符串错误。

type PromptValidator<TResult, TArgs extends any[] = any[]> = (...params: TArgs) => TResult;

验证器规则:

  • 返回任意值表示验证成功,该值将作为 prompt() 的返回值
  • 抛出字符串错误表示验证失败,错误消息会返回给玩家,玩家可重新输入
  • 玩家取消输入时,prompt() 会抛出异常

PromptEvent

提示事件对象,通过 commandRunnerContext.on('prompt', handler) 监听。

type PromptEvent = {
    schema: CommandSchema;
    hintText?: string;
    currentPlayer: string | null;
    tryCommit: (commandOrInput: Command | string) => string | null;  // null=成功string=错误消息
    cancel: (reason?: string) => void;
}

零件系统 (Part)

Part<TMeta>

游戏中的可操作物件(棋子、卡牌、骰子等)。

type Part<TMeta = {}> = {
    id: string;                    // 唯一标识
    sides?: number;                // 总面数(用于骰子/多面牌)
    side?: number;                 // 当前面
    alignments?: string[];         // 可用对齐方式
    alignment?: string;            // 当前对齐方式
    regionId: string;              // 所属区域 ID
    position: number[];            // 在区域中的位置坐标
} & Immutable<TMeta>;             // 自定义元数据(不可变)

使用示例:

import { Part } from '@/index';

export type PieceMeta = {
    owner: 'X' | 'O';
    type: 'pawn' | 'king';
};

export type Piece = Part<PieceMeta>;

// 访问元数据
const piece: Piece = ...;
console.log(piece.owner);  // 'X'
console.log(piece.type);   // 'pawn'

零件操作函数

flip<TMeta>(part)

翻转到下一面(循环)。

function flip<TMeta>(part: Part<TMeta>): void

flipTo<TMeta>(part, side)

翻转到指定面。

function flipTo<TMeta>(part: Part<TMeta>, side: number): void

roll<TMeta>(part, rng)

用 RNG 随机掷骰子。

function roll<TMeta>(part: Part<TMeta>, rng: RNG): void

零件工厂 (Part Factory)

createParts<T>(item, getId, count?)

创建多个相同类型的零件。

function createParts<T>(
    item: T,
    getId: (index: number) => string,
    count?: number
): Record<string, Part<T>>

使用示例:

import { createParts } from '@/index';

const pieces = createParts(
    { owner: 'X', type: 'pawn' },
    (i) => `piece-x-${i}`,
    5  // 创建 5 个
);

createPartsFromTable<T>(items, getId, getCount?)

从配置表批量创建零件。

function createPartsFromTable<T>(
    items: readonly T[],
    getId: (item: T, index: number) => string,
    getCount?: ((item: T) => number) | number
): Record<string, Part<T>>

使用示例:

import { createPartsFromTable } from '@/index';

const cardTable = [
    { name: 'fireball', damage: 3 },
    { name: 'shield', defense: 2 },
];

const cards = createPartsFromTable(
    cardTable,
    (item) => item.name,
    (item) => item.name === 'fireball' ? 4 : 2  // 每种卡牌的数量
);

区域系统 (Region)

Region

游戏区域(棋盘、手牌区等)。

type Region = {
    id: string;                     // 区域 ID
    axes: RegionAxis[];             // 坐标轴定义
    childIds: string[];             // 包含的零件 ID 列表
    partMap: Record<string, string>; // 位置 -> 零件 ID 映射
}

RegionAxis

区域的一个坐标轴。

type RegionAxis = {
    name: string;
    min?: number;
    max?: number;
    align?: 'start' | 'end' | 'center';  // 对齐方式
}

createRegion(id, axes)

创建区域。

function createRegion(id: string, axes: RegionAxis[]): Region

使用示例:

import { createRegion, createRegionAxis } from '@/index';

// 创建 3x3 棋盘
const board = createRegion('board', [
    createRegionAxis('row', 0, 2),
    createRegionAxis('col', 0, 2),
]);

// 或简写
const board = createRegion('board', [
    { name: 'row', min: 0, max: 2 },
    { name: 'col', min: 0, max: 2 },
]);

createRegionAxis(name, min?, max?, align?)

创建坐标轴。

function createRegionAxis(
    name: string,
    min?: number,
    max?: number,
    align?: 'start' | 'end' | 'center'
): RegionAxis

区域操作函数

applyAlign<TMeta>(region, parts)

根据轴的 align 配置重新排列零件位置。

function applyAlign<TMeta>(region: Region, parts: Record<string, Part<TMeta>>): void

shuffle<TMeta>(region, parts, rng)

在区域内随机打乱零件位置。

function shuffle<TMeta>(region: Region, parts: Record<string, Part<TMeta>>, rng: RNG): void

moveToRegion<TMeta>(part, sourceRegion, targetRegion, position?)

将零件从一个区域移动到另一个区域。

function moveToRegion<TMeta>(
    part: Part<TMeta>,
    sourceRegion: Region | null,
    targetRegion: Region | null,
    position?: number[]
): void

命令系统 (Command System)

Command

解析后的命令对象。

type Command = {
    name: string;                                    // 命令名
    flags: Record<string, true>;                     // 标志(如 --verbose
    options: Record<string, unknown>;                // 选项(如 --player X
    params: unknown[];                               // 位置参数
}

CommandSchema

命令模式定义,用于验证和解析。

type CommandSchema = {
    name: string;
    params: CommandParamSchema[];
    options: Record<string, CommandOptionSchema>;
    flags: Record<string, CommandFlagSchema>;
}

CommandResult<T>

命令执行结果(判别联合类型)。

type CommandResult<T = unknown> =
    | { success: true; result: T }
    | { success: false; error: string }

使用示例:

const result = await game.run('move piece1 piece2');

if (result.success) {
    console.log('命令执行成功', result.result);
} else {
    console.error('命令执行失败', result.error);
}

CommandDef<TContext, TFunc>

命令定义对象,用于 registry.register()

type CommandDef<TContext, TFunc extends CommandFunction<TContext>> = {
    schema: string | CommandSchema;
    run: TFunc;
}

type CommandFunction<TContext> = (ctx: TContext, ...args: any[]) => Promise<unknown>;

CommandRegistry<TContext>

命令注册表。

class CommandRegistry<TContext> extends Map<string, CommandRunner<TContext>> {
    register<TFunc>(
        ...args: [schema: CommandSchema | string, run: TFunc] | [CommandDef<TContext, TFunc>]
    ): (ctx, ...args) => Promise<TResult>
}

注册命令的两种方式:

// 方式 1直接传入 schema 和函数
registry.register('move <from> <to>', async function(ctx, from, to) {
    ctx.produce((state) => { /* 修改状态 */ });
    return { success: true, result: undefined };
});

// 方式 2使用 CommandDef 对象
registry.register({
    schema: 'move <from> <to>',
    run: async function(ctx, from, to) {
        ctx.produce((state) => { /* 修改状态 */ });
        return { success: true, result: undefined };
    }
});

parseCommand(input, schema?)

解析命令字符串为 Command 对象。

function parseCommand(input: string, schema?: CommandSchema): Command

parseCommandSchema(schemaStr, name?)

从字符串模式解析命令模式。

function parseCommandSchema(schemaStr: string, name?: string): CommandSchema

Schema 语法:

  • <param> - 必需参数
  • [param] - 可选参数
  • [param:type] - 带类型验证的参数(如 [count:number]
  • --option:value - 必需选项
  • [-o value] - 可选选项
  • [--flag] - 可选标志

随机数生成器 (RNG)

ReadonlyRNG

只读 RNG 接口(IGameContext.rng 返回此类型)。

interface ReadonlyRNG {
    next(max?: number): number;     // [0,1) 随机数,或 [0,max)
    nextInt(max: number): number;   // [0,max) 随机整数
}

RNG

可设置种子的 RNG 接口。

interface RNG extends ReadonlyRNG {
    setSeed(seed: number): void;
    getSeed(): number;
}

使用示例:

// 在 IGameContext 中使用
const roll = game.rng.nextInt(6) + 1;  // 1-6 的随机数

// 在区域操作中使用时
shuffle(region, parts, rng);  // 需要传入 RNG

可变信号 (MutableSignal)

MutableSignal<T>

扩展自 Preact Signal 的可变信号类,支持 mutative-style 的 produce 方法。

class MutableSignal<T> extends Signal<T> {
    produce(fn: (draft: T) => void): void;
    addInterruption(promise: Promise<void>): void;    // 添加中断 Promise用于动画等待
    clearInterruptions(): void;                        // 清除所有中断
    produceAsync(fn: (draft: T) => void): Promise<void>;  // 等待中断后更新状态
}

mutableSignal<T>(initial?, options?)

创建可变信号。

function mutableSignal<T>(initial?: T, options?: SignalOptions<T>): MutableSignal<T>

游戏主机 (GameHost)

GameHost<TState, TResult>

游戏会话的生命周期管理器。

class GameHost<TState, TResult> {
    readonly state: ReadonlySignal<TState>;                    // 游戏状态(响应式)
    readonly status: ReadonlySignal<GameHostStatus>;           // 运行状态
    readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;   // 当前活动提示的模式
    readonly activePromptPlayer: ReadonlySignal<string | null>;          // 当前等待输入的玩家
    readonly activePromptHint: ReadonlySignal<string | null>;            // 当前提示文本

    tryInput(input: string): string | null;                    // 尝试提交输入,返回错误信息或 null
    tryAnswerPrompt<TArgs>(def: PromptDef<TArgs>, ...args: TArgs): void;  // 尝试回答提示
    addInterruption(promise: Promise<void>): void;             // 注册中断 Promise用于动画
    clearInterruptions(): void;                                // 清除所有中断
    start(seed?: number): Promise<TResult>;                    // 启动游戏
    dispose(): void;                                           // 销毁游戏
    on(event: 'start' | 'dispose', listener: () => void): () => void;  // 注册事件监听
}

GameHostStatus

type GameHostStatus = 'created' | 'running' | 'disposed';

createGameHost<TState>(gameModule)

从游戏模块创建 GameHost 实例。

function createGameHost<TState extends Record<string, unknown>>(
    gameModule: GameModule<TState>
): GameHost<TState>

使用示例:

import { createGameHost } from '@/index';
import { gameModule } from './my-game';

const host = createGameHost(gameModule);

// 启动游戏
const result = await host.start(42);  // 传入种子

// 提交玩家输入
const error = host.tryInput('play X 0 0');
if (error) {
    console.error('输入错误:', error);
}

// 监听事件
host.on('start', () => console.log('游戏开始'));
host.on('dispose', () => console.log('游戏结束'));

// 销毁游戏
host.dispose();

Preact Signals 重新导出

export * from '@preact/signals-core';

开发者可直接使用 @preact/signals-core 的所有导出,包括:

  • Signal<T> - 基础信号类
  • ReadonlySignal<T> - 只读信号类型
  • signal<T>(value) - 创建信号
  • computed<T>(fn) - 创建计算信号
  • effect(fn) - 创建副作用
  • batch(fn) - 批量更新
  • untracked(fn) - 非追踪读取

测试辅助函数

以下函数主要用于测试代码:

createGameContext(options)

创建游戏上下文实例。

function createGameContext<TState>(options: {
    initialState: TState;
    registry?: CommandRegistry<IGameContext<TState>>;
    rng?: RNG;
}): IGameContext<TState>

使用示例:

import { createGameContext } from '@/core/game';
import { createInitialState, registry } from './my-game';

const ctx = createGameContext({
    initialState: createInitialState(),
    registry,
});

// 执行命令
await ctx.run('move piece1 piece2');

// 断言状态
expect(ctx.value.score).toBe(10);

createTestContext()

创建用于测试的游戏上下文(简化版)。

function createTestContext<TState>(initialState: TState): IGameContext<TState>

createTestRegion()

创建用于测试的区域。

function createTestRegion(): Region