GameContext
GameContext 是 Plugin 与引擎交互的唯一许可入口。Plugin 在 setup、EventHandler 中,以及(经由 RuleContext)规则中都会收到一个 GameContext。其实现位于 GameContextImpl。
概述
GameContext 接口由 GameContextImpl 实现。GameContextImpl 是 Plugin 与引擎之间的桥接:
- 每个 mutation primitive 通过
stateUtils.*执行一次不可变更新并state.version + 1(因此 mutation 之间发出的事件携带不同的 stateVersion) dispatch与emit委托给引擎,由引擎负责 ActionQueue 的递归控制以及事件元数据加盖
与原始规范的偏差:emit 接受部分 EventInit({ type, payload, playerId? })而非完整 GameEvent。引擎会填充 id、timestamp、stateVersion。这让 Plugin 代码无需关心元数据簿记。
接口
export interface GameContext {
readonly state: GameState
readonly random: RandomProvider
dispatch(action: Action): void
emit(event: EventInit): void
drawCards(playerId: string, count: number): void
moveCard(cardId: string, fromZoneId: string, toZoneId: string): void
getPlayer(playerId: string): Player
getZone(zoneId: string): CardZone
getCard(cardId: string): Card
setVariable(key: string, value: unknown): void
getVariable<T>(key: string): T | undefined
addPlayer(player: Player): void
removePlayer(playerId: string): void
addCard(card: Card): void
removeCard(cardId: string): void
addZone(zone: CardZone): void
removeZone(zoneId: string): void
addCardToZone(cardId: string, zoneId: string): void
removeCardFromZone(cardId: string, zoneId: string): void
setStatus(status: GameState['status']): void
setCurrentPlayer(playerId: string | undefined): void
setTurn(turn: number): void
addWinner(playerId: string): void
addStateCheck(name: string, check: StateCheck): void
removeStateCheck(name: string): void
}属性
| 属性 | 类型 | 说明 |
|---|---|---|
state | GameState | 当前对局状态(只读视图) |
random | RandomProvider | 引擎的随机源;Plugin 必须将所有随机决策通过它进行,严禁使用 Math.random |
方法
dispatch()
将一个 Action 入队,由引擎在当前 tick 内顺序处理。
dispatch(action: Action): void参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | Action | 是 | 待派发的 Action |
行为
在 EventHandler 内调用时会进入队尾而非递归展开,防止无限递归并受 maxActionsPerTick 约束。委托给 engine.dispatch(action)。
示例
ctx.dispatch(createDrawCardAction(playerId, 5))emit()
发出一个事件。
emit(event: EventInit): void参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
event.type | string | 是 | 事件类型 |
event.payload | unknown | 是 | 事件负载 |
event.playerId | string | 否 | 触发事件的玩家 id |
行为
仅需提供 { type, payload, playerId? },引擎会填充 id、timestamp、stateVersion 等元数据。委托给 engine.emitEvent(event),会立即触发 EventBus 同步派发。
示例
ctx.emit({
type: 'GAME_WON',
payload: { winnerIds: [playerId] },
playerId
})drawCards()
抽牌快捷方法。内部构造 drawCard Action 并 dispatch,因此与显式派发走相同的规则校验/执行/事件链路。
drawCards(playerId: string, count: number): void参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
playerId | string | 是 | 目标玩家 id |
count | number | 是 | 抽牌数量 |
行为
等价于 ctx.dispatch(createDrawCardAction(playerId, count))。
moveCard()
在两个 zone 之间移动一张牌(同步、不可变更新)。与 Action 处理路径不同,此方法每次调用单独 version+1。
moveCard(cardId: string, fromZoneId: string, toZoneId: string): void参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
cardId | string | 是 | 卡牌 id |
fromZoneId | string | 是 | 源 zone id |
toZoneId | string | 是 | 目标 zone id |
抛错
- 卡牌不存在
- 源 zone 不存在
- 目标 zone 不存在
- 卡牌不在源 zone 中
- 卡牌已在目标 zone 中
getPlayer()
按 id 查询玩家。
getPlayer(playerId: string): Player抛错
玩家不存在时抛 Error: Player ${playerId} not found。
getZone()
按 id 查询 zone。
getZone(zoneId: string): CardZone抛错
zone 不存在时抛 Error: Zone ${zoneId} not found。
getCard()
按 id 查询卡牌。
getCard(cardId: string): Card抛错
卡牌不存在时抛 Error: Card ${cardId} not found。
setVariable()
写入一个自定义变量。已存在的 key 会被覆盖。每次调用 version+1。
setVariable(key: string, value: unknown): void用途
用于 Plugin 在 GameState 上持久化游戏专属数据,如阶段标记、底牌明牌、剩余回合数等。
getVariable()
读取自定义变量。未设置时返回 undefined。调用方需自行保证类型正确性。
getVariable<T>(key: string): T | undefined示例
const direction = ctx.getVariable<TurnDirection>('direction') ?? 1
const detector = ctx.getVariable<CombinationDetector>('poker.detector')addPlayer() / removePlayer()
新增/移除玩家。每次调用单独 version+1。
addPlayer(player: Player): void
removePlayer(playerId: string): void抛错
addPlayer— 玩家 id 已存在removePlayer— 玩家不存在
addCard() / removeCard()
新增/移除卡牌。每次调用单独 version+1。
addCard(card: Card): void
removeCard(cardId: string): void抛错
addCard— 卡牌 id 已存在removeCard— 卡牌不存在
注意
removeCard 只更新 cards 表,不会从任何 zone 的 cards 列表中清理引用。调用方若需要保持一致性,应自行先调用 removeCardFromZone。
addZone() / removeZone()
新增/移除 zone。每次调用单独 version+1。
addZone(zone: CardZone): void
removeZone(zoneId: string): void抛错
addZone— zone id 已存在removeZone— zone 不存在
注意
removeZone 不校验该 zone 内是否仍持有卡牌引用,调用方应确保已先清空其 cards 列表以避免悬挂引用。
addCardToZone()
将已存在的卡牌加入指定 zone 的卡牌列表末尾。每次调用单独 version+1。
addCardToZone(cardId: string, zoneId: string): void注意
不会校验该卡牌是否已经在其它 zone 中 —— 同一张卡牌同时存在于多个 zone 是调用方的责任。
抛错
- zone 不存在
- 卡牌已在该 zone 中
removeCardFromZone()
从指定 zone 中移除卡牌。每次调用单独 version+1。
removeCardFromZone(cardId: string, zoneId: string): void抛错
- zone 不存在
- 卡牌不在该 zone 中
setStatus()
设置对局状态(waiting / playing / paused / finished)。每次调用单独 version+1。
setStatus(status: GameState['status']): voidsetCurrentPlayer()
设置当前轮到行动的玩家;传 undefined 表示无当前玩家。每次调用单独 version+1。
setCurrentPlayer(playerId: string | undefined): voidsetTurn()
设置当前回合序号。每次调用单独 version+1。
setTurn(turn: number): voidaddWinner()
添加一名获胜者(幂等,重复添加无效果)。每次调用单独 version+1。
addWinner(playerId: string): void行为
幂等性让 EventHandler 在多次触发同一胜负判定时安全调用。
addStateCheck()
注册一个游戏专属的不变式校验。
addStateCheck(name: string, check: StateCheck): void参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 校验名(用于去重与错误信息) |
check | StateCheck | 是 | 校验函数 (state: GameState) => string[] |
行为
引擎会在每次状态变更(createGame / start / dispatch / restoreSnapshot)后与内置的 StateValidator 校验一同运行。
抛错
同名 check 已注册时抛 Error: State check '${name}' is already registered。
示例
ctx.addStateCheck('deck-total', (state) => {
const total = Object.values(state.zones)
.flatMap(z => z.cards).length
return total === 54 ? [] : [`expected 54 cards, got ${total}`]
})removeStateCheck()
移除此前注册的 state check。不存在时为 no-op。
removeStateCheck(name: string): voidGameContextImpl 实现细节
GameContextImpl 持有 EngineInternal 引用,所有方法都委托给引擎或通过 stateUtils.* 做不可变更新。
EngineInternal 接口
GameContextImpl 依赖的引擎内部接口,由 GameEngine 实现;测试中可替换为 stub。
export interface EngineInternal {
readonly state: GameState
readonly random: RandomProvider
setState(updater: (prev: GameState) => GameState): void
dispatch(action: Action): void
emitEvent(init: EventInit): void
addStateCheck(name: string, check: StateCheck): void
removeStateCheck(name: string): void
}removeVariable 私有方法
GameContextImpl 上有一个未出现在公共 GameContext 接口上的方法 removeVariable(key),仅供引擎内部或测试在初始化阶段清理状态使用:
removeVariable(key: string): void示例
Plugin setup 内初始化状态:
setup(ctx) {
if (!ctx.state.zones['deck']) {
ctx.addZone(createZone({ id: 'deck', type: 'deck' }))
}
ctx.setVariable('initialized', true)
ctx.setVariable('direction', 1)
}EventHandler 内链式 dispatch + emit:
events: [
{
type: 'CARD_PLAYED',
handle(event, ctx) {
const { playerId } = event.payload as { playerId: string }
const handZone = ctx.state.zones[playerHandZoneId(playerId)]
if (handZone && handZone.cards.length === 0) {
ctx.addWinner(playerId)
ctx.setStatus('finished')
ctx.emit({ type: 'GAME_WON', payload: { winnerIds: [playerId] }, playerId })
ctx.emit({ type: 'GAME_FINISHED', payload: { winnerIds: [playerId] } })
} else {
ctx.dispatch(createEndTurnAction(playerId))
}
}
}
]注意事项
- Mutation primitive 每次调用单独
version+1,与 Action handler 路径不同(后者引擎统一 bump 一次) - Plugin 必须将所有随机决策通过
ctx.random,禁止使用Math.random以保证可回放 emit接受的是EventInit(无 id/timestamp/stateVersion),引擎会补全元数据dispatch在 EventHandler 内调用进入队尾而非递归展开,受maxActionsPerTick约束