Skip to content

ActionExecutor ​

ActionExecutor 将 Action 派发到与其 type 对应的已注册 ActionHandler。每个 handler 是 (action, ctx) → { state, events } 的纯函数,不会自行 bump state.version。GameEngine 负责每个 action 一次性的 version 递增,并为事件打上元信息。

概述 ​

ActionExecutor 维护一个 type → ActionHandler 的 Map:

  • register(handler) 注册 handler,重复 type 抛 DuplicateActionHandlerError
  • execute(state, action, random) 按 type 查找 handler 并委托执行
  • 不 bump state.version,也不补全事件元信息 —— 这些由上层 GameEngine 统一处理

ActionExecutor 是引擎的内部组件,由 GameEngine.wirePlugins 在 createGame 时把插件的 actions 注册进去。

构造函数 ​

ts
new ActionExecutor()

无参数构造。handlers 内部使用 Map<string, ActionHandler> 存储。

属性 ​

无 public 属性。handlers 为私有字段。

方法 ​

register() ​

注册 ActionHandler。

ts
register(handler: ActionHandler): this

参数 ​

参数类型必填说明
handlerActionHandler是待注册的 ActionHandler,type 必须唯一

返回值 ​

this —— 便于链式调用。

行为 ​

同一 type 不可重复注册,否则抛出 DuplicateActionHandlerError,防止 handler 被意外覆盖。

ts
register(handler: ActionHandler): this {
  if (this.handlers.has(handler.type)) {
    throw new DuplicateActionHandlerError(handler.type)
  }
  this.handlers.set(handler.type, handler)
  return this
}

抛错 ​

当 handler.type 已注册时抛 DuplicateActionHandlerError。

示例 ​

ts
const executor = new ActionExecutor()

executor
  .register({ type: 'PLAY_CARD', execute: (a, ctx) => ({ state: ctx.state, events: [] }) })
  .register({ type: 'DRAW_CARD', execute: (a, ctx) => ({ state: ctx.state, events: [] }) })

unregister() ​

注销指定 type 的 handler。

ts
unregister(type: string): boolean

参数 ​

参数类型必填说明
typestring是Action type

返回值 ​

boolean —— 是否成功移除。

has() ​

是否已注册某 type 的 handler。

ts
has(type: string): boolean

getHandlerTypes() ​

返回当前已注册的所有 handler type 列表(拷贝)。

ts
getHandlerTypes(): string[]

execute() ​

执行给定 Action:按 type 查找 handler 并委托执行。

ts
execute(state: GameState, action: Action, random: RandomProvider): ActionHandlerResult

参数 ​

参数类型必填说明
stateGameState是当前 GameState(仅读,handler 返回新 state)
actionAction是待执行的 Action
randomRandomProvider是引擎统一的随机源,透传给 handler

返回值 ​

ActionHandlerResult:

ts
interface ActionHandlerResult {
  state: GameState
  events: EventInit[]
}
字段类型说明
stateGameStatehandler 产出的新状态(不可变,由 GameEngine 统一 bump version)
eventsEventInit[]待广播的事件列表(GameEngine 会补全 id / timestamp / stateVersion)

行为 ​

按 action.type 查找已注册的 handler,把 action 与 { state, random } 上下文传给 handler 的 execute 方法。

注意:本方法不会 bump state.version,也不会补全事件元信息,这些由上层 GameEngine 统一处理,以保持 executor 自身的纯净。

抛错 ​

当未注册对应 type 的 handler 时抛 UnknownActionError。

示例 ​

ts
const executor = new ActionExecutor()
executor.register({
  type: 'PLAY_CARD',
  execute(action, ctx) {
    const next = moveCard(ctx.state, action.payload.cardId, 'hand', 'discard')
    return {
      state: next,
      events: [{ type: 'CARD_PLAYED', payload: action.payload }]
    }
  }
})

const result = executor.execute(state, playCardAction, randomProvider)
console.log(result.state)    // 新 state
console.log(result.events)   // EventInit[]

ActionHandler 接口 ​

ActionHandler:把特定 type 的 Action 翻译为 { state, events } 的纯函数式处理器。

ts
export interface ActionHandler<TAction extends Action = Action> {
  type: string
  execute(action: TAction, ctx: ActionContext): ActionHandlerResult
}
字段/方法类型说明
typestring该 handler 处理的 Action type,需与 Action.type 一致
execute(action, ctx) => ActionHandlerResult处理 Action 的纯函数

ActionContext 接口 ​

传递给 ActionHandler.execute 的上下文,与 (action, state) 一并提供。

ts
export interface ActionContext {
  state: GameState
  random: RandomProvider
}
字段类型说明
stateGameState当前不可变的 GameState(与引擎持有的实例相同)
randomRandomProvider引擎的 RandomProvider —— handler 必须将所有随机决策通过它进行(禁止使用 Math.random)

约束 ​

  • 必须是纯函数(相同输入产出相同输出),不得直接修改入参 state,应通过 stateUtils 做不可变更新后返回新 state
  • 不要自行修改 state.version 或事件元信息,由 GameEngine 统一处理
  • 所有随机性必须经 ctx.random,禁止使用 Math.random 以保证可回放

