RuleEngine
RuleEngine 按注册顺序依次执行所有已注册 Rule,遇到首个 deny 即短路。校验仅读取 context.state 与 context.action(经 context.game),Rule 禁止修改 state。
概述
RuleEngine 是 GameEngine 的内部组件:
- 维护一个
Rule[]数组与id集合 register(rule)按 id 去重注册,重复抛DuplicateRuleErrorvalidate(context)按注册顺序校验,遇到首个 deny 立即短路返回- 不修改 state;仅作为只读校验入口
由 GameEngine.wirePlugins 在 createGame 时把插件的 rules 注册到 RuleEngine。
构造函数
new RuleEngine()无参数构造。内部维护 rules: Rule[] 与 ids: Set<string>。
属性
无 public 属性。rules 与 ids 均为私有字段。
方法
register()
注册一条 Rule。
register(rule: Rule): this参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
rule | Rule | 是 | 待注册的 Rule,id 必须唯一 |
返回值
this —— 便于链式调用。
行为
- 同一 RuleEngine 内
id必须唯一 - 重复注册抛
DuplicateRuleError以暴露插件冲突 - 注册顺序即校验顺序,先注册的先执行
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。
示例
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。
unregister(id: string): boolean参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | Rule id |
返回值
boolean —— 不存在时返回 false,存在并删除成功返回 true。
clear()
移除所有已注册 Rule。
clear(): void用于引擎重置或销毁场景,避免插件残留。GameEngine.destroy() 会调用此方法。
getRules()
返回所有已注册 Rule 的浅拷贝,顺序为注册顺序(即校验顺序)。
getRules(): Rule[]has()
是否已注册指定 id 的 Rule。
has(id: string): booleanvalidate()
校验给定 context 下的 Action 是否被所有适用 Rule 允许。
validate(context: RuleContext): RuleResult参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
context.state | GameState | 是 | 当前 state(只读) |
context.action | Action | 是 | 待校验的 Action |
context.game | GameContext | 是 | 只读 GameContext |
返回值
RuleResult —— { allowed: true } 或 { allowed: false, reason, code? }。
行为
- 按注册顺序遍历 Rule
- 跳过
appliesTo不匹配当前 actionType 的 Rule(通过ruleApplies(rule, context.action.type)) - 对匹配的 Rule 调用其
validate - 遇到首个
deny立即短路返回该结果 - 所有适用 Rule 都允许才返回
ok() - 不修改 state;仅作为只读校验入口
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 }
}示例
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 时抛出,避免不同插件重复定义同一规则造成覆盖。
class DuplicateRuleError extends Error {
constructor(public readonly ruleId: string)
}| 属性 | 类型 | 说明 |
|---|---|---|
ruleId | string | 重复注册的 Rule id |
错误信息:Rule with id '${ruleId}' is already registered
与 GameEngine 的协作
GameEngine 在 createGame 时通过 wirePlugins 把插件的 rules 注册到 RuleEngine:
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 不变
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 示例
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 按注册顺序:
gameStartedRule(appliesTo: ['PLAY_CARD'])gameNotWonRule(appliesTo: '*')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()用于引擎销毁;不应在校验过程中调用