Skip to content

Events ​

斗地主专用事件类型与对应 Payload。所有事件常量统一使用 DDZ_ 前缀以避免与 Plugin 复用的核心事件(GAME_STARTED、CARD_PLAYED、GAME_WON 等)冲突。

概述 ​

斗地主事件由 DoudizhuPlugin 中的 Action 处理器与事件处理器发出。共 13 个事件常量与 11 个 Payload 接口。事件按发出时机可分为:

  • 开局阶段:DDZ_GAME_STARTED、DDZ_CARDS_DEALT
  • 叫地主阶段:DDZ_LANDLORD_CALLED、DDZ_LANDLORD_ROBBED、DDZ_LANDLORD_ASSIGNED、DDZ_BOTTOM_CARDS_REVEALED
  • 出牌阶段:DDZ_CARDS_PLAYED、DDZ_PLAYER_PASSED、DDZ_ROUND_RESET、DDZ_BOMB_PLAYED、DDZ_ROCKET_PLAYED
  • 结束阶段:DDZ_PLAYER_WON、DDZ_GAME_FINISHED

事件常量一览 ​

常量字面值Payload 类型发出时机
DDZ_GAME_STARTED'DDZ_GAME_STARTED'{ playerIds: string[] }handleGameStarted 完成发牌后
DDZ_CARDS_DEALT'DDZ_CARDS_DEALT'DdzCardsDealtPayload发牌完成(含 redeal 重新发牌)
DDZ_LANDLORD_CALLED'DDZ_LANDLORD_CALLED'DdzLandlordCalledPayloadcallLandlordHandler 处理 CALL_LANDLORD 后
DDZ_LANDLORD_ROBBED'DDZ_LANDLORD_ROBBED'DdzLandlordCalledPayloadrobLandlordHandler 处理 ROB_LANDLORD 后
DDZ_LANDLORD_ASSIGNED'DDZ_LANDLORD_ASSIGNED'DdzLandlordAssignedPayloadrevealBottomCardsHandler 完成地主身份写入后
DDZ_BOTTOM_CARDS_REVEALED'DDZ_BOTTOM_CARDS_REVEALED'DdzBottomCardsRevealedPayload底牌移入地主手牌后(含各家手牌数快照)
DDZ_CARDS_PLAYED'DDZ_CARDS_PLAYED'DdzCardsPlayedPayloadplayCardsHandler 完成出牌后
DDZ_PLAYER_PASSED'DDZ_PLAYER_PASSED'DdzPlayerPassedPayloadpassHandler 累加 passCount 后
DDZ_ROUND_RESET'DDZ_ROUND_RESET'DdzRoundResetPayload连续过牌触发回合重置后(PassEffect)
DDZ_BOMB_PLAYED'DDZ_BOMB_PLAYED'DdzBombPlayedPayload出炸弹后(倍率已 ×2)
DDZ_ROCKET_PLAYED'DDZ_ROCKET_PLAYED'DdzRocketPlayedPayload出火箭后(倍率已 ×2)
DDZ_PLAYER_WON'DDZ_PLAYER_WON'DdzPlayerWonPayload玩家清空手牌确定获胜方后
DDZ_GAME_FINISHED'DDZ_GAME_FINISHED'DdzGameFinishedPayload游戏结束(结构与 DdzPlayerWonPayload 一致)

开局阶段 ​

DDZ_GAME_STARTED ​

ts
const DDZ_GAME_STARTED = 'DDZ_GAME_STARTED' as const

发牌完成、阶段切换到 BIDDING 后由 handleGameStarted 发出。

Payload ​

无独立 Payload 接口——直接使用字面量对象 { playerIds: string[] }:

字段类型说明
playerIdsstring[]按座位顺序排列的所有玩家 id

示例 ​

ts
engine.subscribeAll((e) => {
  if (e.type === DDZ_GAME_STARTED) {
    const p = e.payload as { playerIds: string[] }
    console.log('players:', p.playerIds) // ['p1', 'p2', 'p3']
  }
})

