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

246 lines
6.5 KiB
Markdown
Raw 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>;
// 创建 mutative 游戏初始状态
export function createInitialState(): GameState {
//...
}
// 创建命令注册表(可选)
export const registry = createGameCommandRegistry<GameState>();
// 运行游戏
export async function start(game: Game) {
// ...
}
```
或者导出为 `GameModule` 对象:
```typescript
import { GameModule } from '@/index';
export const gameModule: GameModule<GameState> = {
registry,
createInitialState,
start,
};
```
## 流程
### 0. 确认规则
规则应当存放在 `rule.md`
描述一个桌面游戏的以下要素:
- **主题**:游戏的世界观和背景
- **配件**:棋子、卡牌、骰子等物理组件
- **游戏布置形式**:棋盘、版图、卡牌放置区等
- **游戏流程**:回合结构、阶段划分
- **玩家行动**:每回合玩家可以做什么
- **胜利条件与终局结算**:如何判定胜负
### 1. 创建类型
创建 `types.ts` 并导出游戏所用的类型。
- 为游戏概念创建字符串枚举类型(如 `PlayerType = 'X' | 'O'`
- 使用 `Part<TMeta>` 为游戏配件创建对象类型
- 使用 `Region` 为游戏区域创建容器类型
- 设计游戏的全局状态类型
游戏使用 `mutative` 不可变类型驱动。状态类型必须是**可序列化的**(不支持函数、`Map`、`Set` 等)。
```typescript
import { Part, Region } from '@/index';
export type PlayerType = 'X' | 'O';
export type PieceMeta = {
owner: PlayerType;
};
export type Piece = Part<PieceMeta>;
export type GameState = {
board: Region;
pieces: Record<string, Piece>;
currentPlayer: PlayerType;
turn: number;
winner: PlayerType | null;
};
```
### 2. 创建游戏配件表
若某种游戏配件数量较大可使用csv文件进行配置否则在代码中inline创建。
csv文件遵循以下要求
- 从`#`开头的内容会被当作注释忽略
- 第二行为数据类型,会用于生成.d.ts文件
- 可以有空行
```csv
# parts.csv
type,player,count
string,string,int
kitten,white,8
kitten,black,8
cat,white,8
cat,black,8
- ```
```typescript
import parts from "./parts.csv";
const pieces = createPartsFromTable(
parts,
(item, index) => `${item.player}-${item.type}-${index + 1}`,
(item) => item.count
) as Record<string, BoopPart>;
```
### 3. 创建 Prompts
使用 prompt 来描述需要玩家进行的行动命令 schema。
- prompt 包含一个 schema 和若干参数
- 每个参数通常指定某个配件 ID、某个枚举字符串、或者某个数字
- 参数类型必须是原始类型(`string`、`number`)或字符串枚举
```typescript
import { createPromptDef } from '@/index';
export const prompts = {
play: createPromptDef<[PlayerType, number, number]>(
'play <player> <row:number> <col:number>',
'选择下子位置'
),
};
```
Prompt schema 语法:
- `<param>` - 必需参数
- `[param]` - 可选参数
- `[param:type]` - 带类型验证的参数(如 `[count:number]`
### 3. 创建游戏流程
游戏主循环负责协调游戏进程、等待玩家输入、更新状态。
```typescript
export async function start(game: Game) {
while (true) {
// game.value 可获取当前的全局状态
const currentPlayer = game.value.currentPlayer;
const turnNumber = game.value.turn + 1;
const turnOutput = await turn(game, currentPlayer, turnNumber);
// 更新状态
await game.produceAsync((state) => {
state.winner = turnOutput.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
state.turn = turnNumber;
}
});
// 检查游戏结束条件
if (game.value.winner) break;
}
return game.value;
}
```
回合逻辑示例:
```typescript
async function turn(game: Game, turnPlayer: PlayerType, turnNumber: number) {
// 获取玩家输入
const { player, row, col } = await game.prompt(
prompts.play,
(player, row, col) => {
if (player !== turnPlayer) {
throw `无效的玩家: ${player}。应为 ${turnPlayer}。`;
} else if (!isValidMove(row, col)) {
throw `无效位置: (${row}, ${col})。必须在 0 到 ${BOARD_SIZE - 1} 之间。`;
} else if (isCellOccupied(game, row, col)) {
throw `格子 (${row}, ${col}) 已被占用。`;
} else {
return { player, row, col };
}
},
game.value.currentPlayer
);
// 执行放置逻辑
placePiece(game, row, col, turnPlayer);
// 返回回合结果
return { winner: checkWinner(game) };
}
```
**注意事项:**
- `game.produce(fn)` 用于同步更新状态
- `game.produceAsync(fn)` 用于异步更新状态(会等待中断 Promise 完成,适用于播放动画)
- 验证器函数中抛出字符串错误会返回给玩家,玩家可重新输入
- 循环必须有明确的退出条件,避免无限循环
- 玩家取消输入时,`game.prompt()` 会抛出异常,需要适当处理
### 4. 创建测试
基于 `createGameContext` 来测试游戏逻辑。
- 为每种游戏结束条件准备至少一条测试
- 为每种玩家行动准备至少一条测试
- 使用 `createTestContext()``createTestRegion()` 测试辅助函数
```typescript
import { createGameContext } from '@/core/game';
import { createInitialState, registry, start } from './my-game';
describe('My Game', () => {
it('should detect horizontal win for X', async () => {
const ctx = createGameContext({
initialState: createInitialState(),
registry,
});
// 执行一系列操作
await ctx.run('play X 0 0');
await ctx.run('play O 1 0');
await ctx.run('play X 0 1');
await ctx.run('play O 1 1');
await ctx.run('play X 0 2');
expect(ctx.value.winner).toBe('X');
});
});
```
## 完整示例
参考 `src/samples/boop/` 获取完整的`boop`游戏实现。
## 相关资源
- [API 参考](./references/api.md) - 完整的 API 文档
- [AGENTS.md](../../AGENTS.md) - 项目代码规范和架构说明