Rules
UNO 插件注册到 RuleEngine 的校验规则集合。
概述
UNO 的 4 个规则文件共导出 8 个 Rule 实例,由 UnoPlugin.rules 按顺序注册。RuleEngine 在每次 Action 执行前按顺序短路执行所有 appliesTo 匹配的 Rule——任一返回 deny 即拒绝该 Action。
| Rule | id | appliesTo |
|---|---|---|
gameNotWonRule | uno-game-not-won | '*' |
gameStartedRule | uno-game-started | '*' |
playerTurnRule | uno-player-turn | PLAY_CARD_ACTION, CHOOSE_COLOR_ACTION |
playerOwnsCardRule | uno-player-owns-card | PLAY_CARD_ACTION |
singleCardRule | uno-single-card | PLAY_CARD_ACTION |
canPlayCardRule | uno-can-play-card | PLAY_CARD_ACTION |
canPlayWildDrawFourRule | uno-wild-draw4-legal | PLAY_CARD_ACTION |
chooseColorValidRule | uno-choose-color-valid | CHOOSE_COLOR_ACTION |
通用规则原语:ok() 表示通过,deny(message, code) 表示拒绝并附带错误码。
WinRule
gameStartedRule
const gameStartedRule: Rule| 字段 | 值 |
|---|---|
id | 'uno-game-started' |
appliesTo | '*' |
行为
所有 Action 仅在对局状态为 playing 时才允许。防止在 waiting / finished 等阶段执行出牌、选色等操作。
state.status === 'playing'→ok()- 否则 →
deny('game has not started', 'NOT_STARTED')
gameNotWonRule
const gameNotWonRule: Rule| 字段 | 值 |
|---|---|
id | 'uno-game-not-won' |
appliesTo | '*' |
行为
一旦已有获胜者,拒绝所有后续 Action。用于在有人胜出后锁定对局状态,防止继续出牌或派生动作。
state.winnerIds.length === 0→ok()- 否则 →
deny('game already won', 'ALREADY_WON')
TurnRule
playerTurnRule
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
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
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
const canPlayCardRule: Rule| 字段 | 值 |
|---|---|
id | 'uno-can-play-card' |
appliesTo | [PLAY_CARD_ACTION] |
行为
UNO 出牌合法性规则:打出的牌必须与当前状态匹配。判定顺序(任一满足即通过):
- Wild 类牌(
isWildCard(card)为true,即wild/wild_draw4)——可随时打出,不受颜色 / 牌面约束(Wild Draw Four 的额外限制由canPlayWildDrawFourRule单独校验)。 - 与当前生效颜色同色:
card.data.color === state.variables[UNO_VAR_CURRENT_COLOR]。 - 与弃牌堆顶张牌面相同:
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
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
const chooseColorValidRule: Rule| 字段 | 值 |
|---|---|
id | 'uno-choose-color-valid' |
appliesTo | [CHOOSE_COLOR_ACTION] |
行为
CHOOSE_COLOR Action 的合法性规则:颜色值有效且当前确处于等待选色状态。两个条件必须同时满足:
payload.color是合法的UnoColorChoice(通过isUnoColorChoice守卫,拒绝'wild'与非法字符串)。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_STARTED | gameStartedRule |
ALREADY_WON | gameNotWonRule |
NO_PLAYER | playerTurnRule / playerOwnsCardRule / canPlayWildDrawFourRule |
NOT_YOUR_TURN | playerTurnRule |
NO_CARDS | playerOwnsCardRule |
NO_HAND | playerOwnsCardRule / canPlayWildDrawFourRule |
NOT_OWNED | playerOwnsCardRule |
NOT_SINGLE | singleCardRule |
CARD_NOT_FOUND | canPlayCardRule |
CANNOT_PLAY | canPlayCardRule |
WD4_ILLEGAL | canPlayWildDrawFourRule |
INVALID_COLOR | chooseColorValidRule |
NOT_PENDING | chooseColorValidRule |
示例
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 不会执行——这避免了在游戏未开始时还去校验卡牌归属等无关逻辑。