DDZ_CARDS_DEALT ​

ts
const DDZ_CARDS_DEALT = 'DDZ_CARDS_DEALT' as const

发牌完成时发出(含初始发牌与 redeal 重新发牌)。

DdzCardsDealtPayload ​

ts
interface DdzCardsDealtPayload {
  handSize: number
  bottomSize: number
  bottomCardIds: string[]
}
字段类型说明
handSizenumber每位玩家手牌张数(17)
bottomSizenumber底牌张数(3)
bottomCardIdsstring[]3 张底牌 id 列表

示例 ​

ts
if (e.type === DDZ_CARDS_DEALT) {
  const p = e.payload as DdzCardsDealtPayload
  console.log(`hand: ${p.handSize}, bottom: ${p.bottomSize}`)
  console.log('bottom card ids:', p.bottomCardIds)
}

叫地主阶段 ​

DDZ_LANDLORD_CALLED ​

ts
const DDZ_LANDLORD_CALLED = 'DDZ_LANDLORD_CALLED' as const

callLandlordHandler 处理 CALL_LANDLORD 后发出。

DdzLandlordCalledPayload ​

ts
interface DdzLandlordCalledPayload {
  playerId: string
  call: boolean
}
字段类型说明
playerIdstring执行叫地主的玩家 id
callbooleantrue 表示叫地主,false 表示不叫

示例 ​

ts
if (e.type === DDZ_LANDLORD_CALLED) {
  const p = e.payload as DdzLandlordCalledPayload
  console.log(`${p.playerId} ${p.call ? 'called' : 'passed'} landlord`)
}

DDZ_LANDLORD_ROBBED ​

ts
const DDZ_LANDLORD_ROBBED = 'DDZ_LANDLORD_ROBBED' as const

robLandlordHandler 处理 ROB_LANDLORD 后发出。复用 DdzLandlordCalledPayload,其中 call 字段即 rob 值。

Payload ​

复用 DdzLandlordCalledPayload:

字段类型说明
playerIdstring抢地主的玩家 id
callboolean即 rob 值:true 表示抢地主,false 表示不抢

DDZ_LANDLORD_ASSIGNED ​

ts
const DDZ_LANDLORD_ASSIGNED = 'DDZ_LANDLORD_ASSIGNED' as const

revealBottomCardsHandler 完成地主身份最终写入后发出。

DdzLandlordAssignedPayload ​

ts
interface DdzLandlordAssignedPayload {
  landlordId: string
  farmerIds: string[]
  bottomCardIds: string[]
}
字段类型说明
landlordIdstring最终地主玩家 id
farmerIdsstring[]农民玩家 id 列表(2 位)
bottomCardIdsstring[]3 张底牌 id 列表

示例 ​

ts
if (e.type === DDZ_LANDLORD_ASSIGNED) {
  const p = e.payload as DdzLandlordAssignedPayload
  console.log(`landlord: ${p.landlordId}, farmers: ${p.farmerIds.join(', ')}`)
}

DDZ_BOTTOM_CARDS_REVEALED ​

ts
const DDZ_BOTTOM_CARDS_REVEALED = 'DDZ_BOTTOM_CARDS_REVEALED' as const

底牌移入地主手牌后由 revealBottomCardsHandler 发出,附带各家手牌数快照。

DdzBottomCardsRevealedPayload ​

ts
interface DdzBottomCardsRevealedPayload {
  landlordId: string
  bottomCardIds: string[]
  /** 揭牌完成后,所有玩家(含地主)的手牌数,key 为玩家 id。 */
  handSizesAfter: Record<string, number>
}
字段类型说明
landlordIdstring地主玩家 id
bottomCardIdsstring[]3 张底牌 id 列表
handSizesAfterRecord<string, number>揭牌完成后所有玩家(含地主)的手牌数,key 为玩家 id。地主应持 20 张,农民各 17 张

