Skip to content

RuleEngine ​

RuleEngine 按注册顺序依次执行所有已注册 Rule,遇到首个 deny 即短路。校验仅读取 context.state 与 context.action(经 context.game),Rule 禁止修改 state。

概述 ​

RuleEngine 是 GameEngine 的内部组件:

  • 维护一个 Rule[] 数组与 id 集合
  • register(rule) 按 id 去重注册,重复抛 DuplicateRuleError
  • validate(context) 按注册顺序校验,遇到首个 deny 立即短路返回
  • 不修改 state;仅作为只读校验入口

由 GameEngine.wirePlugins 在 createGame 时把插件的 rules 注册到 RuleEngine。

构造函数 ​

ts
new RuleEngine()

无参数构造。内部维护 rules: Rule[] 与 ids: Set<string>。

属性 ​

无 public 属性。rules 与 ids 均为私有字段。

方法 ​

register() ​

注册一条 Rule。

ts
register(rule: Rule): this

参数 ​

参数类型必填说明
ruleRule是待注册的 Rule,id 必须唯一

返回值 ​

this —— 便于链式调用。

行为 ​

  • 同一 RuleEngine 内 id 必须唯一
  • 重复注册抛 DuplicateRuleError 以暴露插件冲突
  • 注册顺序即校验顺序,先注册的先执行
ts
register(rule: Rule): this {
  if (this.ids.has(rule.id)) {
    throw new DuplicateRuleError(rule.id)
  }
  this.ids.add(rule.id)
  this.rules.push(rule)
  return this
}

抛错 ​

当 rule.id 已注册时抛 DuplicateRuleError。

示例 ​

ts
import { RuleEngine, ok, deny, type Rule } from 'decklet'

const engine = new RuleEngine()

const rule1: Rule = {
  id: 'game-started',
  name: 'Game Started',
  validate(c) {
    return c.state.status === 'playing' ? ok() : deny('not started', 'NOT_STARTED')
  }
}

const rule2: Rule = {
  id: 'player-turn',
  name: 'Player Turn',
  appliesTo: ['PLAY_CARD'],
  validate(c) {
    return c.state.currentPlayerId === c.action.playerId
      ? ok()
      : deny('not your turn', 'NOT_YOUR_TURN')
  }
}

engine.register(rule1).register(rule2)

unregister() ​

按 id 注销 Rule。

ts
unregister(id: string): boolean

参数 ​

参数类型必填说明
idstring是Rule id

返回值 ​

boolean —— 不存在时返回 false,存在并删除成功返回 true。

clear() ​

移除所有已注册 Rule。

ts
clear(): void

用于引擎重置或销毁场景,避免插件残留。GameEngine.destroy() 会调用此方法。

getRules() ​

返回所有已注册 Rule 的浅拷贝,顺序为注册顺序(即校验顺序)。

ts
getRules(): Rule[]

has() ​

是否已注册指定 id 的 Rule。

ts
has(id: string): boolean

validate() ​

校验给定 context 下的 Action 是否被所有适用 Rule 允许。

ts
validate(context: RuleContext): RuleResult

参数 ​

参数类型必填说明
context.stateGameState是当前 state(只读)
context.actionAction是待校验的 Action
context.gameGameContext是只读 GameContext

返回值 ​

RuleResult —— { allowed: true } 或 { allowed: false, reason, code? }。

行为 ​

  • 按注册顺序遍历 Rule
  • 跳过 appliesTo 不匹配当前 actionType 的 Rule(通过 ruleApplies(rule, context.action.type))
  • 对匹配的 Rule 调用其 validate
  • 遇到首个 deny 立即短路返回该结果
  • 所有适用 Rule 都允许才返回 ok()
  • 不修改 state;仅作为只读校验入口
ts
validate(context: RuleContext): RuleResult {
  for (const rule of this.rules) {
    if (!ruleApplies(rule, context.action.type)) continue
    const result = rule.validate(context)
    if (!result.allowed) {
      return result
    }
  }
  return { allowed: true }
}

示例 ​

ts
import { RuleEngine, ok, deny, type RuleContext } from 'decklet'

const engine = new RuleEngine()
engine.register({
  id: 'game-started',
  name: 'Game Started',
  validate(c) {
    return c.state.status === 'playing' ? ok() : deny('not started', 'NOT_STARTED')
  }
})

const result = engine.validate({
  state,
  action: { id: 'a1', type: 'PLAY_CARD', payload: {}, timestamp: 0 },
  game: context
})
console.log(result)
// { allowed: false, reason: 'not started', code: 'NOT_STARTED' }  (假设 status='waiting')

短路校验机制 ​

RuleEngine 按注册顺序依次执行 Rule,遇到首个 deny 即短路终止:

