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.ts | applyCallLandlordSideEffects |
effects/PassEffect.ts | applyPassSideEffects、readPassCount、readLastPlayerId |
effects/PlayCardsEffect.ts | applyPlayCardsSideEffects、emitBombFamilyEvent、declareWin、resetRoundState |
effects/RobLandlordEffect.ts | applyRobLandlordSideEffects |
CallLandlordEffect
applyCallLandlordSideEffects()
function applyCallLandlordSideEffects(
ctx: GameContext,
playerId: string,
call: boolean
): void在 CallLandlord Action 处理器更新完 landlordCandidateId(不叫时则更新 bidderPassed)之后,由 DDZ_LANDLORD_CALLED 事件处理器调用。
参数
ctx: GameContext— 游戏上下文playerId: string— 执行叫地主的玩家 idcall: boolean— 是否叫地主
行为
Phase 5 简化流程(按规格"第一个叫地主成功的人成为地主"):
call === true→ 终局该玩家为地主,派发REVEAL_BOTTOM_CARDSAction(其处理器将设置landlordId/farmerIds/phase = PLAYING)。call === false→ 读取当前快照:- 若
bidderPassed.length >= ordered.length(所有玩家都不叫)→ 调用内部redeal(ctx)重新洗牌发牌、重置叫分状态、再次发出DDZ_CARDS_DEALT事件; - 否则 → 派发
END_TURNAction 推进到下一位叫分人。
- 若
内部函数:redeal()
function redeal(ctx: GameContext): void重新洗牌并发手牌,开始新一轮叫分。在所有叫分人都不叫时触发。阶段保持 BIDDING;bidderPassed 与 bidderIndex 会被重置;currentPlayerId 重置为最低座位号玩家。流程:
- 将所有手牌 zone、
ddz-deck、ddz-bottom中的卡牌逐张移回ddz-deck; - 通过
ctx.random.shuffle重新洗牌ddz-deck中的卡牌(使用临时 custom zone 中转); - 重新发牌:每位玩家 17 张手牌,底牌 3 张;
- 重置
DDZ_VAR_BIDDER_PASSED = []、DDZ_VAR_BIDDER_INDEX = 0、DDZ_VAR_LANDLORD_CANDIDATE_ID = undefined; - 将
currentPlayerId重置为首位座位玩家; - 发出
DDZ_CARDS_DEALT事件。
示例
// 由 DoudizhuPlugin 内部事件处理器调用,通常不直接使用:
// handleLandlordCalled → applyCallLandlordSideEffects(ctx, payload.playerId, payload.call)PassEffect
applyPassSideEffects()
function applyPassSideEffects(
ctx: GameContext,
playerId: string
): void在 Pass Action 处理器累加完 passCount 之后,由 DDZ_PLAYER_PASSED 事件处理器调用。
参数
ctx: GameContext— 游戏上下文playerId: string— 选择过牌的玩家 id
行为
Phase 5 规则(3 人游戏):当 passCount >= N - 1(即另外两位玩家都过牌)时,重置当前轮次:
- 计算
starterId = getLastPlayerId(state) ?? playerId(最后一位真正出牌的玩家成为新一轮的首出者;若不存在则取当前过牌玩家); - 调用
resetRoundState(ctx, starterId)(来自PlayCardsEffect)清空lastCombination/passCount并设置currentPlayerId; - 发出
DDZ_ROUND_RESET事件,payload 为{ starterId },playerId字段为starterId。
否则(过牌次数尚不足)→ 派发 END_TURN Action 推进到下一位玩家。
示例
// 由 DoudizhuPlugin 内部事件处理器调用:
// handlePlayerPassed → applyPassSideEffects(ctx, payload.playerId)readPassCount()
function readPassCount(ctx: GameContext): number测试辅助函数:读取当前过牌次数。
返回值
number—ctx.state.variables[DDZ_VAR_PASS_COUNT],未设置时为0。
readLastPlayerId()
function readLastPlayerId(ctx: GameContext): string | undefined测试辅助函数:读取最后一位出牌玩家 id。
返回值
string | undefined—ctx.state.variables[DDZ_VAR_LAST_PLAYER_ID],未设置时为undefined。
PlayCardsEffect
applyPlayCardsSideEffects()
function applyPlayCardsSideEffects(
ctx: GameContext,
playerId: string
): void在 PlayCards Action 处理器完成卡牌移动、更新 lastCombination、并针对炸弹 / 火箭累加倍率之后调用。
参数
ctx: GameContext— 游戏上下文playerId: string— 出牌玩家 id
行为
- 若该玩家手牌已空(
player:{playerId}:handzone 的cards.length === 0)→ 调用declareWin(ctx, playerId)宣告胜者并结束游戏; - 否则 → 派发
END_TURNAction 推进到下一位玩家。
牌型从 pending 变量读取,因此此处无需重新检测。
示例
// 由 DoudizhuPlugin 内部事件处理器调用:
// handleCardsPlayed → applyPlayCardsSideEffects(ctx, payload.playerId)emitBombFamilyEvent()
function emitBombFamilyEvent(
ctx: GameContext,
playerId: string,
combination: DoudizhuCombination
): void检测出牌为炸弹家族时需要发出的 BOMB / ROCKET 倍率事件。在 PlayCards Action 处理器识别出牌型后调用。
参数
ctx: GameContext— 游戏上下文playerId: string— 出牌玩家 idcombination: 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()
function declareWin(ctx: GameContext, winnerId: string): void在 ctx 上设置胜利状态:将获胜队伍的玩家 id 加入 winnerIds、标记游戏为结束,并发出核心与斗地主专用的胜利事件。
参数
ctx: GameContext— 游戏上下文winnerId: string— 清空手牌的玩家 id
行为
- 读取
landlordId、farmers、teamOf(winnerId, state); - 计算
winnerIds:team === 'LANDLORD' && landlordId→[landlordId]team === 'FARMERS'→farmers- 其它(地主未确定等异常情况) →
[winnerId]
- 读取
multiplier(默认 1); - 对每个
id调用ctx.addWinner(id),并ctx.setStatus('finished'); - 依次发出:
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')。
示例
import { declareWin } from './effects/PlayCardsEffect.js'
// 当玩家清空手牌后由 handleCardsPlayed 调用
declareWin(ctx, 'p1')resetRoundState()
function resetRoundState(ctx: GameContext, starterId: string): void在新一轮开始时清空 lastCombination / passCount 状态。
参数
ctx: GameContext— 游戏上下文starterId: string— 新一轮首位出牌玩家 id
行为
DDZ_VAR_LAST_COMBINATION = undefinedDDZ_VAR_LAST_PLAYER_ID = starterIdDDZ_VAR_PASS_COUNT = 0currentPlayerId = starterId
由 PassEffect.applyPassSideEffects 在连续过牌触发回合重置时调用。
RobLandlordEffect
applyRobLandlordSideEffects()
function applyRobLandlordSideEffects(
ctx: GameContext,
playerId: string,
rob: boolean
): voidPhase 5 首版的 RobLandlord 副作用。
参数
ctx: GameContext— 游戏上下文playerId: string— 抢地主的玩家 idrob: boolean— 是否抢地主
行为
Phase 5 默认叫分流程不会触发 ROB_LANDLORD——首位叫地主者立即成为地主。注册此处理器是为了前向兼容更完整的叫分流程。被调用时的行为:
- 读取
ddz = getDoudizhuState(state),candidateId = ddz.landlordCandidateId; - 计算
landlordId = rob ? playerId : (candidateId ?? playerId); - 按座位顺序推导
farmerIds(除 landlordId 外的玩家); - 写入:
DDZ_VAR_LANDLORD_CANDIDATE_ID = landlordIdDDZ_VAR_LANDLORD_ID = landlordIdDDZ_VAR_FARMER_IDS = farmerIds
- 若
rob === true→ 读取DDZ_VAR_MULTIPLIER,DDZ_VAR_MULTIPLIER = m * 2(标准斗地主中"抢地主"使倍数翻倍); - 派发
REVEAL_BOTTOM_CARDSAction 终局。
注意事项
与 CallLandlord 的副作用不同,RobLandlord 副作用直接设置 DDZ_VAR_LANDLORD_ID 与 DDZ_VAR_FARMER_IDS,然后派发 REVEAL_BOTTOM_CARDS——而 REVEAL_BOTTOM_CARDS 处理器会再次用 landlordCandidateId 覆盖这两个字段。因此最终结果以 REVEAL_BOTTOM_CARDS 处理器的写入为准。
示例
// 由 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 实际运行中由处理器内联发出,本函数作为可复用辅助保留。