示例 ​

ts
if (e.type === DDZ_BOTTOM_CARDS_REVEALED) {
  const p = e.payload as DdzBottomCardsRevealedPayload
  console.log(`landlord ${p.landlordId} now has ${p.handSizesAfter[p.landlordId]} cards`)
}

出牌阶段 ​

DDZ_CARDS_PLAYED ​

ts
const DDZ_CARDS_PLAYED = 'DDZ_CARDS_PLAYED' as const

playCardsHandler 完成出牌后发出,含识别出的牌型与主点数。

DdzCardsPlayedPayload ​

ts
interface DdzCardsPlayedPayload {
  playerId: string
  cardIds: string[]
  /** 牌型字面量,未识别时为 'UNKNOWN'。 */
  combinationType: string
  /** 牌型主点数(如三带一取三张的点数),无主点数时为 0。 */
  mainRank: number
}
字段类型说明
playerIdstring出牌玩家 id
cardIdsstring[]打出的卡牌 id 列表
combinationTypestring牌型字面量(如 'SINGLE'、'PAIR'、'BOMB'),未识别时为 'UNKNOWN'
mainRanknumber牌型主点数(如三带一取三张的点数),无主点数时为 0

示例 ​

ts
if (e.type === DDZ_CARDS_PLAYED) {
  const p = e.payload as DdzCardsPlayedPayload
  console.log(`${p.playerId} played ${p.cardIds.length} cards, type=${p.combinationType}, rank=${p.mainRank}`)
}

注意事项 ​

playCardsHandler 在 DDZ_VAR_PENDING_COMBINATION 缺失(Rule 被旁路场景)时,combinationType 为 'UNKNOWN'、mainRank 为 0——但此时处理器不会更新 lastCombination 也不会发出 BOMB / ROCKET 倍率事件。

DDZ_PLAYER_PASSED ​

ts
const DDZ_PLAYER_PASSED = 'DDZ_PLAYER_PASSED' as const

passHandler 累加 passCount 后发出。

DdzPlayerPassedPayload ​

ts
interface DdzPlayerPassedPayload {
  playerId: string
  passCount: number
}
字段类型说明
playerIdstring过牌玩家 id
passCountnumber本轮累计过牌次数(含本次)

DDZ_ROUND_RESET ​

ts
const DDZ_ROUND_RESET = 'DDZ_ROUND_RESET' as const

连续过牌触发回合重置后由 PassEffect.applyPassSideEffects 发出。

DdzRoundResetPayload ​

ts
interface DdzRoundResetPayload {
  /** 新回合首位出牌玩家 id(通常为上一手实际出牌者)。 */
  starterId: string
}
字段类型说明
starterIdstring新回合首位出牌玩家 id(通常为上一手实际出牌者,无则取当前过牌玩家)

发出条件 ​

当 passCount >= N - 1(即另外两位玩家都过牌)时触发。事件 payload 的 starterId 即事件的 playerId 字段。

DDZ_BOMB_PLAYED ​

ts
const DDZ_BOMB_PLAYED = 'DDZ_BOMB_PLAYED' as const

玩家打出炸弹后发出(倍率已 ×2)。

DdzBombPlayedPayload ​

ts
interface DdzBombPlayedPayload {
  playerId: string
  mainRank: number
  multiplier: number
}
字段类型说明
playerIdstring出炸弹玩家 id
mainRanknumber炸弹四张点数
multipliernumber倍率已 ×2 后的当前倍率

DDZ_ROCKET_PLAYED ​

ts
const DDZ_ROCKET_PLAYED = 'DDZ_ROCKET_PLAYED' as const

玩家打出火箭(双王)后发出(倍率已 ×2)。

DdzRocketPlayedPayload ​

