Skip to content

Rules ​

UNO 插件注册到 RuleEngine 的校验规则集合。

概述 ​

UNO 的 4 个规则文件共导出 8 个 Rule 实例,由 UnoPlugin.rules 按顺序注册。RuleEngine 在每次 Action 执行前按顺序短路执行所有 appliesTo 匹配的 Rule——任一返回 deny 即拒绝该 Action。

RuleidappliesTo
gameNotWonRuleuno-game-not-won'*'
gameStartedRuleuno-game-started'*'
playerTurnRuleuno-player-turnPLAY_CARD_ACTION, CHOOSE_COLOR_ACTION
playerOwnsCardRuleuno-player-owns-cardPLAY_CARD_ACTION
singleCardRuleuno-single-cardPLAY_CARD_ACTION
canPlayCardRuleuno-can-play-cardPLAY_CARD_ACTION
canPlayWildDrawFourRuleuno-wild-draw4-legalPLAY_CARD_ACTION
chooseColorValidRuleuno-choose-color-validCHOOSE_COLOR_ACTION

通用规则原语:ok() 表示通过,deny(message, code) 表示拒绝并附带错误码。

WinRule ​

gameStartedRule ​

ts
const gameStartedRule: Rule
字段值
id'uno-game-started'
appliesTo'*'

行为 ​

所有 Action 仅在对局状态为 playing 时才允许。防止在 waiting / finished 等阶段执行出牌、选色等操作。

  • state.status === 'playing' → ok()
  • 否则 → deny('game has not started', 'NOT_STARTED')

gameNotWonRule ​

ts
const gameNotWonRule: Rule
字段值
id'uno-game-not-won'
appliesTo'*'

行为 ​

一旦已有获胜者,拒绝所有后续 Action。用于在有人胜出后锁定对局状态,防止继续出牌或派生动作。

  • state.winnerIds.length === 0 → ok()
  • 否则 → deny('game already won', 'ALREADY_WON')

TurnRule ​

playerTurnRule ​

ts
const playerTurnRule: Rule
字段值
id'uno-player-turn'
appliesTo[PLAY_CARD_ACTION, CHOOSE_COLOR_ACTION]

行为 ​

回合归属规则:PLAY_CARD 与 CHOOSE_COLOR 必须由当前轮到行动的玩家发起。通过比对 state.currentPlayerId 与 action.playerId 判定。

  • action.playerId 缺省 → deny('action has no playerId', 'NO_PLAYER')
  • state.currentPlayerId !== action.playerId → deny('not ${playerId}\'s turn', 'NOT_YOUR_TURN')
  • 否则 → ok()

playerOwnsCardRule ​

ts
const playerOwnsCardRule: Rule
字段值
id'uno-player-owns-card'
appliesTo[PLAY_CARD_ACTION]

行为 ​

卡牌归属规则:PLAY_CARD 中的所有卡牌必须确实位于发起玩家的手牌 zone 中(playerHandZoneId(playerId))。

  • cardIds.length === 0 → deny('no cards to play', 'NO_CARDS')
  • action.playerId 缺省 → deny('no player', 'NO_PLAYER')
  • 玩家无手牌 zone → deny('player has no hand', 'NO_HAND')
  • 任一 cardId 不在手牌 zone 中 → deny('player does not own card ${cardId}', 'NOT_OWNED')
  • 否则 → ok()

singleCardRule ​

ts
const singleCardRule: Rule
字段值
id'uno-single-card'
appliesTo[PLAY_CARD_ACTION]

行为 ​

单张牌规则:UNO 一次出牌必须且只能打出一张牌(UNO 不支持组合出牌)。

  • cardIds.length !== 1 → deny('UNO play must be exactly one card', 'NOT_SINGLE')
  • 否则 → ok()

CanPlayCardRule ​

canPlayCardRule ​

ts
const canPlayCardRule: Rule
字段值
id'uno-can-play-card'
appliesTo[PLAY_CARD_ACTION]

行为 ​

UNO 出牌合法性规则:打出的牌必须与当前状态匹配。判定顺序(任一满足即通过):

  1. Wild 类牌(isWildCard(card) 为 true,即 wild / wild_draw4)——可随时打出,不受颜色 / 牌面约束(Wild Draw Four 的额外限制由 canPlayWildDrawFourRule 单独校验)。
  2. 与当前生效颜色同色:card.data.color === state.variables[UNO_VAR_CURRENT_COLOR]。
  3. 与弃牌堆顶张牌面相同:unoCardFace(card) === unoCardFace(topCard)(数字牌按点数 number:N 匹配、功能牌按类型 skip / reverse / draw2 匹配)。

其余情况一律拒绝。

  • cardId 缺省 → ok()(由其它规则处理)
  • 卡牌未找到 → deny('card ${cardId} not found', 'CARD_NOT_FOUND')
  • Wild 类 → ok()
  • 与当前色同色 → ok()
  • 与顶张牌面相同 → ok()
  • 否则 → deny('card does not match current color (${currentColor}) or top card face', 'CANNOT_PLAY')

DrawFourRule ​

