Skip to content

GameEngine ​

GameEngine 是引擎门面,将 PluginManager / RuleEngine / ActionExecutor / EventBus / RandomProvider / ActionHistory / Snapshot / StateValidator 围绕一个 immutable GameState 组装起来。

概述 ​

GameEngine 实现了 EngineInternal 接口,并通过公共 API 暴露完整的对局生命周期管理能力。dispatch 流程为:

dispatch(Action)
  → ActionQueue(非递归;一次处理一个 action)
  → RuleEngine.validate(state, action, ctx)
  → 拒绝时:state 不变,返回 denied
  → 通过时:ActionExecutor.execute(state, action, random) → { state, events }
  → bump state.version 一次
  → emit 每个事件(同步;handler 可入队新 action)
  → 成功时:将 ActionRecord 追加到 ActionHistory
  → 校验 state(除非 validationMode 为 'none')

由 event handler 触发的 dispatch 进入队尾,在同一个 tick 内处理,受 maxActionsPerTick 约束以打断意外的无限循环。

构造函数 ​

ts
new GameEngine(config?: GameConfigInit & { validation?: ValidationMode })
参数类型必填默认值说明
config.gameIdstring否'game-default'对局 id
config.maxActionsPerTicknumber否1000单 tick 内最大 Action 数
config.defaultDirection1 | -1否1默认回合方向
config.seednumber否自动生成随机种子
config.validationValidationMode否'always'状态校验模式

构造时:

  1. resolveConfig(config) 解析配置
  2. 解析种子:config.seed ?? generateRandomSeed()
  3. 把实际种子回写到 config(外部可通过 getConfig() 读取)
  4. 依据是否请求了种子选择 provider:SeededRandomProvider 或 MathRandomProvider
  5. createGameState({ gameId, seed }) 初始化空 state
  6. new GameContextImpl(this) 创建桥接实例

属性 ​

GameEngine 公开访问的属性:

属性类型说明
stateGameState当前对局状态(不可变引用,外部不应直接修改)

通过 EngineInternal 接口暴露的内部 API(由 GameContextImpl 使用):

属性类型说明
randomRandomProvider引擎的随机源
setState(updater)(prev: GameState) => GameState => void用纯函数 updater 计算下一个 state
emitEvent(init)(EventInit) => void用当前 state.version 补全 EventInit,加入 tickEvents 并触发 EventBus
addStateCheck(name, check)(string, StateCheck) => void注册状态校验
removeStateCheck(name)(string) => void移除状态校验
dispatch(action)(Action) => void把 Action 入队,由引擎主循环统一处理

方法 ​

use() ​

注册一个插件。必须在 createGame 之前调用。

ts
use(plugin: GamePlugin): this

返回值 ​

this —— 支持链式调用。

行为 ​

  • 引擎已销毁时抛 EngineDestroyedError
  • 委托 PluginManager.use 注册(重复 id 抛 DuplicatePluginError)

示例 ​

ts
engine
  .use(StandardDeckPlugin)
  .use(TurnPlugin)
  .use(DrawPlugin)
  .use(SimpleGamePlugin)

createGame() ​

创建并初始化一局游戏。

ts
createGame(options: CreateGameOptions): this

参数 ​

参数类型必填说明
options.playersCreateGamePlayerInit[]是玩家列表
options.gameIdstring否对局 id 覆盖
options.initialVariablesRecord<string, unknown>否初始 variables
options.seednumber否单局种子覆盖

CreateGamePlayerInit 字段:

字段类型必填默认值说明
idstring是—玩家 id
namestring是—显示名
seatnumber否下标 +1座位号;同局不可重复
statusPlayerStatus否'active'玩家状态
dataRecord<string, unknown>否{}游戏专属数据

返回值 ​

this

行为 ​

  1. 校验前置条件(未销毁 / 对局未创建 / 玩家非空 / 座位不重复)
  2. 解析玩家:对每个 init 调用 createPlayer,按 seat ?? index + 1 填充座位
  3. 写入初始 GameState
  4. wirePlugins() 把插件的 rules / actions / events 注册到子系统
  5. 进入 setup 阶段(inSetupPhase = true)
  6. 调用每个插件的 setup(context)
  7. 标记 gameCreated = true,发出 GAME_CREATED 事件
  8. pumpIfIdle 处理 setup 阶段链式 dispatch
  9. 退出 setup 阶段
  10. maybeValidate('createGame') 状态校验

抛错 ​

  • EngineDestroyedError — 引擎已销毁
  • Error: Game already created... — 对局已创建
  • Error: Cannot create a game with no players
  • Error: Duplicate seat ${seat} for player ${id}
  • DuplicateRuleError(被包裹为 Plugin '${id}' failed to register rule '${id}'...)
  • DuplicateActionHandlerError
  • StateValidationError — 状态校验失败