ts
interface DdzRocketPlayedPayload {
  playerId: string
  multiplier: number
}
字段类型说明
playerIdstring出火箭玩家 id
multipliernumber倍率已 ×2 后的当前倍率

结束阶段 ​

DDZ_PLAYER_WON ​

ts
const DDZ_PLAYER_WON = 'DDZ_PLAYER_WON' as const

玩家清空手牌、确定获胜方后由 PlayCardsEffect.declareWin 发出。

DdzPlayerWonPayload ​

ts
interface DdzPlayerWonPayload {
  winnerIds: string[]
  /** 获胜队伍:地主单独获胜为 'LANDLORD',任一农民清空手牌为 'FARMERS'。 */
  winnerTeam: 'LANDLORD' | 'FARMERS'
  multiplier: number
}
字段类型说明
winnerIdsstring[]获胜队伍全部玩家 id(地主胜出仅 1 人,农民胜出 2 人)
winnerTeam'LANDLORD' | 'FARMERS'获胜队伍:地主单独获胜为 'LANDLORD',任一农民清空手牌为 'FARMERS'
multipliernumber最终倍率

示例 ​

ts
if (e.type === DDZ_PLAYER_WON) {
  const p = e.payload as DdzPlayerWonPayload
  console.log(`winners: ${p.winnerIds.join(', ')} (${p.winnerTeam}), ×${p.multiplier}`)
}

DDZ_GAME_FINISHED ​

ts
const DDZ_GAME_FINISHED = 'DDZ_GAME_FINISHED' as const

游戏结束。结构与 DdzPlayerWonPayload 一致。

DdzGameFinishedPayload ​

ts
interface DdzGameFinishedPayload {
  winnerIds: string[]
  winnerTeam: 'LANDLORD' | 'FARMERS'
  multiplier: number
}
字段类型说明
winnerIdsstring[]获胜队伍全部玩家 id
winnerTeam'LANDLORD' | 'FARMERS'获胜队伍
multipliernumber最终倍率

与核心事件的关系 ​

DoudizhuPlugin 还会发出 / 监听核心事件(不带 DDZ_ 前缀):

核心事件角色
GAME_STARTED监听——触发 handleGameStarted 发牌
CARD_PLAYED监听——触发 handleCardsPlayed 判定胜负或推进轮次;同时 playCardsHandler 也会发出此事件
GAME_WONdeclareWin 发出——核心胜负事件
GAME_FINISHEDdeclareWin 发出——核心结束事件
TURN_ENDED 等其它由 TurnPlugin / DrawPlugin 等核心插件管理

斗地主专用事件(DDZ_*)通常与对应的核心事件成对发出,例如 CARD_PLAYED 与 DDZ_CARDS_PLAYED、GAME_WON 与 DDZ_PLAYER_WON。订阅者可按需选择监听哪一层。

完整对照表 ​

事件常量Payload 接口字段
DDZ_GAME_STARTED(字面量)playerIds: string[]
DDZ_CARDS_DEALTDdzCardsDealtPayloadhandSize, bottomSize, bottomCardIds
DDZ_LANDLORD_CALLEDDdzLandlordCalledPayloadplayerId, call
DDZ_LANDLORD_ROBBEDDdzLandlordCalledPayload(复用)playerId, call(即 rob)
DDZ_LANDLORD_ASSIGNEDDdzLandlordAssignedPayloadlandlordId, farmerIds, bottomCardIds
DDZ_BOTTOM_CARDS_REVEALEDDdzBottomCardsRevealedPayloadlandlordId, bottomCardIds, handSizesAfter
DDZ_CARDS_PLAYEDDdzCardsPlayedPayloadplayerId, cardIds, combinationType, mainRank
DDZ_PLAYER_PASSEDDdzPlayerPassedPayloadplayerId, passCount
DDZ_ROUND_RESETDdzRoundResetPayloadstarterId
DDZ_BOMB_PLAYEDDdzBombPlayedPayloadplayerId, mainRank, multiplier
DDZ_ROCKET_PLAYEDDdzRocketPlayedPayloadplayerId, multiplier
DDZ_PLAYER_WONDdzPlayerWonPayloadwinnerIds, winnerTeam, multiplier
DDZ_GAME_FINISHEDDdzGameFinishedPayloadwinnerIds, winnerTeam, multiplier

