Effects
UNO 卡牌效果的副作用函数集合,作为 CARD_PLAYED 事件回调内按卡牌类型派发的纯函数拆分。
概述
effects/ 目录下每个文件导出一个 applyXxxEffect(ctx, playerId, ...) 函数,由 handleCardPlayed 在 CARD_PLAYED 事件回调中按 card.type 派发。这些函数不返回值,仅通过 GameContext 派发 Action / 发出事件 / 修改变量。
| 卡牌类型 | Effect 函数 | 触发事件 |
|---|---|---|
skip | applySkipEffect | UNO_PLAYER_SKIPPED |
reverse | applyReverseEffect | UNO_PLAYER_REVERSED(+ UNO_PLAYER_SKIPPED 当两人局) |
draw2 | applyDrawTwoEffect | UNO_DRAW_TWO |
wild | applyWildEffect | — |
wild_draw4 | applyWildDrawFourEffect(第一阶段) + applyWildDrawFourPostChoiceEffect(选色后第二阶段) | UNO_DRAW_FOUR |
applySkipEffect
function applySkipEffect(ctx: GameContext, playerId: string): void应用 Skip(跳过)牌的效果:跳过下家。
参数
| 名称 | 类型 | 说明 |
|---|---|---|
ctx | GameContext | 用于读取方向、派发 Action 与发出事件 |
playerId | string | 打出 Skip 牌的玩家 ID |
行为
实现方式是连续派发两个 END_TURN:
- 当前玩家的
END_TURN——把出牌权交给下家; - 下家的
END_TURN——立即结束其回合,把出牌权再交给下下家。
两次 END_TURN 之间下家没有任何行动机会,从而表现为"被跳过"。同时发出 UNO_PLAYER_SKIPPED 事件(payload: { skippedPlayerId: nextId })。
下家 ID 通过 nextPlayerId(playerId, playersInSeatOrder(state.players), direction) 计算,方向来自 TURN_VARIABLE_DIRECTION(缺省 1)。
applyReverseEffect
function applyReverseEffect(
ctx: GameContext,
playerId: string,
playerCount: number
): void应用 Reverse(反转方向)牌的效果:反转出牌方向。
参数
| 名称 | 类型 | 说明 |
|---|---|---|
ctx | GameContext | — |
playerId | string | 打出 Reverse 牌的玩家 ID |
playerCount | number | 当前游戏玩家总数,用于判断是否走两人局分支 |
行为
- 取当前方向
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
function applyDrawTwoEffect(ctx: GameContext, playerId: string): void应用 Draw Two(罚抽 2)牌的效果:下家抽 2 张并跳过。
参数
| 名称 | 类型 | 说明 |
|---|---|---|
ctx | GameContext | — |
playerId | string | 打出 Draw Two 牌的玩家 ID |
行为
- 计算下家
nextId(按当前方向)。 - 发出
UNO_DRAW_TWO事件(payload:{ targetPlayerId: nextId, count: 2 })。 ctx.dispatch(createDrawCardAction(nextId, 2))让下家抽 2 张。- 连续派发两个
END_TURN(当前玩家 → 下家),使下家在抽完 2 张后立即失去出牌机会,等价于"抽牌 + 跳过"。
applyWildEffect
function applyWildEffect(ctx: GameContext, _playerId: string): void应用 Wild(百搭)牌的效果:进入等待玩家指定颜色的状态。
参数
| 名称 | 类型 | 说明 |
|---|---|---|
ctx | GameContext | — |
_playerId | string | 打出 Wild 牌的玩家 ID(未使用,保留以保持 effect 签名一致) |
行为
- 仅
ctx.setVariable(UNO_VAR_PENDING_COLOR, true),不立即推进回合。 - 回合的推进被推迟到玩家提交
CHOOSE_COLOR_ACTION之后:由handleColorChosen在玩家选色完成后派发END_TURN。
applyWildDrawFourEffect
function applyWildDrawFourEffect(ctx: GameContext, _playerId: string): void应用 Wild Draw Four 牌的第一阶段效果:进入等待玩家指定颜色的状态。
参数
| 名称 | 类型 | 说明 |
|---|---|---|
ctx | GameContext | — |
_playerId | string | 打出 Wild Draw Four 牌的玩家 ID(未使用,保留签名一致) |
行为
与普通 Wild 一样先置 UNO_VAR_PENDING_COLOR = true 暂停回合推进。罚抽 4 张的动作推迟到玩家选色后由 applyWildDrawFourPostChoiceEffect 执行,这样可以把"指定颜色"与"下家罚抽"在时序上明确分离,符合 UNO 规则。
applyWildDrawFourPostChoiceEffect
function applyWildDrawFourPostChoiceEffect(
ctx: GameContext,
playerId: string
): void应用 Wild Draw Four 牌的第二阶段效果:玩家选色后下家抽 4 张并跳过。
参数
| 名称 | 类型 | 说明 |
|---|---|---|
ctx | GameContext | — |
playerId | string | 打出 Wild Draw Four 牌并已完成选色的玩家 ID |
行为
在 handleColorChosen 收到 UNO_COLOR_CHOSEN 且 cardType === 'wild_draw4' 时调用。流程与 Draw Two 一致,仅抽牌数量为 4:
- 计算下家
nextId(按当前方向)。 - 发出
UNO_DRAW_FOUR事件(payload:{ targetPlayerId: nextId, count: 4 })。 ctx.dispatch(createDrawCardAction(nextId, 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示例
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。