Skip to content

Effects ​

UNO 卡牌效果的副作用函数集合,作为 CARD_PLAYED 事件回调内按卡牌类型派发的纯函数拆分。

概述 ​

effects/ 目录下每个文件导出一个 applyXxxEffect(ctx, playerId, ...) 函数,由 handleCardPlayed 在 CARD_PLAYED 事件回调中按 card.type 派发。这些函数不返回值,仅通过 GameContext 派发 Action / 发出事件 / 修改变量。

卡牌类型Effect 函数触发事件
skipapplySkipEffectUNO_PLAYER_SKIPPED
reverseapplyReverseEffectUNO_PLAYER_REVERSED(+ UNO_PLAYER_SKIPPED 当两人局)
draw2applyDrawTwoEffectUNO_DRAW_TWO
wildapplyWildEffect—
wild_draw4applyWildDrawFourEffect(第一阶段) + applyWildDrawFourPostChoiceEffect(选色后第二阶段)UNO_DRAW_FOUR

applySkipEffect ​

ts
function applySkipEffect(ctx: GameContext, playerId: string): void

应用 Skip(跳过)牌的效果:跳过下家。

参数 ​

名称类型说明
ctxGameContext用于读取方向、派发 Action 与发出事件
playerIdstring打出 Skip 牌的玩家 ID

行为 ​

实现方式是连续派发两个 END_TURN:

  1. 当前玩家的 END_TURN——把出牌权交给下家;
  2. 下家的 END_TURN——立即结束其回合,把出牌权再交给下下家。

两次 END_TURN 之间下家没有任何行动机会,从而表现为"被跳过"。同时发出 UNO_PLAYER_SKIPPED 事件(payload: { skippedPlayerId: nextId })。

下家 ID 通过 nextPlayerId(playerId, playersInSeatOrder(state.players), direction) 计算,方向来自 TURN_VARIABLE_DIRECTION(缺省 1)。

applyReverseEffect ​

ts
function applyReverseEffect(
  ctx: GameContext,
  playerId: string,
  playerCount: number
): void

应用 Reverse(反转方向)牌的效果:反转出牌方向。

参数 ​

名称类型说明
ctxGameContext—
playerIdstring打出 Reverse 牌的玩家 ID
playerCountnumber当前游戏玩家总数,用于判断是否走两人局分支

行为 ​

  • 取当前方向 prevDir(来自 TURN_VARIABLE_DIRECTION,缺省 1),计算 newDir = prevDir === 1 ? -1 : 1。
  • ctx.setVariable(TURN_VARIABLE_DIRECTION, newDir)。
  • 发出 UNO_PLAYER_REVERSED 事件(payload: { newDirection: newDir })。

两人局特殊处理(playerCount === 2):

  • 反转方向在效果上等价于跳过另一名玩家(因为反转后"下家"仍是当前玩家本人)。
  • 此时按 Skip 牌的逻辑,用旧方向 prevDir 计算出另一名玩家 nextId,发出 UNO_PLAYER_SKIPPED 事件,并连续派发两个 END_TURN(当前玩家 → 下家)将对方跳过。

多人局(playerCount >= 3):

  • 只切换方向变量并派发当前玩家的 END_TURN,由 TurnPlugin 按新方向决定下一位出牌者。

applyDrawTwoEffect ​

ts
function applyDrawTwoEffect(ctx: GameContext, playerId: string): void

应用 Draw Two(罚抽 2)牌的效果:下家抽 2 张并跳过。

参数 ​

名称类型说明
ctxGameContext—
playerIdstring打出 Draw Two 牌的玩家 ID

行为 ​

  1. 计算下家 nextId(按当前方向)。
  2. 发出 UNO_DRAW_TWO 事件(payload: { targetPlayerId: nextId, count: 2 })。
  3. ctx.dispatch(createDrawCardAction(nextId, 2)) 让下家抽 2 张。
  4. 连续派发两个 END_TURN(当前玩家 → 下家),使下家在抽完 2 张后立即失去出牌机会,等价于"抽牌 + 跳过"。

applyWildEffect ​

ts
function applyWildEffect(ctx: GameContext, _playerId: string): void

应用 Wild(百搭)牌的效果:进入等待玩家指定颜色的状态。

参数 ​

名称类型说明
ctxGameContext—
_playerIdstring打出 Wild 牌的玩家 ID(未使用,保留以保持 effect 签名一致)

行为 ​

  • 仅 ctx.setVariable(UNO_VAR_PENDING_COLOR, true),不立即推进回合。
  • 回合的推进被推迟到玩家提交 CHOOSE_COLOR_ACTION 之后:由 handleColorChosen 在玩家选色完成后派发 END_TURN。

applyWildDrawFourEffect ​

ts
function applyWildDrawFourEffect(ctx: GameContext, _playerId: string): void

应用 Wild Draw Four 牌的第一阶段效果:进入等待玩家指定颜色的状态。

参数 ​

名称类型说明
ctxGameContext—
_playerIdstring打出 Wild Draw Four 牌的玩家 ID(未使用,保留签名一致)

行为 ​