start() ​

把对局状态从 waiting 切换到 playing,并发出 GAME_STARTED 事件。

ts
start(): this

返回值 ​

this

行为 ​

  1. 校验前置条件(未销毁 / 对局已创建 / 当前状态必须是 waiting)
  2. setState(prev => bumpVersion(setStatusUtil(prev, 'playing')))
  3. 进入 setup 阶段 → startTick → emit GAME_STARTED → pumpIfIdle → finishTick
  4. 退出 setup 阶段
  5. maybeValidate('start')

GAME_STARTED 处理器中触发的链式 dispatch 不会被记入 ActionHistory,回放时通过重新执行 start 复现。

抛错 ​

  • EngineDestroyedError
  • Error: Game has not been created yet; call createGame() first
  • Error: Cannot start game in status: ${status}
  • StateValidationError

dispatch() ​

派发一个 Action。

ts
dispatch(action: Action): DispatchResult

参数 ​

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

返回值 ​

DispatchResult:

ts
interface DispatchResult {
  ok: boolean
  action: Action
  denyReason?: string
  denyCode?: string
  error?: string
  events: GameEvent[]
  processedActions: ProcessedAction[]
}

行为 ​

  • 顶级调用(非 EventHandler 链式触发):同步处理整个 tick 并返回完整结果
  • 链式调用(EventHandler 内调用):仅入队并返回占位结果,实际处理由外层 tick 完成

顶级 dispatch 处理流程:

  1. startTick() 重置 tick 缓冲
  2. 标记 action 为顶级(除非处于 setup 阶段)
  3. 入队 queue.push(action)
  4. pump() 主循环消费队列
  5. finishTick()
  6. buildDispatchResult(action, wasTopLevel) 构造结果

pump 内的 processAction 步骤:

  1. RuleEngine 校验 → 拒绝时返回 { allowed: false, denyReason, denyCode, events: [] }
  2. ActionExecutor.execute → { state, events };执行抛错时返回 { allowed: true, error, events: [] }
  3. this.state = { ...nextState, version: this.state.version + 1 } —— 一次性 bump
  4. 把每个 EventInit 通过 eventFromInit 补全为 GameEvent 并 eventBus.emit
  5. 仅顶级 dispatch 记入 ActionHistory
  6. maybeValidate('dispatch')

抛错 ​

  • EngineDestroyedError
  • ActionQueueOverflowError — 单 tick 内 Action 数超过 maxActionsPerTick
  • StateValidationError

getState() ​

返回当前 GameState(不可变引用,外部不应直接修改)。

ts
getState(): GameState

getPlugins() ​

返回所有已注册插件的浅拷贝列表(保持插入顺序)。

ts
getPlugins(): GamePlugin[]

getConfig() ​

返回配置的浅拷贝,防止外部修改引擎内部配置。

ts
getConfig(): GameConfig

getSeed() ​

返回引擎使用的种子(未设置时返回 0)。

ts
getSeed(): number

getRandom() ​

返回引擎的 RandomProvider。

ts
getRandom(): RandomProvider

getActionHistory() ​

返回 ActionHistory 实例。

ts
getActionHistory(): ActionHistory

getStateValidator() ​

返回 StateValidator 实例。

ts
getStateValidator(): StateValidator

subscribe() ​

订阅指定类型的事件,支持 WILDCARD 监听全部类型。

ts
subscribe(type: string | typeof WILDCARD, listener: EventListener): () => void

参数 ​

参数类型必填说明
typestring | typeof WILDCARD是事件类型或 WILDCARD
listenerEventListener是监听器函数

返回值 ​

() => void —— 取消订阅函数。

抛错 ​

  • EngineDestroyedError

subscribeAll() ​

订阅所有事件(等价于 subscribe(WILDCARD, listener))。

ts
subscribeAll(listener: EventListener): () => void

createSnapshot() ​

创建当前 GameState 的快照,用于持久化或回滚。

ts
createSnapshot(): GameSnapshot

返回值 ​

GameSnapshot —— 包含 gameId / version / state(深拷贝)/ createdAt。

抛错 ​

  • EngineDestroyedError

restoreSnapshot() ​

从快照恢复 GameState。

ts
restoreSnapshot(snapshot: GameSnapshot): void

行为 ​

  • 对快照做深拷贝以隔离调用方持有的对象引用
  • 清空 ActionQueue,避免已入队的链式 dispatch 被应用到恢复后的旧状态上
  • 调用 maybeValidate('restoreSnapshot')

抛错 ​

  • EngineDestroyedError
  • StateValidationError

destroy() ​

