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

176 lines
4.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: Create Game Module
description: Create a runnable logic module for a board game with 'boardgame-core', '@preact/signals' and 'mutative'
---
# 如何编写游戏模组
## 必须导出的接口
```typescript
import { IGameContext, createGameCommandRegistry } from '@/index';
export type GameState = { /* 你的状态类型 */ };
export type Game = IGameContext<GameState>;
export function createInitialState(): GameState { /* ... */ }
export async function start(game: Game) { /* 游戏主循环 */ }
```
或导出为 `GameModule` 对象:
```typescript
export const gameModule: GameModule<GameState> = { registry, createInitialState, start };
```
## 流程
### 0. 确认规则
`rule.md` 中描述:主题、配件、布置形式、游戏流程、玩家行动、胜利条件。
### 1. 创建类型 (`types.ts`)
- 使用字符串枚举表示游戏概念(如 `PlayerType = 'X' | 'O'`
- 使用 `Part<TMeta>` 表示配件(棋子、卡牌等)
- 使用 `Region` 表示区域(棋盘、牌堆等)
- 状态必须可序列化(不支持函数、`Map`、`Set`
```typescript
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. 创建配件
少量配件直接在代码创建:
```typescript
const pieces = createParts({ owner: 'X' }, (i) => `piece-${i}`, 5);
```
大量配件使用 CSV
```csv
# parts.csv
type,player,count
string,string,int
kitten,white,8
```
```typescript
import parts from "./parts.csv";
const pieces = createPartsFromTable(parts, (item, i) => `${item.type}-${i}`, (item) => item.count);
```
### 3. 创建 Prompts
```typescript
export const prompts = {
play: createPromptDef<['X' | 'O', number, number]>(
'play <player> <row:number> <col:number>',
'选择下子位置'
),
};
```
Schema 语法:`<param>` 必需,`[param]` 可选,`[param:type]` 带验证。详见 [API 参考](./references/api.md)。
### 4. 创建游戏流程
```typescript
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`
```typescript
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`
**运行测试:**
```bash
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/`
## 相关资源
- [API 参考](./references/api.md) - 完整 API 文档
- [AGENTS.md](../../AGENTS.md) - 项目代码规范