canPlayWildDrawFourRule ​

ts
const canPlayWildDrawFourRule: Rule
字段值
id'uno-wild-draw4-legal'
appliesTo[PLAY_CARD_ACTION]

行为 ​

Wild Draw Four 的额外合法性规则:仅在玩家手中无与当前颜色同色的牌时才允许打出。这是 UNO 官方规则对 Wild Draw Four 的限制——若玩家手中已有当前色牌,则不允许用 Wild Draw Four "逃牌"。

判定方式:遍历玩家手牌 zone,一旦发现任意一张与当前生效颜色同色的牌即拒绝(错误码 WD4_ILLEGAL)。

  • cardId 缺省 → ok()
  • 卡牌非 wild_draw4 → ok()(仅针对 wild_draw4)
  • currentColor 为 undefined 或 'wild'(开局未定色) → ok()(不做限制)
  • action.playerId 缺省 → deny('no player', 'NO_PLAYER')
  • 玩家无手牌 zone → deny('no hand', 'NO_HAND')
  • 手中存在与 currentColor 同色的牌 → deny('player has a card of current color; cannot play Wild Draw Four', 'WD4_ILLEGAL')
  • 否则 → ok()

chooseColorValidRule ​

ts
const chooseColorValidRule: Rule
字段值
id'uno-choose-color-valid'
appliesTo[CHOOSE_COLOR_ACTION]

行为 ​

CHOOSE_COLOR Action 的合法性规则:颜色值有效且当前确处于等待选色状态。两个条件必须同时满足:

  1. payload.color 是合法的 UnoColorChoice(通过 isUnoColorChoice 守卫,拒绝 'wild' 与非法字符串)。
  2. UNO_VAR_PENDING_COLOR === true,即确实有一张 Wild 牌刚打出、正等待玩家指定颜色。
  • isUnoColorChoice(payload.color) 为 false → deny('invalid color', 'INVALID_COLOR')
  • pending !== true → deny('not awaiting color choice', 'NOT_PENDING')
  • 否则 → ok()

Rule 注册顺序与短路 ​

UnoPlugin.rules 数组顺序如下,RuleEngine 按此顺序短路执行:

1. gameNotWonRule         (任意 Action)
2. gameStartedRule        (任意 Action)
3. playerTurnRule         (PLAY_CARD / CHOOSE_COLOR)
4. playerOwnsCardRule     (PLAY_CARD)
5. singleCardRule         (PLAY_CARD)
6. canPlayCardRule        (PLAY_CARD)
7. canPlayWildDrawFourRule(PLAY_CARD)
8. chooseColorValidRule  (CHOOSE_COLOR)

基础校验(游戏状态 / 回合归属)必须先于业务规则(出牌合法性 / Wild Draw Four 限制),这一顺序由 createDefaultPluginRegistry() 保证(TurnPlugin → DrawPlugin → UnoPlugin)。

错误码汇总 ​

错误码触发 Rule
NOT_STARTEDgameStartedRule
ALREADY_WONgameNotWonRule
NO_PLAYERplayerTurnRule / playerOwnsCardRule / canPlayWildDrawFourRule
NOT_YOUR_TURNplayerTurnRule
NO_CARDSplayerOwnsCardRule
NO_HANDplayerOwnsCardRule / canPlayWildDrawFourRule
NOT_OWNEDplayerOwnsCardRule
NOT_SINGLEsingleCardRule
CARD_NOT_FOUNDcanPlayCardRule
CANNOT_PLAYcanPlayCardRule
WD4_ILLEGALcanPlayWildDrawFourRule
INVALID_COLORchooseColorValidRule
NOT_PENDINGchooseColorValidRule

示例 ​

ts
import { GameEngine, TurnPlugin, DrawPlugin, createPlayCardAction } from 'decklet'
import { UnoPlugin } from 'decklet/uno'

const engine = new GameEngine()
engine.use(TurnPlugin).use(DrawPlugin).use(UnoPlugin)
engine.createGame({
  players: [
    { id: 'p1', name: 'Alice', seat: 1 },
    { id: 'p2', name: 'Bob', seat: 2 }
  ]
})
engine.start()

// 试图由非当前玩家出牌 → 触发 playerTurnRule 拒绝
const state = engine.getState()
const otherId = state.currentPlayerId === 'p1' ? 'p2' : 'p1'
const result = engine.dispatch(createPlayCardAction(otherId, ['<someCardId>']))
console.log(result.ok, result.denyCode) // false, 'NOT_YOUR_TURN'

注意事项 ​

  • Rule 是只读校验——它们不能修改 GameState,仅返回 ok() / deny()。
  • appliesTo: '*' 表示对任意 Action 类型生效(包括 UNO 自定义 Action 与引擎通用 Action)。
  • canPlayWildDrawFourRule 只在 currentColor 为实色时才检查手中同色牌;游戏开局未定色时不做限制(与 UNO 官方规则一致)。
  • Rule 短路顺序意味着:若 gameStartedRule 拒绝,后续 Rule 不会执行——这避免了在游戏未开始时还去校验卡牌归属等无关逻辑。