与普通 Wild 一样先置 UNO_VAR_PENDING_COLOR = true 暂停回合推进。罚抽 4 张的动作推迟到玩家选色后由 applyWildDrawFourPostChoiceEffect 执行,这样可以把"指定颜色"与"下家罚抽"在时序上明确分离,符合 UNO 规则。

applyWildDrawFourPostChoiceEffect ​

ts
function applyWildDrawFourPostChoiceEffect(
  ctx: GameContext,
  playerId: string
): void

应用 Wild Draw Four 牌的第二阶段效果:玩家选色后下家抽 4 张并跳过。

参数 ​

名称类型说明
ctxGameContext—
playerIdstring打出 Wild Draw Four 牌并已完成选色的玩家 ID

行为 ​

在 handleColorChosen 收到 UNO_COLOR_CHOSEN 且 cardType === 'wild_draw4' 时调用。流程与 Draw Two 一致,仅抽牌数量为 4:

  1. 计算下家 nextId(按当前方向)。
  2. 发出 UNO_DRAW_FOUR 事件(payload: { targetPlayerId: nextId, count: 4 })。
  3. ctx.dispatch(createDrawCardAction(nextId, 4)) 让下家抽 4 张。
  4. 连续派发两个 END_TURN(当前玩家 → 下家),跳过下家。

Effect 与事件关系图 ​

                            CARD_PLAYED 事件
                                  │
                                  ▼
                       ┌──────────────────────┐
                       │  handleCardPlayed    │
                       │  按 card.type 分支    │
                       └───┬──────────────────┘
                           │
       ┌──────────┬────────┼────────┬──────────┬──────────┐
       ▼          ▼        ▼        ▼          ▼          ▼
   number       skip   reverse   draw2       wild    wild_draw4
       │          │        │        │          │          │
       │          ▼        ▼        ▼          ▼          ▼
       │   applySkip  applyReverse applyDraw2 applyWild  applyWildDraw4
       │   Effect     Effect        Effect    Effect     Effect (置 pending)
       │          │        │        │          │          │
       │          ▼        ▼        ▼          │          ▼
       │   UNO_PLAYER  UNO_PLAYER  UNO_DRAW_TWO │     (等待玩家选色)
       │   _SKIPPED   _REVERSED                │          │
       │          │        │        │          │          │
       │          └──END_TURN×2 / END_TURN     │          │
       │                                          │          │
       └──END_TURN◀──────────────────────────────┘          │
                                                              │
                                       CHOOSE_COLOR_ACTION    │
                                              │               │
                                              ▼               │
                                       UNO_COLOR_CHOSEN 事件  │
                                              │               │
                                              ▼               │
                                       handleColorChosen       │
                                              │               │
                              ┌───────────────┴───────────────┘
                              ▼                                
                  cardType === 'wild_draw4' ?
                       │              │
                       ▼ true         ▼ false
        applyWildDraw4PostChoice   END_TURN
        Effect                     (普通 wild 走此分支)
              │
              ▼
        UNO_DRAW_FOUR
        DRAW_CARD(4) + END_TURN×2

示例 ​

ts
import { GameEngine, TurnPlugin, DrawPlugin } from 'decklet'
import {
  UnoPlugin,
  UNO_PLAYER_SKIPPED,
  UNO_PLAYER_REVERSED,
  UNO_DRAW_TWO,
  UNO_DRAW_FOUR
} from 'decklet/uno'

const engine = new GameEngine()
engine.use(TurnPlugin).use(DrawPlugin).use(UnoPlugin)

engine.subscribeAll((e) => {
  switch (e.type) {
    case UNO_PLAYER_SKIPPED:
      console.log('skipped:', (e.payload as { skippedPlayerId: string }).skippedPlayerId)
      break
    case UNO_PLAYER_REVERSED:
      console.log('reversed, new dir:', (e.payload as { newDirection: number }).newDirection)
      break
    case UNO_DRAW_TWO:
      console.log('draw 2:', (e.payload as { targetPlayerId: string }).targetPlayerId)
      break
    case UNO_DRAW_FOUR:
      console.log('draw 4:', (e.payload as { targetPlayerId: string }).targetPlayerId)
      break
  }
})

engine.createGame({
  players: [
    { id: 'p1', name: 'Alice', seat: 1 },
    { id: 'p2', name: 'Bob', seat: 2 },
    { id: 'p3', name: 'Carol', seat: 3 }
  ]
})
engine.start()
// 出牌 / 选色 … 由 UnoPlugin 内部自动派发对应 Effect

注意事项 ​

  • 所有 Effect 函数都是同步副作用函数,不返回 Promise;它们通过 ctx.dispatch / ctx.emit / ctx.setVariable 操作引擎状态。
  • applyWildEffect 与 applyWildDrawFourEffect 都仅置 UNO_VAR_PENDING_COLOR = true,不立即结束回合——必须等玩家提交 CHOOSE_COLOR_ACTION 后由 handleColorChosen 推进回合。
  • 两人局的 Reverse 效果等价于 Skip——这一特殊处理由 applyReverseEffect 根据 playerCount === 2 分支实现,调用方(handleCardPlayed)需正确传入 playerCount。
  • Effect 派发的 END_TURN 链式触发 TurnPlugin 推进 currentPlayerId,因此 Effect 函数本身不直接操作 currentPlayerId。