3.0 KiB
3.0 KiB
如何编写游戏模组
要求
游戏模组需要以下接口导出:
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) {
// ...
}
流程
- 确认规则。
规则应当存放在rule.md。
描述一个桌面游戏的以下要素:
- 主题
- 配件
- 游戏布置形式
- 游戏流程
- 玩家行动
- 胜利条件与终局结算
- 创建类型:创建
types.ts并导出游戏所用的类型。
- 为游戏概念创建字符串枚举类型。
- 使用
@/core/part.ts为游戏配件创建对象类型。 - 使用
@/core/region.ts为游戏区域创建容器类型。 - 设计游戏的全局状态类型。
游戏使用mutative不可变类型驱动。
- 创建prompts:
使用prompt来描述需要玩家进行的行动命令schema。
- prompt包含一个id和若干params。
- 每个param通常指定某个配件id、某个枚举字符串、或者某个数字。
export const prompts = {
play: createPromptDef<[PlayerType, number, number]>(
'play <player> <row:number> <col:number>',
'选择下子位置')
}
- 创建游戏流程:
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;
}
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);
}
- 创建测试
基于@/core/game.ts的createGameContext来测试。
为每种游戏结束条件准备至少一条测试。 为每种玩家行动至少准备一条测试。