Skip to content

DoudizhuPlugin ​

斗地主游戏插件主入口,装配 Rule / ActionHandler / EventHandler / setup 为一个 GamePlugin,通过 GameContext 与 Engine 交互。

概述 ​

DoudizhuPlugin 是一个 GamePlugin 对象(非类),通过 engine.use(DoudizhuPlugin) 装配到 GameEngine。它负责:

  1. 在 createGame 时创建斗地主所需的三个 zone(deck / bottom / discard)并洗牌生成 54 张牌;
  2. 注册 9 条 Rule(阶段、轮次、牌权、所属权、牌型、PASS、叫 / 抢地主);
  3. 注册 5 个 ActionHandler(PLAY_CARDS / PASS / CALL_LANDLORD / ROB_LANDLORD / REVEAL_BOTTOM_CARDS);
  4. 监听 5 个事件(GAME_STARTED / CARD_PLAYED / DDZ_LANDLORD_CALLED / DDZ_LANDLORD_ROBBED / DDZ_PLAYER_PASSED),将副作用委托给 effects/* 中的纯函数。

所有 ActionHandler 为纯函数:输入 (action, ctx),输出 { state, events },通过 stateUtils.* 做不可变更新。

插件字段 ​

名称类型说明
idstring插件标识符,固定为 'doudizhu'
namestring人类可读名称 'Doudizhu (斗地主)'
versionstring插件版本 '1.0.0'
setup(ctx: GameContext) => voidcreateGame 时调用一次(回放会再次执行,故需幂等)
rulesRule[]注册到 RuleEngine 的规则数组,顺序决定短路顺序
actionsActionHandler[]注册到 ActionExecutor 的处理器数组
events{ type: string; handle: PluginEventHandler }[]注册到 EventBus 的事件处理器数组

setup 行为 ​

ts
setup(ctx: GameContext): void

在 createGame 时调用一次(回放会再次执行,故需幂等):

  1. 若不存在则创建三个 zone:
    • ddz-deck(type: 'deck')
    • ddz-bottom(type: 'custom')
    • ddz-discard(type: 'discard')
  2. 通过 createShuffledDoudizhuDeck(ctx.random) 构建并加入 54 张斗地主卡牌到 deck zone(已存在则跳过);
  3. 初始化所有斗地主专用变量到默认值:
    • DDZ_VAR_PHASE = DoudizhuPhase.DEALING
    • DDZ_VAR_BIDDER_INDEX = 0、DDZ_VAR_BIDDER_PASSED = []
    • DDZ_VAR_LANDLORD_CANDIDATE_ID = undefined、DDZ_VAR_LANDLORD_ID = undefined、DDZ_VAR_FARMER_IDS = []
    • DDZ_VAR_BOTTOM_CARD_IDS = []、DDZ_VAR_LAST_COMBINATION = undefined、DDZ_VAR_LAST_PLAYER_ID = undefined
    • DDZ_VAR_PASS_COUNT = 0、DDZ_VAR_MULTIPLIER = 1
  4. 将斗地主状态不变式 doudizhuStateCheck 注册到核心 StateValidator(注册名 DDZ_STATE_CHECK_NAME = 'doudizhu')。

注册的 Rule ​

按短路顺序排列:

序号Rule id名称适用 Action
1ddz-game-not-wonDoudizhu Game Not Won*(全部)
2ddz-game-startedDoudizhu Game Started*(全部)
3ddz-phaseDoudizhu Phase*(全部)
4ddz-player-turnDoudizhu Player Turn*(全部)
5ddz-player-owns-cardsDoudizhu Player Owns CardsPLAY_CARDS
6ddz-combination-validDoudizhu Combination ValidPLAY_CARDS
7ddz-passDoudizhu PassPASS
8ddz-call-landlordDoudizhu Call LandlordCALL_LANDLORD
9ddz-rob-landlordDoudizhu Rob LandlordROB_LANDLORD

注册顺序决定了 RuleEngine 的短路顺序——阶段、轮次、牌权、所属权、牌型等基础校验必须先于业务规则(叫 / 抢地主)执行。详见 rules.md。

注册的 ActionHandler ​

playCardsHandler(PLAY_CARDS) ​

ts
type: PLAY_CARDS_ACTION
execute(action: Action<PlayCardsAction>, ctx: GameContext): ActionHandlerResult

行为:

  1. 将 payload.cardIds 中的每张卡牌从玩家手牌 zone 移动到弃牌堆 ddz-discard;
  2. 读取 CombinationRule 预检阶段写入 DDZ_VAR_PENDING_COMBINATION 的牌型,避免重复检测;缺失时(说明 Rule 被旁路)则保持 state 不变且不发出任何 events——Engine 会将其视为未产生 events 的 Action;
  3. 命中后更新 DDZ_VAR_LAST_COMBINATION / DDZ_VAR_LAST_PLAYER_ID,并重置 DDZ_VAR_PASS_COUNT = 0;
  4. 炸弹 / 火箭触发倍率 ×2,并按需追加 DDZ_BOMB_PLAYED / DDZ_ROCKET_PLAYED 事件;
  5. 始终发出核心 CARD_PLAYED 事件与斗地主专用 DDZ_CARDS_PLAYED 事件。

passHandler(PASS) ​

ts
type: PASS_ACTION
execute(action: PassAction, ctx: GameContext): ActionHandlerResult

行为:累加 DDZ_VAR_PASS_COUNT 并发出 DDZ_PLAYER_PASSED 事件。连续 PASS 触发回合重置的逻辑由 PassEffect 负责,此处只更新计数。

callLandlordHandler(CALL_LANDLORD) ​

ts
type: CALL_LANDLORD_ACTION
execute(action: CallLandlordAction, ctx: GameContext): ActionHandlerResult

行为:

  • call=true:当前玩家成为地主候选人(写入 DDZ_VAR_LANDLORD_CANDIDATE_ID)。最终地主身份、农民列表与阶段切换由后续派发的 REVEAL_BOTTOM_CARDS 完成。
  • call=false:将玩家加入 DDZ_VAR_BIDDER_PASSED,并推进 DDZ_VAR_BIDDER_INDEX。
  • 始终发出 DDZ_LANDLORD_CALLED 事件。

robLandlordHandler(ROB_LANDLORD) ​

ts
type: ROB_LANDLORD_ACTION
execute(action: RobLandlordAction, ctx: GameContext): ActionHandlerResult

行为:

  • rob=true:从上一候选人手中夺取地主候选人身份(写入 DDZ_VAR_LANDLORD_CANDIDATE_ID);倍率 ×2(抢地主翻倍规则)。
  • rob=false:保持当前候选人不变,由副作用通过 REVEAL_BOTTOM_CARDS 终局。
  • 始终发出 DDZ_LANDLORD_ROBBED 事件(payload 复用 DdzLandlordCalledPayload,call 字段即 rob 值)。

Phase 5 默认叫分流程不触发此 Action,仅用于前向兼容更完整的叫分逻辑。

revealBottomCardsHandler(REVEAL_BOTTOM_CARDS) ​

ts
type: REVEAL_BOTTOM_CARDS_ACTION
execute(_action: RevealBottomCardsAction, ctx: GameContext): ActionHandlerResult

系统派发(无 playerId),故被 PhaseRule / PlayerTurnRule 豁免,同时会被 ActionHistory 记录以便 ReplayEngine 忠实重放。流程:

  1. 读取 DDZ_VAR_LANDLORD_CANDIDATE_ID 作为最终地主(缺失时抛错 REVEAL_BOTTOM_CARDS: no landlord candidate set);
  2. 按座位顺序推导另两位玩家为农民;
  3. 将 3 张底牌从 ddz-bottom 移入地主手牌(确保 zone 存在);
  4. 终局写入 DDZ_VAR_LANDLORD_ID / DDZ_VAR_FARMER_IDS,切换 DDZ_VAR_PHASE = PLAYING,重置 DDZ_VAR_PASS_COUNT = 0,清除上轮 DDZ_VAR_LAST_COMBINATION / DDZ_VAR_LAST_PLAYER_ID,并将 currentPlayerId 设为地主;
  5. 发出 DDZ_LANDLORD_ASSIGNED 与 DDZ_BOTTOM_CARDS_REVEALED 事件(含各家手牌数快照)。

注册的 EventHandler ​

handleGameStarted(GAME_STARTED) ​

ts
{ type: 'GAME_STARTED', handle: handleGameStarted }

洗牌已在 setup 完成,此处负责发牌并进入 BIDDING 阶段:

  1. 按座位顺序确保每位玩家拥有手牌 zone;
  2. 每位玩家发 DOUDIZHU_HAND_SIZE(17)张,剩 DOUDIZHU_BOTTOM_SIZE(3)张进入 ddz-bottom;
  3. 记录底牌 id 到 DDZ_VAR_BOTTOM_CARD_IDS;
  4. 重置叫分相关变量并切换 DDZ_VAR_PHASE = BIDDING;首位叫分人为最低座位号玩家(TurnPlugin 的 GAME_STARTED 处理已把 currentPlayerId 设到首位);
  5. 发出 DDZ_CARDS_DEALT 与 DDZ_GAME_STARTED。

handleCardsPlayed(CARD_PLAYED) ​

ts
{ type: 'CARD_PLAYED', handle: handleCardsPlayed }

若出牌玩家手牌已空则宣告胜利(调用 declareWin),否则派发 END_TURN 推进轮次。胜负判定与终局事件由 PlayCardsEffect.declareWin 完成。

handleLandlordCalled(DDZ_LANDLORD_CALLED) ​

ts
{ type: DDZ_LANDLORD_CALLED, handle: handleLandlordCalled }

委托 CallLandlordEffect.applyCallLandlordSideEffects 处理叫 / 不叫的副作用。

handleRobLandlord(DDZ_LANDLORD_ROBBED) ​

ts
{ type: DDZ_LANDLORD_ROBBED, handle: handleRobLandlord }

委托 RobLandlordEffect.applyRobLandlordSideEffects 处理抢 / 不抢的副作用。

handlePlayerPassed(DDZ_PLAYER_PASSED) ​

ts
{ type: DDZ_PLAYER_PASSED, handle: handlePlayerPassed }

委托 PassEffect.applyPassSideEffects 处理过牌后的轮次推进 / 重置。

重新导出的符号 ​

DoudizhuPlugin.ts 末尾重新导出以下符号,供调用方(测试、示例)使用:

符号来源说明
DoudizhuCardcards/DoudizhuCard.ts斗地主卡牌类型(Card<DoudizhuCardData>)
asDoudizhuCardcards/DoudizhuCard.ts将通用 Card 收窄为 DoudizhuCard 的类型守卫
DoudizhuPhasestate/DoudizhuPhase.ts斗地主阶段枚举
DoudizhuTeamstate/DoudizhuTeam.ts斗地主队伍枚举
teamOfstate/DoudizhuState.ts根据玩家 id 解析所属队伍

示例 ​

ts
import {
  GameEngine,
  TurnPlugin,
  DrawPlugin,
  DoudizhuPlugin,
  DoudizhuPhase,
  getDoudizhuPhase,
  getLandlordId,
  getMultiplier,
  createCallLandlordAction
} from 'decklet/doudizhu'

const engine = new GameEngine({ seed: 2024 })
engine.use(TurnPlugin).use(DrawPlugin).use(DoudizhuPlugin)
engine.createGame({
  players: [
    { id: 'p1', name: 'Alice', seat: 1 },
    { id: 'p2', name: 'Bob', seat: 2 },
    { id: 'p3', name: 'Carol', seat: 3 }
  ]
})
engine.start()

// setup 后处于 DEALING,start() 派发 GAME_STARTED → 进入 BIDDING
console.log(getDoudizhuPhase(engine.getState()) === DoudizhuPhase.BIDDING) // true

// 首位叫分人叫地主 → 副作用派发 REVEAL_BOTTOM_CARDS → 进入 PLAYING
engine.dispatch(createCallLandlordAction('p1', true))
console.log(getDoudizhuPhase(engine.getState()) === DoudizhuPhase.PLAYING) // true
console.log(getLandlordId(engine.getState())) // 'p1'
console.log(getMultiplier(engine.getState())) // 1

注意事项 ​

  • setup 必须幂等:回放(ReplayEngine)会再次调用 setup,所有 zone 与卡牌的创建均带 if (!exists) 守卫。
  • 所有内部 ActionHandler 与 EventHandler 都是 DoudizhuPlugin.ts 文件内的局部常量 / 函数,未对外导出——调用方只能通过 engine.use(DoudizhuPlugin) 装配后由 Engine 调度,无法直接引用。
  • REVEAL_BOTTOM_CARDS 为系统派发的 Action(无 playerId),由 CallLandlord / RobLandlord 的副作用在候选人确定后派发,不应由客户端直接派发。