UnoPlugin
UNO 游戏插件主对象,把 setup、rules、actions、events 装配为一个 GamePlugin。
概述
UnoPlugin 是一个 GamePlugin 对象(非 class),通过 engine.use(UnoPlugin) 注册到引擎后即提供完整的 UNO 对局能力。它本身不持有状态——所有运行时状态都写入 GameState(zones / cards / variables)。
UnoPlugin 内部按职责拆分:
| 职责 | 实现位置 |
|---|---|
setup | 创建牌堆 / 弃牌堆 zone、生成并洗入 108 张 UNO 牌、配置抽牌堆重洗策略 |
actions | PLAY_CARD(出牌并更新颜色 / 发出事件)、CHOOSE_COLOR(玩家选色)、UNO_FLIP_INITIAL(翻初始弃牌) |
events | GAME_STARTED(发初始手牌 + 翻初始牌)、CARD_PLAYED(判定胜负并按卡牌类型派发 Effect)、UNO_COLOR_CHOSEN(选色后推进回合或执行罚抽) |
rules | 组合胜负 / 回合 / 归属 / 出牌合法性 / 选色合法性等校验规则 |
Effect(applyXxxEffect)作为 EventHandler 内副作用的纯函数拆分,集中位于 effects/ 目录。
属性/字段
UnoPlugin 为对象字面量,符合 GamePlugin 接口:
| 名称 | 类型 | 说明 |
|---|---|---|
id | 'uno' | 插件 id,与 gameType 一致 |
name | 'UNO' | 展示名 |
version | '1.0.0' | 版本号 |
setup | (ctx: GameContext) => void | 引擎 createGame 时执行一次(回放会再次执行,故通过存在性判断保持幂等) |
rules | Rule[] | 注册的 8 条规则 |
actions | ActionHandler[] | 注册的 3 个 ActionHandler |
events | EventHandler[] | 注册的 3 个事件处理器 |
setup
ts
setup(ctx: GameContext): void行为
- 若
UNO_DECK_ZONE_ID('deck')zone 不存在则创建。 - 若
UNO_DISCARD_ZONE_ID('discard')zone 不存在则创建。 - 调用
createShuffledUnoDeck(ctx.random)生成洗好的 108 张 UNO 牌,逐张ctx.addCard+ctx.addCardToZone(card.id, UNO_DECK_ZONE_ID)入牌堆(已存在的卡牌跳过addCard以保持幂等)。 ctx.setVariable(UNO_VAR_PENDING_COLOR, false)初始化"等待选色"标记。- 配置
DrawPlugin的重洗策略:DRAW_VAR_RESHUFFLE_FROM = UNO_DISCARD_ZONE_ID:抽牌堆耗尽时从弃牌堆回收洗回。DRAW_VAR_RESHUFFLE_KEEP_TOP = true:保留弃牌堆顶张以维持当前生效颜色 / 牌面不变。
幂等性设计使得回放重放 setup 时不会重复注册 zone / 卡牌。
注册的 Rules
按数组顺序注册(RuleEngine 短路执行):
| 顺序 | Rule | 说明 |
|---|---|---|
| 1 | gameNotWonRule | 已有获胜者后拒绝所有 Action |
| 2 | gameStartedRule | 仅在 status === 'playing' 时允许 Action |
| 3 | playerTurnRule | PLAY_CARD / CHOOSE_COLOR 必须由当前玩家发起 |
| 4 | playerOwnsCardRule | PLAY_CARD 的卡牌必须在发起玩家手牌 zone 中 |
| 5 | singleCardRule | PLAY_CARD 必须且只能打出一张牌 |
| 6 | canPlayCardRule | 出牌合法性(同色 / 同牌面 / Wild) |
| 7 | canPlayWildDrawFourRule | Wild Draw Four 仅在手中无同色牌时允许 |
| 8 | chooseColorValidRule | CHOOSE_COLOR 颜色值有效且当前处于等待选色状态 |
详见 rules.md。
注册的 Actions
| ActionHandler | type | 说明 |
|---|---|---|
playCardHandler | PLAY_CARD_ACTION | 出牌:移牌手→弃牌堆、发出 CARD_PLAYED 与 UNO_CARD_PLAYED;实色牌额外更新当前色并发出 UNO_COLOR_CHANGED |
chooseColorHandler | CHOOSE_COLOR_ACTION | 选色:更新当前色、解除 pending、发出 UNO_COLOR_CHANGED 与 UNO_COLOR_CHOSEN |
flipInitialCardHandler | UNO_FLIP_INITIAL_ACTION | 翻初始弃牌:从牌堆顶翻牌直到数字牌 |
详见 actions.md。
注册的 Events
| 事件类型 | 处理函数 | 说明 |
|---|---|---|
GAME_STARTED | handleGameStarted | 给每位玩家派发 DRAW_CARD(7) + 派发 UNO_FLIP_INITIAL |
CARD_PLAYED | handleCardPlayed | 判定胜负;按卡牌类型派发对应 Effect |
UNO_COLOR_CHOSEN | handleColorChosen | 选色完成后推进回合或执行 Wild Draw Four 罚抽 |
handleGameStarted
ts
function handleGameStarted(_event: GameEvent, ctx: GameContext): void行为
- 取
playersInSeatOrder(ctx.state.players)按座次遍历,每位玩家ctx.dispatch(createDrawCardAction(p.id, UNO_INITIAL_HAND_SIZE))发 7 张。 ctx.dispatch(createUnoFlipInitialAction())翻初始弃牌。
handleCardPlayed
ts
function handleCardPlayed(event: GameEvent, ctx: GameContext): void行为
- 从事件 payload 取
playerId与cardIds[0],取出卡牌。 - 胜负判定:若发起玩家手牌 zone 为空,则
ctx.addWinner(playerId)、ctx.setStatus('finished'),并发出GAME_WON、GAME_FINISHED、UNO_GAME_WON三个事件后 return。 - 否则按
card.type派发对应 Effect:
card.type | Effect 调用 |
|---|---|
'number' | ctx.dispatch(createEndTurnAction(playerId)) |
'skip' | applySkipEffect(ctx, playerId) |
'reverse' | applyReverseEffect(ctx, playerId, playerCount) |
'draw2' | applyDrawTwoEffect(ctx, playerId) |
'wild' | applyWildEffect(ctx, playerId) |
'wild_draw4' | applyWildDrawFourEffect(ctx, playerId) |
其中 playerCount = Object.keys(ctx.state.players).length。
handleColorChosen
ts
function handleColorChosen(event: GameEvent, ctx: GameContext): void行为
- 从 payload 取
playerId与cardType。 - 若
cardType === 'wild_draw4':调用applyWildDrawFourPostChoiceEffect(ctx, playerId)执行下家罚抽 4 张并跳过。 - 否则(普通
wild):ctx.dispatch(createEndTurnAction(playerId))结束当前玩家回合。
示例
ts
import { GameEngine, TurnPlugin, DrawPlugin } from 'decklet'
import { UnoPlugin } from 'decklet/uno'
const engine = new GameEngine()
engine.use(TurnPlugin).use(DrawPlugin).use(UnoPlugin)
engine.subscribeAll((e) => {
if (e.type === 'UNO_GAME_WON') {
console.log('winner:', (e.payload as { winnerIds: string[] }).winnerIds)
}
})
engine.createGame({
players: [
{ id: 'p1', name: 'Alice', seat: 1 },
{ id: 'p2', name: 'Bob', seat: 2 },
{ id: 'p3', name: 'Carol', seat: 3 }
]
})
engine.start()注意事项
UnoPlugin必须在TurnPlugin与DrawPlugin之后注册,否则 setup 阶段引用的DRAW_VAR_RESHUFFLE_FROM等变量无效果,回合推进也不会发生。handleCardPlayed内的胜负判定发生在 Effect 派发之前——一旦判定获胜,本次出牌不再触发任何 Effect。handleCardPlayed中发出的GAME_WON/GAME_FINISHED是引擎通用事件,UNO_GAME_WON是 UNO 专属事件,三者并存供不同订阅方使用。