boardgame-core/.qwen/skills/create-game-module/SKILL.md

4.9 KiB
Raw Blame History

name description
Create Game Module Create a runnable logic module for a board game with 'boardgame-core', '@preact/signals' and 'mutative'

如何编写游戏模组

必须导出的接口

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

export type GameState = { /* 你的状态类型 */ };
export type Game = IGameContext<GameState>;

export function createInitialState(): GameState { /* ... */ }
export async function start(game: Game) { /* 游戏主循环 */ }

或导出为 GameModule 对象:

export const gameModule: GameModule<GameState> = { registry, createInitialState, start };

流程

0. 确认规则

rule.md 中描述:主题、配件、布置形式、游戏流程、玩家行动、胜利条件。

1. 创建类型 (types.ts)

  • 使用字符串枚举表示游戏概念(如 PlayerType = 'X' | 'O'
  • 使用 Part<TMeta> 表示配件(棋子、卡牌等)
  • 使用 Region 表示区域(棋盘、牌堆等)
  • 状态必须可序列化(不支持函数、MapSet
import { Part, Region } from '@/index';

export type Piece = Part<{ owner: 'X' | 'O' }>;
export type GameState = {
    board: Region;
    pieces: Record<string, Piece>;
    currentPlayer: 'X' | 'O';
    turn: number;
    winner: 'X' | 'O' | null;
};

2. 创建配件

少量配件直接在代码创建:

const pieces = createParts({ owner: 'X' }, (i) => `piece-${i}`, 5);

大量配件使用 CSV

# parts.csv
type,player,count
string,string,int
kitten,white,8
import parts from "./parts.csv";
const pieces = createPartsFromTable(parts, (item, i) => `${item.type}-${i}`, (item) => item.count);

3. 创建 Prompts

export const prompts = {
    play: createPromptDef<['X' | 'O', number, number]>(
        'play <player> <row:number> <col:number>',
        '选择下子位置'
    ),
};

Schema 语法:<param> 必需,[param] 可选,[param:type] 带验证。详见 API 参考

4. 创建游戏流程

export async function start(game: Game) {
    while (!game.value.winner) {
        const { row, col } = await game.prompt(
            prompts.play,
            (player, row, col) => {
                if (player !== game.value.currentPlayer) throw '无效玩家';
                if (!isValidMove(row, col)) throw '无效位置';
                if (isCellOccupied(game, row, col)) throw '位置已被占用';
                return { row, col };
            },
            game.value.currentPlayer
        );

        game.produce((state) => {
            state.pieces[`p-${row}-${col}`] = { /* ... */ };
        });

        const winner = checkWinner(game);
        if (winner) {
            game.produce((state) => { state.winner = winner; });
            break;
        }

        game.produce((state) => {
            state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
            state.turn++;
        });
    }
    return game.value;
}

注意事项:

  • game.produce(fn) 同步更新,game.produceAsync(fn) 异步更新(等待动画)
  • 验证器抛出字符串表示失败,返回值表示成功
  • 玩家取消时 game.prompt() 抛出异常
  • 循环必须有明确退出条件

5. 创建测试

位于 tests/samples/<game-name>.test.ts

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

describe('My Game', () => {
    function createTestContext() {
        return createGameContext(registry, createInitialState());
    }

    it('should perform action correctly', async () => {
        const game = createTestContext();
        await game.run('play X 0 0');
        expect(game.value.pieces['p-0-0']).toBeDefined();
    });

    it('should fail on invalid input', async () => {
        const game = createTestContext();
        const result = await game.run('invalid');
        expect(result.success).toBe(false);
    });

    it('should complete a full game cycle', async () => {
        // 模拟完整游戏流程,验证胜利条件
    });
});

要求:

  • 每种游戏结束条件、玩家行动、边界情况各至少一条测试
  • 使用 createGameContext(registry, initialState) 创建上下文
  • 使用 game.run('command') 执行命令,验证 game.value 状态
  • 测试命名使用 should... 格式,异步测试用 async/await

运行测试:

npm run test:run                              # 所有测试
npx vitest run tests/samples/my-game.test.ts  # 特定文件
npx vitest run -t "should perform"            # 特定用例

完整示例

参考 src/samples/boop/src/samples/regicide/

相关资源