EventInit 接口 ​

由 ActionHandler 产出的部分事件。GameEngine 在向 EventBus 派发前会补全 id、timestamp 与 stateVersion 字段,从而让 handler 无需关心元信息维护。

ts
export interface EventInit<TType extends string = string, TPayload = unknown> {
  type: TType
  payload: TPayload
  playerId?: string
}
字段类型必填说明
typeTType是事件类型
payloadTPayload是事件负载
playerIdstring否触发该事件的玩家 id

ActionHandlerResult 接口 ​

ts
export interface ActionHandlerResult {
  state: GameState
  events: EventInit[]
}
字段类型说明
stateGameStatehandler 产出的新状态(不可变,由 GameEngine 统一 bump version)
eventsEventInit[]待广播的事件列表(GameEngine 会补全 id / timestamp / stateVersion)

错误类 ​

UnknownActionError ​

未找到匹配 type 的 handler 时抛出。

ts
class UnknownActionError extends Error {
  constructor(public readonly actionType: string)
}
属性类型说明
actionTypestring未注册 handler 的 Action type

错误信息:No handler registered for action type: ${actionType}

DuplicateActionHandlerError ​

同一 type 被重复注册时抛出,避免 handler 被静默覆盖。

ts
class DuplicateActionHandlerError extends Error {
  constructor(public readonly actionType: string)
}
属性类型说明
actionTypestring重复注册的 Action type

错误信息:Action handler for type '${actionType}' is already registered

与 GameEngine 的协作 ​

GameEngine 在 createGame 时通过 wirePlugins 把插件的 actions 注册到 ActionExecutor:

ts
private wirePlugins(): void {
  for (const p of this.plugins.getAll()) {
    for (const a of p.actions ?? []) {
      this.actionExecutor.register(a)
    }
    // ... rules / events ...
  }
}

GameEngine.processAction 在 Rule 校验通过后调用 ActionExecutor.execute:

ts
const result = this.actionExecutor.execute(this.state, action, this.randomProvider)
nextState = result.state
handlerEvents = result.events
// ... 引擎统一 bump version + emit events ...

示例 ​

定义并注册 ActionHandler ​

ts
import { ActionExecutor, type ActionHandler } from 'decklet'
import { moveCard } from 'decklet/core/stateUtils.js'

const executor = new ActionExecutor()

const playCardHandler: ActionHandler = {
  type: 'PLAY_CARD',
  execute(action, ctx) {
    const { cardId, fromZoneId, toZoneId } = action.payload as {
      cardId: string
      fromZoneId: string
      toZoneId: string
    }
    const next = moveCard(ctx.state, cardId, fromZoneId, toZoneId)
    return {
      state: next,
      events: [
        {
          type: 'CARD_PLAYED',
          payload: { cardId, fromZoneId, toZoneId },
          playerId: action.playerId
        }
      ]
    }
  }
}

executor.register(playCardHandler)
console.log(executor.has('PLAY_CARD'))  // true
console.log(executor.getHandlerTypes()) // ['PLAY_CARD']

在 Plugin 中提供 ActionHandler ​

ts
import {
  type GamePlugin, type ActionHandler,
  PLAY_CARD_ACTION, type PlayCardAction, type PlayCardPayload
} from 'decklet'
import { moveCard, addZone } from 'decklet/core/stateUtils.js'
import { createZone } from 'decklet'

const playCardHandler: ActionHandler<PlayCardAction> = {
  type: PLAY_CARD_ACTION,
  execute(action, ctx) {
    const payload = (action.payload ?? {}) as PlayCardPayload
    const cardIds = payload.cardIds
    const toZoneId = payload.toZoneId ?? 'discard'
    const playerId = action.playerId!
    const handZoneId = `player:${playerId}:hand`

    let next = ctx.state
    if (!next.zones[toZoneId]) {
      next = addZone(next, createZone({ id: toZoneId, type: 'discard' }))
    }
    for (const cardId of cardIds) {
      next = moveCard(next, cardId, handZoneId, toZoneId)
    }
    return {
      state: next,
      events: [{ type: 'CARD_PLAYED', payload: { playerId, cardIds: [...cardIds] }, playerId }]
    }
  }
}

export const MyPlugin: GamePlugin = {
  id: 'my-plugin',
  name: 'My Plugin',
  version: '1.0.0',
  actions: [playCardHandler]
}

注意事项 ​

  • ActionExecutor 不会 bump state.version,也不会补全事件元信息
  • handler 必须是纯函数:相同输入产出相同输出
  • handler 不得直接修改入参 state,应通过 stateUtils 做不可变更新
  • 所有随机性必须经 ctx.random,禁止使用 Math.random 以保证可回放
  • 同一 type 不可重复注册(避免 handler 被静默覆盖)
  • 未注册对应 type 的 handler 时抛 UnknownActionError —— 这是 dispatch 失败的常见原因