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

176 lines
4.9 KiB
Markdown
Raw Normal View History

2026-04-10 12:47:00 +08:00
---
name: Create Game Module
description: Create a runnable logic module for a board game with 'boardgame-core', '@preact/signals' and 'mutative'
---
# 如何编写游戏模组
2026-04-10 13:52:32 +08:00
## 必须导出的接口
2026-04-10 12:47:00 +08:00
```typescript
import { IGameContext, createGameCommandRegistry } from '@/index';
2026-04-10 13:52:32 +08:00
export type GameState = { /* 你的状态类型 */ };
2026-04-10 12:47:00 +08:00
export type Game = IGameContext<GameState>;
2026-04-10 13:52:32 +08:00
export function createInitialState(): GameState { /* ... */ }
export async function start(game: Game) { /* 游戏主循环 */ }
2026-04-10 12:47:00 +08:00
```
2026-04-10 13:52:32 +08:00
或导出为 `GameModule` 对象:
2026-04-10 12:47:00 +08:00
```typescript
2026-04-10 13:52:32 +08:00
export const gameModule: GameModule<GameState> = { registry, createInitialState, start };
2026-04-10 12:47:00 +08:00
```
## 流程
### 0. 确认规则
2026-04-10 13:52:32 +08:00
`rule.md` 中描述:主题、配件、布置形式、游戏流程、玩家行动、胜利条件。
2026-04-10 12:47:00 +08:00
2026-04-10 13:52:32 +08:00
### 1. 创建类型 (`types.ts`)
2026-04-10 12:47:00 +08:00
2026-04-10 13:52:32 +08:00
- 使用字符串枚举表示游戏概念(如 `PlayerType = 'X' | 'O'`
- 使用 `Part<TMeta>` 表示配件(棋子、卡牌等)
- 使用 `Region` 表示区域(棋盘、牌堆等)
- 状态必须可序列化(不支持函数、`Map`、`Set`
2026-04-10 12:47:00 +08:00
```typescript
import { Part, Region } from '@/index';
2026-04-10 13:52:32 +08:00
export type Piece = Part<{ owner: 'X' | 'O' }>;
2026-04-10 12:47:00 +08:00
export type GameState = {
board: Region;
pieces: Record<string, Piece>;
2026-04-10 13:52:32 +08:00
currentPlayer: 'X' | 'O';
2026-04-10 12:47:00 +08:00
turn: number;
2026-04-10 13:52:32 +08:00
winner: 'X' | 'O' | null;
2026-04-10 12:47:00 +08:00
};
```
2026-04-10 13:52:32 +08:00
### 2. 创建配件
2026-04-10 12:47:00 +08:00
2026-04-10 13:52:32 +08:00
少量配件直接在代码创建:
```typescript
const pieces = createParts({ owner: 'X' }, (i) => `piece-${i}`, 5);
```
2026-04-10 12:47:00 +08:00
2026-04-10 13:52:32 +08:00
大量配件使用 CSV
2026-04-10 12:47:00 +08:00
```csv
# parts.csv
type,player,count
string,string,int
kitten,white,8
2026-04-10 13:45:10 +08:00
```
2026-04-10 12:47:00 +08:00
```typescript
import parts from "./parts.csv";
2026-04-10 13:52:32 +08:00
const pieces = createPartsFromTable(parts, (item, i) => `${item.type}-${i}`, (item) => item.count);
2026-04-10 12:47:00 +08:00
```
### 3. 创建 Prompts
```typescript
export const prompts = {
2026-04-10 13:52:32 +08:00
play: createPromptDef<['X' | 'O', number, number]>(
2026-04-10 12:47:00 +08:00
'play <player> <row:number> <col:number>',
'选择下子位置'
),
};
```
2026-04-10 13:52:32 +08:00
Schema 语法:`<param>` 必需,`[param]` 可选,`[param:type]` 带验证。详见 [API 参考](./references/api.md)。
2026-04-10 12:47:00 +08:00
2026-04-10 13:52:32 +08:00
### 4. 创建游戏流程
2026-04-10 12:47:00 +08:00
```typescript
export async function start(game: Game) {
2026-04-10 13:52:32 +08:00
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}`] = { /* ... */ };
2026-04-10 12:47:00 +08:00
});
2026-04-10 13:52:32 +08:00
const winner = checkWinner(game);
if (winner) {
game.produce((state) => { state.winner = winner; });
break;
}
2026-04-10 12:47:00 +08:00
2026-04-10 13:52:32 +08:00
game.produce((state) => {
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
state.turn++;
});
}
2026-04-10 12:47:00 +08:00
return game.value;
}
```
**注意事项:**
2026-04-10 13:52:32 +08:00
- `game.produce(fn)` 同步更新,`game.produceAsync(fn)` 异步更新(等待动画)
- 验证器抛出字符串表示失败,返回值表示成功
- 玩家取消时 `game.prompt()` 抛出异常
- 循环必须有明确退出条件
2026-04-10 12:47:00 +08:00
2026-04-10 13:52:32 +08:00
### 5. 创建测试
2026-04-10 12:47:00 +08:00
2026-04-10 13:52:32 +08:00
位于 `tests/samples/<game-name>.test.ts`
2026-04-10 13:45:10 +08:00
2026-04-10 12:47:00 +08:00
```typescript
2026-04-10 13:45:10 +08:00
import { describe, it, expect } from 'vitest';
2026-04-10 12:47:00 +08:00
import { createGameContext } from '@/core/game';
2026-04-10 13:45:10 +08:00
import { registry, createInitialState } from './my-game';
2026-04-10 12:47:00 +08:00
describe('My Game', () => {
2026-04-10 13:45:10 +08:00
function createTestContext() {
return createGameContext(registry, createInitialState());
}
2026-04-10 13:52:32 +08:00
it('should perform action correctly', async () => {
const game = createTestContext();
await game.run('play X 0 0');
expect(game.value.pieces['p-0-0']).toBeDefined();
2026-04-10 13:45:10 +08:00
});
2026-04-10 12:47:00 +08:00
2026-04-10 13:52:32 +08:00
it('should fail on invalid input', async () => {
const game = createTestContext();
const result = await game.run('invalid');
expect(result.success).toBe(false);
2026-04-10 13:45:10 +08:00
});
2026-04-10 13:52:32 +08:00
it('should complete a full game cycle', async () => {
// 模拟完整游戏流程,验证胜利条件
2026-04-10 12:47:00 +08:00
});
});
```
2026-04-10 13:52:32 +08:00
**要求:**
- 每种游戏结束条件、玩家行动、边界情况各至少一条测试
- 使用 `createGameContext(registry, initialState)` 创建上下文
- 使用 `game.run('command')` 执行命令,验证 `game.value` 状态
- 测试命名使用 `should...` 格式,异步测试用 `async/await`
2026-04-10 13:45:10 +08:00
**运行测试:**
```bash
2026-04-10 13:52:32 +08:00
npm run test:run # 所有测试
npx vitest run tests/samples/my-game.test.ts # 特定文件
npx vitest run -t "should perform" # 特定用例
2026-04-10 13:45:10 +08:00
```
2026-04-10 12:47:00 +08:00
## 完整示例
2026-04-10 13:52:32 +08:00
参考 `src/samples/boop/``src/samples/regicide/`
2026-04-10 12:47:00 +08:00
## 相关资源
2026-04-10 13:52:32 +08:00
- [API 参考](./references/api.md) - 完整 API 文档
- [AGENTS.md](../../AGENTS.md) - 项目代码规范