销毁引擎:清理 EventBus / Plugin / RuleEngine / 队列 / 历史 / 校验器,并设置 destroyed = true。

ts
destroy(): void

后续任何公共 API 调用都会抛出 EngineDestroyedError。

子系统协作 ​

GameEngine 与以下子系统协作:

子系统用途
PluginManager插件注册表,持有 setup / rules / actions / events
RuleEnginedispatch 时按注册顺序短路校验
ActionExecutortype → ActionHandler 派发,返回 { state, events }
EventBus同步派发事件,支持通配订阅
ActionHistory只追加日志,记录已成功执行的 Action
StateValidator8 项内置不变量 + addCheck 注册的游戏专属检查
RandomProviderMathRandomProvider / SeededRandomProvider
GameContextImplPlugin 与引擎之间的桥接

状态校验 ​

maybeValidate 在以下时机调用:

时机_phase 参数
createGame 后'createGame'
start 后'start'
dispatch 内每个 Action 处理后'dispatch'
restoreSnapshot 后'restoreSnapshot'

ValidationMode 取值:

模式行为
none从不校验
development与 always 相同(别名)
always在上述所有时机校验(默认)

校验失败抛 StateValidationError,附带所有违规条目(errors: string[])。

回放基础 ​

引擎的 ActionHistory 记录策略是回放可行的关键:

  • 只记录顶级 dispatch,不记录 event handler 通过 ctx.dispatch 入队的链式 action
  • setup 阶段(createGame / start)内的顶级 dispatch 也不被记入历史
  • 回放时,ReplayEngine 重新派发一个顶级 action,引擎自身的 event handler 会自动复现其链式后继

详见 ReplayEngine 与 ActionHistory。

错误类 ​

ActionQueueOverflowError ​

当一次 tick 内累计处理的 Action 数量超过 maxActionsPerTick 时抛出。

ts
class ActionQueueOverflowError extends Error {
  constructor(public readonly limit: number, public readonly remaining: number)
}
属性类型说明
limitnumber配置的 maxActionsPerTick
remainingnumber队列中剩余未处理的 Action 数

EngineDestroyedError ​

引擎已销毁后再调用任何公共 API 时抛出。

ts
class EngineDestroyedError extends Error {
  constructor()
}

StateValidationError ​

状态校验失败时抛出,附带所有违规条目。

ts
class StateValidationError extends Error {
  constructor(message: string, public readonly errors: string[])
}
属性类型说明
errorsstring[]所有违规描述

示例 ​

最小可运行示例:

ts
import {
  GameEngine, StandardDeckPlugin, TurnPlugin, DrawPlugin, SimpleGamePlugin,
  createSimplePlayCardAction, playerHandZoneId
} from 'decklet'

const engine = new GameEngine({ seed: 123456 })

engine.subscribeAll((e) => console.log(`[event] ${e.type}`))

engine
  .use(StandardDeckPlugin)
  .use(TurnPlugin)
  .use(DrawPlugin)
  .use(SimpleGamePlugin)
  .createGame({
    players: [
      { id: 'p1', name: 'Alice', seat: 1 },
      { id: 'p2', name: 'Bob', seat: 2 }
    ]
  })
  .start()

while (engine.getState().status === 'playing') {
  const id = engine.getState().currentPlayerId!
  const hand = engine.getState().zones[playerHandZoneId(id)]!
  if (hand.cards.length === 0) break
  engine.dispatch(createSimplePlayCardAction(id, [hand.cards[0]]))
}

console.log(`winners: ${engine.getState().winnerIds.join(', ')}`)
engine.destroy()

注意事项 ​

  • 引擎实例只能 createGame 一次;要开新局请新建 GameEngine 实例
  • 顶级 dispatch 同步处理整个 tick;链式 dispatch 仅入队,返回占位结果
  • getState() 返回的引用虽可访问子字段,但外部不应直接修改 —— 应通过 stateUtils.* 或 GameContext
  • destroy() 后所有公共 API 调用都会抛 EngineDestroyedError

关联类型 ​

ts
interface CreateGamePlayerInit {
  id: string
  name: string
  seat?: number
  status?: PlayerStatus
  data?: Record<string, unknown>
}

interface CreateGameOptions {
  players: CreateGamePlayerInit[]
  gameId?: string
  initialVariables?: Record<string, unknown>
  seed?: number
}

interface ProcessedAction {
  action: Action
  allowed: boolean
  denyReason?: string
  denyCode?: string
  error?: string
  events: GameEvent[]
}

interface DispatchResult {
  ok: boolean
  action: Action
  denyReason?: string
  denyCode?: string
  error?: string
  events: GameEvent[]
  processedActions: ProcessedAction[]
}

type ValidationMode = 'none' | 'development' | 'always'