示例:完整事件订阅 ​

ts
import {
  GameEngine,
  TurnPlugin,
  DrawPlugin,
  DoudizhuPlugin,
  createCallLandlordAction,
  DDZ_GAME_STARTED,
  DDZ_CARDS_DEALT,
  DDZ_LANDLORD_ASSIGNED,
  DDZ_BOTTOM_CARDS_REVEALED,
  DDZ_CARDS_PLAYED,
  DDZ_PLAYER_PASSED,
  DDZ_BOMB_PLAYED,
  DDZ_ROCKET_PLAYED,
  DDZ_PLAYER_WON,
  DDZ_GAME_FINISHED,
  asDoudizhuCard,
  doudizhuCardFace,
  type DdzCardsPlayedPayload,
  type DdzPlayerWonPayload
} 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.subscribeAll((e) => {
  switch (e.type) {
    case DDZ_GAME_STARTED:
      console.log('game started')
      break
    case DDZ_CARDS_DEALT:
      console.log('cards dealt')
      break
    case DDZ_LANDLORD_ASSIGNED: {
      const p = e.payload as { landlordId: string; farmerIds: string[] }
      console.log(`landlord: ${p.landlordId}`)
      break
    }
    case DDZ_BOTTOM_CARDS_REVEALED: {
      const p = e.payload as { bottomCardIds: string[] }
      const faces = p.bottomCardIds
        .map((id) => doudizhuCardFace(asDoudizhuCard(engine.getState().cards[id])!))
        .join(' ')
      console.log(`bottom: ${faces}`)
      break
    }
    case DDZ_CARDS_PLAYED: {
      const p = e.payload as DdzCardsPlayedPayload
      console.log(`${p.playerId} played ${p.combinationType}`)
      break
    }
    case DDZ_BOMB_PLAYED:
      console.log('BOMB!')
      break
    case DDZ_ROCKET_PLAYED:
      console.log('ROCKET!')
      break
    case DDZ_PLAYER_WON: {
      const p = e.payload as DdzPlayerWonPayload
      console.log(`winner: ${p.winnerIds.join(',')} (${p.winnerTeam}) ×${p.multiplier}`)
      break
    }
    case DDZ_GAME_FINISHED:
      console.log('finished')
      break
  }
})

engine.start()
engine.dispatch(createCallLandlordAction('p1', true))

注意事项 ​

  • 所有事件常量均使用 as const 断言(如 'DDZ_GAME_STARTED' as const),可作为字面量类型在 EventBus 的 type 字段中使用。
  • DDZ_LANDLORD_ROBBED 复用 DdzLandlordCalledPayload——其 call 字段语义在 rob 场景下即"是否抢地主"。handleRobLandlord 事件处理器通过 payload.call === true 判断 rob 值。
  • DDZ_GAME_STARTED 没有独立的 Payload 接口导出,直接使用字面量对象 { playerIds: string[] }。
  • DDZ_CARDS_DEALT 在两种场景下发出:初始 handleGameStarted 发牌,以及 CallLandlordEffect.redeal() 重新洗牌发牌(所有人不叫时触发)。
  • 倍率事件(DDZ_BOMB_PLAYED / DDZ_ROCKET_PLAYED)中 multiplier 字段为倍率已 ×2 后的当前值;DDZ_PLAYER_WON / DDZ_GAME_FINISHED 中的 multiplier 为最终倍率。
  • 所有 DDZ_* 事件常量与 Ddz*Payload 类型均在 src/index.ts 中重新导出,调用方可直接从包入口导入。