Rule[0].validate(context)
  ├─ ok()   → 继续
  └─ deny() → 短路返回(不调用后续 Rule)
Rule[1].validate(context)
  ├─ appliesTo 不匹配 → 跳过
  ├─ ok()   → 继续
  └─ deny() → 短路返回
Rule[2].validate(context)
  └─ ...

全部通过 → 返回 ok()

要点:

  • appliesTo 不匹配的 Rule 被跳过(不调用 validate)
  • 首个 deny 立即返回,不调用后续 Rule
  • 所有适用 Rule 都允许才返回 ok()

错误类 ​

DuplicateRuleError ​

试图注册 id 已存在的 Rule 时抛出,避免不同插件重复定义同一规则造成覆盖。

ts
class DuplicateRuleError extends Error {
  constructor(public readonly ruleId: string)
}
属性类型说明
ruleIdstring重复注册的 Rule id

错误信息:Rule with id '${ruleId}' is already registered

与 GameEngine 的协作 ​

GameEngine 在 createGame 时通过 wirePlugins 把插件的 rules 注册到 RuleEngine:

ts
private wirePlugins(): void {
  for (const p of this.plugins.getAll()) {
    for (const r of p.rules ?? []) {
      try {
        this.ruleEngine.register(r)
      } catch (e) {
        if (e instanceof DuplicateRuleError) {
          throw new Error(`Plugin '${p.id}' failed to register rule '${r.id}': ${e.message}`)
        }
        throw e
      }
    }
    // ... actions / events ...
  }
}

要点:

  • 重复 rule id 会以"插件 + rule id"信息重新抛出,便于定位冲突来源
  • GameEngine.processAction 在 dispatch 时调用 ruleEngine.validate(context)
  • 拒绝时直接返回 ProcessedAction,state 不变
ts
const ruleResult = this.ruleEngine.validate({
  state: this.state,
  action,
  game: this.context
})
if (!ruleResult.allowed) {
  return {
    action,
    allowed: false,
    denyReason: ruleResult.reason,
    denyCode: ruleResult.code,
    events: []
  }
}

GameEngine.destroy() 会调用 ruleEngine.clear()。

示例 ​

完整 Plugin 示例 ​

ts
import { type GamePlugin, ok, deny, type Rule } from 'decklet'

const gameStartedRule: Rule = {
  id: 'simple-game-started',
  name: 'Game Started',
  appliesTo: ['PLAY_CARD', 'DRAW_CARD', 'END_TURN', 'PASS'],
  validate(c) {
    return c.state.status === 'playing'
      ? ok()
      : deny('game has not started', 'NOT_STARTED')
  }
}

const playerTurnRule: Rule = {
  id: 'simple-player-turn',
  name: 'Player Turn',
  appliesTo: ['PLAY_CARD'],
  validate(c) {
    if (!c.action.playerId) return deny('action has no playerId', 'NO_PLAYER')
    if (c.state.currentPlayerId !== c.action.playerId) {
      return deny(`not ${c.action.playerId}'s turn`, 'NOT_YOUR_TURN')
    }
    return ok()
  }
}

export const MyPlugin: GamePlugin = {
  id: 'my-plugin',
  name: 'My Plugin',
  version: '1.0.0',
  rules: [gameStartedRule, playerTurnRule]
  // ... actions / events ...
}

校验流程示例 ​

假设有 3 条 Rule 按注册顺序:

  1. gameStartedRule (appliesTo: ['PLAY_CARD'])
  2. gameNotWonRule (appliesTo: '*')
  3. playerTurnRule (appliesTo: ['PLAY_CARD'])

派发 PLAY_CARD action 时:

gameStartedRule (PLAY_CARD 匹配)  → ok()  继续
gameNotWonRule  ('*' 匹配)        → ok()  继续
playerTurnRule  (PLAY_CARD 匹配) → deny('not your turn', 'NOT_YOUR_TURN')
                                          ↓
                                       短路返回

派发 END_TURN action 时:

gameStartedRule (PLAY_CARD 不匹配) → 跳过
gameNotWonRule  ('*' 匹配)         → ok()  继续
playerTurnRule  (PLAY_CARD 不匹配) → 跳过
                                            ↓
                                       返回 ok()

注意事项 ​

  • RuleEngine 按注册顺序短路:注册顺序影响校验顺序
  • appliesTo 不匹配的 Rule 被跳过,不调用 validate
  • Rule 禁止修改 state;只读校验
  • 重复 id 抛 DuplicateRuleError,GameEngine.wirePlugins 会包裹为 Plugin '${id}' failed to register rule '${id}': ${msg}
  • clear() 用于引擎销毁;不应在校验过程中调用