ActionExecutor
ActionExecutor 将 Action 派发到与其 type 对应的已注册 ActionHandler。每个 handler 是 (action, ctx) → { state, events } 的纯函数,不会自行 bump state.version。GameEngine 负责每个 action 一次性的 version 递增,并为事件打上元信息。
概述
ActionExecutor 维护一个 type → ActionHandler 的 Map:
register(handler)注册 handler,重复 type 抛DuplicateActionHandlerErrorexecute(state, action, random)按 type 查找 handler 并委托执行- 不 bump
state.version,也不补全事件元信息 —— 这些由上层GameEngine统一处理
ActionExecutor 是引擎的内部组件,由 GameEngine.wirePlugins 在 createGame 时把插件的 actions 注册进去。
构造函数
new ActionExecutor()无参数构造。handlers 内部使用 Map<string, ActionHandler> 存储。
属性
无 public 属性。handlers 为私有字段。
方法
register()
注册 ActionHandler。
register(handler: ActionHandler): this参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
handler | ActionHandler | 是 | 待注册的 ActionHandler,type 必须唯一 |
返回值
this —— 便于链式调用。
行为
同一 type 不可重复注册,否则抛出 DuplicateActionHandlerError,防止 handler 被意外覆盖。
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。
示例
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。
unregister(type: string): boolean参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | 是 | Action type |
返回值
boolean —— 是否成功移除。
has()
是否已注册某 type 的 handler。
has(type: string): booleangetHandlerTypes()
返回当前已注册的所有 handler type 列表(拷贝)。
getHandlerTypes(): string[]execute()
执行给定 Action:按 type 查找 handler 并委托执行。
execute(state: GameState, action: Action, random: RandomProvider): ActionHandlerResult参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
state | GameState | 是 | 当前 GameState(仅读,handler 返回新 state) |
action | Action | 是 | 待执行的 Action |
random | RandomProvider | 是 | 引擎统一的随机源,透传给 handler |
返回值
ActionHandlerResult:
interface ActionHandlerResult {
state: GameState
events: EventInit[]
}| 字段 | 类型 | 说明 |
|---|---|---|
state | GameState | handler 产出的新状态(不可变,由 GameEngine 统一 bump version) |
events | EventInit[] | 待广播的事件列表(GameEngine 会补全 id / timestamp / stateVersion) |
行为
按 action.type 查找已注册的 handler,把 action 与 { state, random } 上下文传给 handler 的 execute 方法。
注意:本方法不会 bump state.version,也不会补全事件元信息,这些由上层 GameEngine 统一处理,以保持 executor 自身的纯净。
抛错
当未注册对应 type 的 handler 时抛 UnknownActionError。
示例
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 } 的纯函数式处理器。
export interface ActionHandler<TAction extends Action = Action> {
type: string
execute(action: TAction, ctx: ActionContext): ActionHandlerResult
}| 字段/方法 | 类型 | 说明 |
|---|---|---|
type | string | 该 handler 处理的 Action type,需与 Action.type 一致 |
execute | (action, ctx) => ActionHandlerResult | 处理 Action 的纯函数 |
ActionContext 接口
传递给 ActionHandler.execute 的上下文,与 (action, state) 一并提供。
export interface ActionContext {
state: GameState
random: RandomProvider
}| 字段 | 类型 | 说明 |
|---|---|---|
state | GameState | 当前不可变的 GameState(与引擎持有的实例相同) |
random | RandomProvider | 引擎的 RandomProvider —— handler 必须将所有随机决策通过它进行(禁止使用 Math.random) |
约束
- 必须是纯函数(相同输入产出相同输出),不得直接修改入参 state,应通过
stateUtils做不可变更新后返回新 state - 不要自行修改
state.version或事件元信息,由 GameEngine 统一处理 - 所有随机性必须经
ctx.random,禁止使用Math.random以保证可回放
EventInit 接口
由 ActionHandler 产出的部分事件。GameEngine 在向 EventBus 派发前会补全 id、timestamp 与 stateVersion 字段,从而让 handler 无需关心元信息维护。
export interface EventInit<TType extends string = string, TPayload = unknown> {
type: TType
payload: TPayload
playerId?: string
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | TType | 是 | 事件类型 |
payload | TPayload | 是 | 事件负载 |
playerId | string | 否 | 触发该事件的玩家 id |
ActionHandlerResult 接口
export interface ActionHandlerResult {
state: GameState
events: EventInit[]
}| 字段 | 类型 | 说明 |
|---|---|---|
state | GameState | handler 产出的新状态(不可变,由 GameEngine 统一 bump version) |
events | EventInit[] | 待广播的事件列表(GameEngine 会补全 id / timestamp / stateVersion) |
错误类
UnknownActionError
未找到匹配 type 的 handler 时抛出。
class UnknownActionError extends Error {
constructor(public readonly actionType: string)
}| 属性 | 类型 | 说明 |
|---|---|---|
actionType | string | 未注册 handler 的 Action type |
错误信息:No handler registered for action type: ${actionType}
DuplicateActionHandlerError
同一 type 被重复注册时抛出,避免 handler 被静默覆盖。
class DuplicateActionHandlerError extends Error {
constructor(public readonly actionType: string)
}| 属性 | 类型 | 说明 |
|---|---|---|
actionType | string | 重复注册的 Action type |
错误信息:Action handler for type '${actionType}' is already registered
与 GameEngine 的协作
GameEngine 在 createGame 时通过 wirePlugins 把插件的 actions 注册到 ActionExecutor:
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:
const result = this.actionExecutor.execute(this.state, action, this.randomProvider)
nextState = result.state
handlerEvents = result.events
// ... 引擎统一 bump version + emit events ...示例
定义并注册 ActionHandler
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
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不会 bumpstate.version,也不会补全事件元信息- handler 必须是纯函数:相同输入产出相同输出
- handler 不得直接修改入参 state,应通过
stateUtils做不可变更新 - 所有随机性必须经
ctx.random,禁止使用Math.random以保证可回放 - 同一 type 不可重复注册(避免 handler 被静默覆盖)
- 未注册对应 type 的 handler 时抛
UnknownActionError—— 这是 dispatch 失败的常见原因