Skip to content

Effects ​

斗地主插件的副作用处理函数:在 Action 处理器完成主状态更新、发出对应事件后,由 DoudizhuPlugin 的事件处理器委托这些纯函数处理后续的轮次推进、回合重置、叫分轮换、终局切换与胜负宣告。

概述 ​

斗地主插件将"业务副作用"与"状态更新"分离:

  • Action 处理器(在 DoudizhuPlugin.ts 内,不导出):负责不可变的状态更新与发出主事件;
  • Effect 函数(本模块导出):由事件处理器调用,负责派发后续 Action(如 END_TURN / REVEAL_BOTTOM_CARDS)、重置回合、宣告胜负、发出次要事件(如 DDZ_ROUND_RESET / DDZ_PLAYER_WON)。

所有 Effect 函数均接收 GameContext(提供 dispatch / emit / setVariable / moveCard 等命令式 API)。

文件与导出 ​

文件导出函数
effects/CallLandlordEffect.tsapplyCallLandlordSideEffects
effects/PassEffect.tsapplyPassSideEffects、readPassCount、readLastPlayerId
effects/PlayCardsEffect.tsapplyPlayCardsSideEffects、emitBombFamilyEvent、declareWin、resetRoundState
effects/RobLandlordEffect.tsapplyRobLandlordSideEffects

CallLandlordEffect ​

applyCallLandlordSideEffects() ​

ts
function applyCallLandlordSideEffects(
  ctx: GameContext,
  playerId: string,
  call: boolean
): void

在 CallLandlord Action 处理器更新完 landlordCandidateId(不叫时则更新 bidderPassed)之后,由 DDZ_LANDLORD_CALLED 事件处理器调用。

参数 ​

  • ctx: GameContext — 游戏上下文
  • playerId: string — 执行叫地主的玩家 id
  • call: boolean — 是否叫地主

行为 ​

Phase 5 简化流程(按规格"第一个叫地主成功的人成为地主"):

  • call === true → 终局该玩家为地主,派发 REVEAL_BOTTOM_CARDS Action(其处理器将设置 landlordId / farmerIds / phase = PLAYING)。
  • call === false → 读取当前快照:
    • 若 bidderPassed.length >= ordered.length(所有玩家都不叫)→ 调用内部 redeal(ctx) 重新洗牌发牌、重置叫分状态、再次发出 DDZ_CARDS_DEALT 事件;
    • 否则 → 派发 END_TURN Action 推进到下一位叫分人。

内部函数:redeal() ​

ts
function redeal(ctx: GameContext): void

重新洗牌并发手牌,开始新一轮叫分。在所有叫分人都不叫时触发。阶段保持 BIDDING;bidderPassed 与 bidderIndex 会被重置;currentPlayerId 重置为最低座位号玩家。流程:

  1. 将所有手牌 zone、ddz-deck、ddz-bottom 中的卡牌逐张移回 ddz-deck;
  2. 通过 ctx.random.shuffle 重新洗牌 ddz-deck 中的卡牌(使用临时 custom zone 中转);
  3. 重新发牌:每位玩家 17 张手牌,底牌 3 张;
  4. 重置 DDZ_VAR_BIDDER_PASSED = []、DDZ_VAR_BIDDER_INDEX = 0、DDZ_VAR_LANDLORD_CANDIDATE_ID = undefined;
  5. 将 currentPlayerId 重置为首位座位玩家;
  6. 发出 DDZ_CARDS_DEALT 事件。

示例 ​

ts
// 由 DoudizhuPlugin 内部事件处理器调用,通常不直接使用:
// handleLandlordCalled → applyCallLandlordSideEffects(ctx, payload.playerId, payload.call)

PassEffect ​

applyPassSideEffects() ​

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

在 Pass Action 处理器累加完 passCount 之后,由 DDZ_PLAYER_PASSED 事件处理器调用。

参数 ​

  • ctx: GameContext — 游戏上下文
  • playerId: string — 选择过牌的玩家 id

行为 ​

Phase 5 规则(3 人游戏):当 passCount >= N - 1(即另外两位玩家都过牌)时,重置当前轮次:

  1. 计算 starterId = getLastPlayerId(state) ?? playerId(最后一位真正出牌的玩家成为新一轮的首出者;若不存在则取当前过牌玩家);
  2. 调用 resetRoundState(ctx, starterId)(来自 PlayCardsEffect)清空 lastCombination / passCount 并设置 currentPlayerId;
  3. 发出 DDZ_ROUND_RESET 事件,payload 为 { starterId },playerId 字段为 starterId。

