boardgame-core/skills/game-module.md

117 lines
3.0 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.

# 如何编写游戏模组
## 要求
游戏模组需要以下接口导出:
```
import {createGameCommandRegistry, IGameContext} from "boardgame-core";
// 定义类型
export type GameState = {
//...
}
export type Game = IGameContext<GameState>;
// 创建mutative游戏初始状态
export function createInitialState(): GameState {
//...
}
// 运行游戏
export async function start(game: Game) {
// ...
}
```
## 流程
0. 确认规则。
规则应当存放在`rule.md`。
描述一个桌面游戏的以下要素:
- 主题
- 配件
- 游戏布置形式
- 游戏流程
- 玩家行动
- 胜利条件与终局结算
1. 创建类型:创建`types.ts`并导出游戏所用的类型。
- 为游戏概念创建字符串枚举类型。
- 使用`@/core/part.ts`为游戏配件创建对象类型。
- 使用`@/core/region.ts`为游戏区域创建容器类型。
- 设计游戏的全局状态类型。
游戏使用`mutative`不可变类型驱动。
2. 创建prompts
使用prompt来描述需要玩家进行的行动命令schema。
- prompt包含一个id和若干params。
- 每个param通常指定某个配件id、某个枚举字符串、或者某个数字。
```typescript
export const prompts = {
play: createPromptDef<[PlayerType, number, number]>(
'play <player> <row:number> <col:number>',
'选择下子位置')
}
```
3. 创建游戏流程:
```typescript
export async function start(game: TicTacToeGame) {
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 run(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) {
// 获取玩家输入
const {player, row, col} = await game.prompt(
prompts.play,
(player, row, col) => {
if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
} else if (!isValidMove(row, col)) {
throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
} else if (isCellOccupied(game, row, col)) {
throw `Cell (${row}, ${col}) is already occupied.`;
} else {
return {player, row, col};
}
},
game.value.currentPlayer
);
placePiece(game, row, col, turnPlayer);
}
```
4. 创建测试
基于`@/core/game.ts`的createGameContext来测试。
为每种游戏结束条件准备至少一条测试。
为每种玩家行动至少准备一条测试。