117 lines
3.0 KiB
Markdown
117 lines
3.0 KiB
Markdown
|
|
# 如何编写游戏模组
|
|||
|
|
|
|||
|
|
## 要求
|
|||
|
|
|
|||
|
|
游戏模组需要以下接口导出:
|
|||
|
|
```
|
|||
|
|
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来测试。
|
|||
|
|
|
|||
|
|
为每种游戏结束条件准备至少一条测试。
|
|||
|
|
为每种玩家行动至少准备一条测试。
|