否则(过牌次数尚不足)→ 派发 END_TURN Action 推进到下一位玩家。

示例 ​

ts
// 由 DoudizhuPlugin 内部事件处理器调用:
// handlePlayerPassed → applyPassSideEffects(ctx, payload.playerId)

readPassCount() ​

ts
function readPassCount(ctx: GameContext): number

测试辅助函数:读取当前过牌次数。

返回值 ​

  • number — ctx.state.variables[DDZ_VAR_PASS_COUNT],未设置时为 0。

readLastPlayerId() ​

ts
function readLastPlayerId(ctx: GameContext): string | undefined

测试辅助函数:读取最后一位出牌玩家 id。

返回值 ​

  • string | undefined — ctx.state.variables[DDZ_VAR_LAST_PLAYER_ID],未设置时为 undefined。

PlayCardsEffect ​

applyPlayCardsSideEffects() ​

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

在 PlayCards Action 处理器完成卡牌移动、更新 lastCombination、并针对炸弹 / 火箭累加倍率之后调用。

参数 ​

  • ctx: GameContext — 游戏上下文
  • playerId: string — 出牌玩家 id

行为 ​

  1. 若该玩家手牌已空(player:{playerId}:hand zone 的 cards.length === 0)→ 调用 declareWin(ctx, playerId) 宣告胜者并结束游戏;
  2. 否则 → 派发 END_TURN Action 推进到下一位玩家。

牌型从 pending 变量读取,因此此处无需重新检测。

示例 ​

ts
// 由 DoudizhuPlugin 内部事件处理器调用:
// handleCardsPlayed → applyPlayCardsSideEffects(ctx, payload.playerId)

emitBombFamilyEvent() ​

ts
function emitBombFamilyEvent(
  ctx: GameContext,
  playerId: string,
  combination: DoudizhuCombination
): void

检测出牌为炸弹家族时需要发出的 BOMB / ROCKET 倍率事件。在 PlayCards Action 处理器识别出牌型后调用。

参数 ​

  • ctx: GameContext — 游戏上下文
  • playerId: string — 出牌玩家 id
  • combination: DoudizhuCombination — 已识别的牌型

行为 ​

  • 若 combination.type === DoudizhuCombinationType.BOMB → 读取 DDZ_VAR_MULTIPLIER,发出 DDZ_BOMB_PLAYED 事件,payload 为 { playerId, mainRank, multiplier };
  • 若 combination.type === DoudizhuCombinationType.ROCKET → 读取 DDZ_VAR_MULTIPLIER,发出 DDZ_ROCKET_PLAYED 事件,payload 为 { playerId, multiplier };
  • 其它牌型 → 不做任何事。

注意事项 ​

Phase 5 实际实现中,DoudizhuPlugin.playCardsHandler 已在 Action 处理器内部直接发出 DDZ_BOMB_PLAYED / DDZ_ROCKET_PLAYED 事件,本函数作为可复用的辅助函数保留。

declareWin() ​

ts
function declareWin(ctx: GameContext, winnerId: string): void

在 ctx 上设置胜利状态:将获胜队伍的玩家 id 加入 winnerIds、标记游戏为结束,并发出核心与斗地主专用的胜利事件。

参数 ​

  • ctx: GameContext — 游戏上下文
  • winnerId: string — 清空手牌的玩家 id

行为 ​

  1. 读取 landlordId、farmers、teamOf(winnerId, state);
  2. 计算 winnerIds:
    • team === 'LANDLORD' && landlordId → [landlordId]
    • team === 'FARMERS' → farmers
    • 其它(地主未确定等异常情况) → [winnerId]
  3. 读取 multiplier(默认 1);
  4. 对每个 id 调用 ctx.addWinner(id),并 ctx.setStatus('finished');
  5. 依次发出:
    • GAME_WON(payload { winnerIds },playerId 为 winnerId)
    • GAME_FINISHED(payload { winnerIds },无 playerId)
    • DDZ_PLAYER_WON(payload { winnerIds, winnerTeam, multiplier })
    • DDZ_GAME_FINISHED(payload { winnerIds, winnerTeam, multiplier })

其中 winnerTeam = team ?? 'LANDLORD'(teamOf 返回 undefined 时默认为 'LANDLORD')。

示例 ​

ts
import { declareWin } from './effects/PlayCardsEffect.js'

// 当玩家清空手牌后由 handleCardsPlayed 调用
declareWin(ctx, 'p1')

resetRoundState() ​

ts
function resetRoundState(ctx: GameContext, starterId: string): void

在新一轮开始时清空 lastCombination / passCount 状态。

