Skip to content

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 代码无需关心元数据簿记。

接口 ​

ts
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
}

属性 ​

属性类型说明
stateGameState当前对局状态(只读视图)
randomRandomProvider引擎的随机源;Plugin 必须将所有随机决策通过它进行,严禁使用 Math.random

方法 ​

dispatch() ​

将一个 Action 入队,由引擎在当前 tick 内顺序处理。

ts
dispatch(action: Action): void

参数 ​

参数类型必填说明
actionAction是待派发的 Action

行为 ​

在 EventHandler 内调用时会进入队尾而非递归展开,防止无限递归并受 maxActionsPerTick 约束。委托给 engine.dispatch(action)。

示例 ​

ts
ctx.dispatch(createDrawCardAction(playerId, 5))

emit() ​

发出一个事件。

ts
emit(event: EventInit): void

参数 ​

参数类型必填说明
event.typestring是事件类型
event.payloadunknown是事件负载
event.playerIdstring否触发事件的玩家 id

行为 ​

仅需提供 { type, payload, playerId? },引擎会填充 id、timestamp、stateVersion 等元数据。委托给 engine.emitEvent(event),会立即触发 EventBus 同步派发。

示例 ​

ts
ctx.emit({
  type: 'GAME_WON',
  payload: { winnerIds: [playerId] },
  playerId
})

drawCards() ​

抽牌快捷方法。内部构造 drawCard Action 并 dispatch,因此与显式派发走相同的规则校验/执行/事件链路。

ts
drawCards(playerId: string, count: number): void

参数 ​

参数类型必填说明
playerIdstring是目标玩家 id
countnumber是抽牌数量

行为 ​

等价于 ctx.dispatch(createDrawCardAction(playerId, count))。

moveCard() ​

在两个 zone 之间移动一张牌(同步、不可变更新)。与 Action 处理路径不同,此方法每次调用单独 version+1。

ts
moveCard(cardId: string, fromZoneId: string, toZoneId: string): void

参数 ​

参数类型必填说明
cardIdstring是卡牌 id
fromZoneIdstring是源 zone id
toZoneIdstring是目标 zone id

抛错 ​

  • 卡牌不存在
  • 源 zone 不存在
  • 目标 zone 不存在
  • 卡牌不在源 zone 中
  • 卡牌已在目标 zone 中

getPlayer() ​

按 id 查询玩家。

ts
getPlayer(playerId: string): Player

抛错 ​

玩家不存在时抛 Error: Player ${playerId} not found。

getZone() ​

按 id 查询 zone。

ts
getZone(zoneId: string): CardZone

抛错 ​

zone 不存在时抛 Error: Zone ${zoneId} not found。

getCard() ​

按 id 查询卡牌。

ts
getCard(cardId: string): Card

抛错 ​

卡牌不存在时抛 Error: Card ${cardId} not found。

setVariable() ​

写入一个自定义变量。已存在的 key 会被覆盖。每次调用 version+1。

ts
setVariable(key: string, value: unknown): void

用途 ​

用于 Plugin 在 GameState 上持久化游戏专属数据,如阶段标记、底牌明牌、剩余回合数等。

getVariable() ​

读取自定义变量。未设置时返回 undefined。调用方需自行保证类型正确性。

ts
getVariable<T>(key: string): T | undefined

示例 ​

ts
const direction = ctx.getVariable<TurnDirection>('direction') ?? 1
const detector = ctx.getVariable<CombinationDetector>('poker.detector')

addPlayer() / removePlayer() ​

新增/移除玩家。每次调用单独 version+1。

ts
addPlayer(player: Player): void
removePlayer(playerId: string): void

抛错 ​

  • addPlayer — 玩家 id 已存在
  • removePlayer — 玩家不存在

addCard() / removeCard() ​

新增/移除卡牌。每次调用单独 version+1。

ts
addCard(card: Card): void
removeCard(cardId: string): void

抛错 ​

  • addCard — 卡牌 id 已存在
  • removeCard — 卡牌不存在

注意 ​

removeCard 只更新 cards 表,不会从任何 zone 的 cards 列表中清理引用。调用方若需要保持一致性,应自行先调用 removeCardFromZone。

addZone() / removeZone() ​

新增/移除 zone。每次调用单独 version+1。

ts
addZone(zone: CardZone): void
removeZone(zoneId: string): void

抛错 ​

  • addZone — zone id 已存在
  • removeZone — zone 不存在

注意 ​

removeZone 不校验该 zone 内是否仍持有卡牌引用,调用方应确保已先清空其 cards 列表以避免悬挂引用。

addCardToZone() ​

将已存在的卡牌加入指定 zone 的卡牌列表末尾。每次调用单独 version+1。

ts
addCardToZone(cardId: string, zoneId: string): void

注意 ​

不会校验该卡牌是否已经在其它 zone 中 —— 同一张卡牌同时存在于多个 zone 是调用方的责任。

抛错 ​

  • zone 不存在
  • 卡牌已在该 zone 中

removeCardFromZone() ​

从指定 zone 中移除卡牌。每次调用单独 version+1。

ts
removeCardFromZone(cardId: string, zoneId: string): void

抛错 ​

  • zone 不存在
  • 卡牌不在该 zone 中

setStatus() ​

设置对局状态(waiting / playing / paused / finished)。每次调用单独 version+1。

ts
setStatus(status: GameState['status']): void

setCurrentPlayer() ​

设置当前轮到行动的玩家;传 undefined 表示无当前玩家。每次调用单独 version+1。

ts
setCurrentPlayer(playerId: string | undefined): void

setTurn() ​

设置当前回合序号。每次调用单独 version+1。

ts
setTurn(turn: number): void

addWinner() ​

添加一名获胜者(幂等,重复添加无效果)。每次调用单独 version+1。

ts
addWinner(playerId: string): void

行为 ​

幂等性让 EventHandler 在多次触发同一胜负判定时安全调用。

addStateCheck() ​

注册一个游戏专属的不变式校验。

ts
addStateCheck(name: string, check: StateCheck): void

参数 ​

参数类型必填说明
namestring是校验名(用于去重与错误信息)
checkStateCheck是校验函数 (state: GameState) => string[]

行为 ​

引擎会在每次状态变更(createGame / start / dispatch / restoreSnapshot)后与内置的 StateValidator 校验一同运行。

抛错 ​

同名 check 已注册时抛 Error: State check '${name}' is already registered。

示例 ​

ts
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。

ts
removeStateCheck(name: string): void

GameContextImpl 实现细节 ​

GameContextImpl 持有 EngineInternal 引用,所有方法都委托给引擎或通过 stateUtils.* 做不可变更新。

EngineInternal 接口 ​

GameContextImpl 依赖的引擎内部接口,由 GameEngine 实现;测试中可替换为 stub。

ts
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),仅供引擎内部或测试在初始化阶段清理状态使用:

ts
removeVariable(key: string): void

示例 ​

Plugin setup 内初始化状态:

ts
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:

ts
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 约束