4.9 KiB
4.9 KiB
| 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表示区域(棋盘、牌堆等) - 状态必须可序列化(不支持函数、
Map、Set)
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/。