参数 ​

  • ctx: GameContext — 游戏上下文
  • starterId: string — 新一轮首位出牌玩家 id

行为 ​

  • DDZ_VAR_LAST_COMBINATION = undefined
  • DDZ_VAR_LAST_PLAYER_ID = starterId
  • DDZ_VAR_PASS_COUNT = 0
  • currentPlayerId = starterId

由 PassEffect.applyPassSideEffects 在连续过牌触发回合重置时调用。


RobLandlordEffect ​

applyRobLandlordSideEffects() ​

ts
function applyRobLandlordSideEffects(
  ctx: GameContext,
  playerId: string,
  rob: boolean
): void

Phase 5 首版的 RobLandlord 副作用。

参数 ​

  • ctx: GameContext — 游戏上下文
  • playerId: string — 抢地主的玩家 id
  • rob: boolean — 是否抢地主

行为 ​

Phase 5 默认叫分流程不会触发 ROB_LANDLORD——首位叫地主者立即成为地主。注册此处理器是为了前向兼容更完整的叫分流程。被调用时的行为:

  1. 读取 ddz = getDoudizhuState(state),candidateId = ddz.landlordCandidateId;
  2. 计算 landlordId = rob ? playerId : (candidateId ?? playerId);
  3. 按座位顺序推导 farmerIds(除 landlordId 外的玩家);
  4. 写入:
    • DDZ_VAR_LANDLORD_CANDIDATE_ID = landlordId
    • DDZ_VAR_LANDLORD_ID = landlordId
    • DDZ_VAR_FARMER_IDS = farmerIds
  5. 若 rob === true → 读取 DDZ_VAR_MULTIPLIER,DDZ_VAR_MULTIPLIER = m * 2(标准斗地主中"抢地主"使倍数翻倍);
  6. 派发 REVEAL_BOTTOM_CARDS Action 终局。

注意事项 ​

与 CallLandlord 的副作用不同,RobLandlord 副作用直接设置 DDZ_VAR_LANDLORD_ID 与 DDZ_VAR_FARMER_IDS,然后派发 REVEAL_BOTTOM_CARDS——而 REVEAL_BOTTOM_CARDS 处理器会再次用 landlordCandidateId 覆盖这两个字段。因此最终结果以 REVEAL_BOTTOM_CARDS 处理器的写入为准。

示例 ​

ts
// 由 DoudizhuPlugin 内部事件处理器调用:
// handleRobLandlord → applyRobLandlordSideEffects(ctx, payload.playerId, rob)

完整调用链路 ​

Action 处理器(更新 state + 发出主事件)
        │
        ▼
EventBus 派发主事件 → DoudizhuPlugin 的事件处理器
        │
        ▼
调用 Effect 函数
        │
        ├─ applyCallLandlordSideEffects
        │     ├─ call=true  → dispatch(REVEAL_BOTTOM_CARDS)
        │     └─ call=false → dispatch(END_TURN) 或 redeal()
        │
        ├─ applyPassSideEffects
        │     ├─ passCount>=N-1 → resetRoundState + emit(DDZ_ROUND_RESET)
        │     └─ 否则           → dispatch(END_TURN)
        │
        ├─ applyRobLandlordSideEffects
        │     └─ 写入 landlordId/farmerIds + (rob ? ×2 : ) + dispatch(REVEAL_BOTTOM_CARDS)
        │
        └─ applyPlayCardsSideEffects
              ├─ 手牌空 → declareWin(发出 GAME_WON/GAME_FINISHED/DDZ_PLAYER_WON/DDZ_GAME_FINISHED)
              └─ 否则  → dispatch(END_TURN)

注意事项 ​

  • 所有 Effect 函数均为命令式(通过 ctx 操作状态),不是纯函数——这与 Action 处理器的"纯函数返回 { state, events }"模式不同,因为它们运行在事件处理阶段(已脱离 Action 的原子事务边界)。
  • redeal 中存在一段注释说明 ctx 没有直接暴露 createZone,因此 drawN 在手牌 zone 缺失时不会主动创建(依赖初始发牌阶段已创建)。Phase 5 的实际流程中初始 handleGameStarted 已确保手牌 zone 存在,故 redeal 路径上手牌 zone 已存在——此分支仅在异常情况下触发。待确认:若手牌 zone 在 redeal 时确实缺失,drawNTo 会将牌移入不存在的 zone,行为未定义。
  • emitBombFamilyEvent 与 DoudizhuPlugin.playCardsHandler 内联的发出逻辑重复——Phase 5 实际运行中由处理器内联发出,本函数作为可复用辅助保留。