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 约束以打断意外的无限循环。
构造函数
new GameEngine(config?: GameConfigInit & { validation?: ValidationMode })| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
config.gameId | string | 否 | 'game-default' | 对局 id |
config.maxActionsPerTick | number | 否 | 1000 | 单 tick 内最大 Action 数 |
config.defaultDirection | 1 | -1 | 否 | 1 | 默认回合方向 |
config.seed | number | 否 | 自动生成 | 随机种子 |
config.validation | ValidationMode | 否 | 'always' | 状态校验模式 |
构造时:
resolveConfig(config)解析配置- 解析种子:
config.seed ?? generateRandomSeed() - 把实际种子回写到
config(外部可通过getConfig()读取) - 依据是否请求了种子选择 provider:
SeededRandomProvider或MathRandomProvider createGameState({ gameId, seed })初始化空 statenew GameContextImpl(this)创建桥接实例
属性
GameEngine 公开访问的属性:
| 属性 | 类型 | 说明 |
|---|---|---|
state | GameState | 当前对局状态(不可变引用,外部不应直接修改) |
通过 EngineInternal 接口暴露的内部 API(由 GameContextImpl 使用):
| 属性 | 类型 | 说明 |
|---|---|---|
random | RandomProvider | 引擎的随机源 |
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 之前调用。
use(plugin: GamePlugin): this返回值
this —— 支持链式调用。
行为
- 引擎已销毁时抛
EngineDestroyedError - 委托
PluginManager.use注册(重复 id 抛DuplicatePluginError)
示例
engine
.use(StandardDeckPlugin)
.use(TurnPlugin)
.use(DrawPlugin)
.use(SimpleGamePlugin)createGame()
创建并初始化一局游戏。
createGame(options: CreateGameOptions): this参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
options.players | CreateGamePlayerInit[] | 是 | 玩家列表 |
options.gameId | string | 否 | 对局 id 覆盖 |
options.initialVariables | Record<string, unknown> | 否 | 初始 variables |
options.seed | number | 否 | 单局种子覆盖 |
CreateGamePlayerInit 字段:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
id | string | 是 | — | 玩家 id |
name | string | 是 | — | 显示名 |
seat | number | 否 | 下标 +1 | 座位号;同局不可重复 |
status | PlayerStatus | 否 | 'active' | 玩家状态 |
data | Record<string, unknown> | 否 | {} | 游戏专属数据 |
返回值
this
行为
- 校验前置条件(未销毁 / 对局未创建 / 玩家非空 / 座位不重复)
- 解析玩家:对每个 init 调用
createPlayer,按seat ?? index + 1填充座位 - 写入初始 GameState
wirePlugins()把插件的 rules / actions / events 注册到子系统- 进入 setup 阶段(
inSetupPhase = true) - 调用每个插件的
setup(context) - 标记
gameCreated = true,发出GAME_CREATED事件 pumpIfIdle处理 setup 阶段链式 dispatch- 退出 setup 阶段
maybeValidate('createGame')状态校验
抛错
EngineDestroyedError— 引擎已销毁Error: Game already created...— 对局已创建Error: Cannot create a game with no playersError: Duplicate seat ${seat} for player ${id}DuplicateRuleError(被包裹为Plugin '${id}' failed to register rule '${id}'...)DuplicateActionHandlerErrorStateValidationError— 状态校验失败
start()
把对局状态从 waiting 切换到 playing,并发出 GAME_STARTED 事件。
start(): this返回值
this
行为
- 校验前置条件(未销毁 / 对局已创建 / 当前状态必须是
waiting) setState(prev => bumpVersion(setStatusUtil(prev, 'playing')))- 进入 setup 阶段 →
startTick→ emitGAME_STARTED→pumpIfIdle→finishTick - 退出 setup 阶段
maybeValidate('start')
GAME_STARTED 处理器中触发的链式 dispatch 不会被记入 ActionHistory,回放时通过重新执行 start 复现。
抛错
EngineDestroyedErrorError: Game has not been created yet; call createGame() firstError: Cannot start game in status: ${status}StateValidationError
dispatch()
派发一个 Action。
dispatch(action: Action): DispatchResult参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | Action | 是 | 待派发的 Action |
返回值
DispatchResult:
interface DispatchResult {
ok: boolean
action: Action
denyReason?: string
denyCode?: string
error?: string
events: GameEvent[]
processedActions: ProcessedAction[]
}行为
- 顶级调用(非 EventHandler 链式触发):同步处理整个 tick 并返回完整结果
- 链式调用(EventHandler 内调用):仅入队并返回占位结果,实际处理由外层 tick 完成
顶级 dispatch 处理流程:
startTick()重置 tick 缓冲- 标记 action 为顶级(除非处于 setup 阶段)
- 入队
queue.push(action) pump()主循环消费队列finishTick()buildDispatchResult(action, wasTopLevel)构造结果
pump 内的 processAction 步骤:
- RuleEngine 校验 → 拒绝时返回
{ allowed: false, denyReason, denyCode, events: [] } - ActionExecutor.execute →
{ state, events };执行抛错时返回{ allowed: true, error, events: [] } this.state = { ...nextState, version: this.state.version + 1 }—— 一次性 bump- 把每个
EventInit通过eventFromInit补全为GameEvent并eventBus.emit - 仅顶级 dispatch 记入 ActionHistory
maybeValidate('dispatch')
抛错
EngineDestroyedErrorActionQueueOverflowError— 单 tick 内 Action 数超过maxActionsPerTickStateValidationError
getState()
返回当前 GameState(不可变引用,外部不应直接修改)。
getState(): GameStategetPlugins()
返回所有已注册插件的浅拷贝列表(保持插入顺序)。
getPlugins(): GamePlugin[]getConfig()
返回配置的浅拷贝,防止外部修改引擎内部配置。
getConfig(): GameConfiggetSeed()
返回引擎使用的种子(未设置时返回 0)。
getSeed(): numbergetRandom()
返回引擎的 RandomProvider。
getRandom(): RandomProvidergetActionHistory()
返回 ActionHistory 实例。
getActionHistory(): ActionHistorygetStateValidator()
返回 StateValidator 实例。
getStateValidator(): StateValidatorsubscribe()
订阅指定类型的事件,支持 WILDCARD 监听全部类型。
subscribe(type: string | typeof WILDCARD, listener: EventListener): () => void参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | typeof WILDCARD | 是 | 事件类型或 WILDCARD |
listener | EventListener | 是 | 监听器函数 |
返回值
() => void —— 取消订阅函数。
抛错
EngineDestroyedError
subscribeAll()
订阅所有事件(等价于 subscribe(WILDCARD, listener))。
subscribeAll(listener: EventListener): () => voidcreateSnapshot()
创建当前 GameState 的快照,用于持久化或回滚。
createSnapshot(): GameSnapshot返回值
GameSnapshot —— 包含 gameId / version / state(深拷贝)/ createdAt。
抛错
EngineDestroyedError
restoreSnapshot()
从快照恢复 GameState。
restoreSnapshot(snapshot: GameSnapshot): void行为
- 对快照做深拷贝以隔离调用方持有的对象引用
- 清空 ActionQueue,避免已入队的链式 dispatch 被应用到恢复后的旧状态上
- 调用
maybeValidate('restoreSnapshot')
抛错
EngineDestroyedErrorStateValidationError
destroy()
销毁引擎:清理 EventBus / Plugin / RuleEngine / 队列 / 历史 / 校验器,并设置 destroyed = true。
destroy(): void后续任何公共 API 调用都会抛出 EngineDestroyedError。
子系统协作
GameEngine 与以下子系统协作:
| 子系统 | 用途 |
|---|---|
PluginManager | 插件注册表,持有 setup / rules / actions / events |
RuleEngine | dispatch 时按注册顺序短路校验 |
ActionExecutor | type → ActionHandler 派发,返回 { state, events } |
EventBus | 同步派发事件,支持通配订阅 |
ActionHistory | 只追加日志,记录已成功执行的 Action |
StateValidator | 8 项内置不变量 + addCheck 注册的游戏专属检查 |
RandomProvider | MathRandomProvider / SeededRandomProvider |
GameContextImpl | Plugin 与引擎之间的桥接 |
状态校验
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 时抛出。
class ActionQueueOverflowError extends Error {
constructor(public readonly limit: number, public readonly remaining: number)
}| 属性 | 类型 | 说明 |
|---|---|---|
limit | number | 配置的 maxActionsPerTick |
remaining | number | 队列中剩余未处理的 Action 数 |
EngineDestroyedError
引擎已销毁后再调用任何公共 API 时抛出。
class EngineDestroyedError extends Error {
constructor()
}StateValidationError
状态校验失败时抛出,附带所有违规条目。
class StateValidationError extends Error {
constructor(message: string, public readonly errors: string[])
}| 属性 | 类型 | 说明 |
|---|---|---|
errors | string[] | 所有违规描述 |
示例
最小可运行示例:
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.*或GameContextdestroy()后所有公共 API 调用都会抛EngineDestroyedError
关联类型
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'