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' | DdzLandlordCalledPayload | callLandlordHandler 处理 CALL_LANDLORD 后 |
DDZ_LANDLORD_ROBBED | 'DDZ_LANDLORD_ROBBED' | DdzLandlordCalledPayload | robLandlordHandler 处理 ROB_LANDLORD 后 |
DDZ_LANDLORD_ASSIGNED | 'DDZ_LANDLORD_ASSIGNED' | DdzLandlordAssignedPayload | revealBottomCardsHandler 完成地主身份写入后 |
DDZ_BOTTOM_CARDS_REVEALED | 'DDZ_BOTTOM_CARDS_REVEALED' | DdzBottomCardsRevealedPayload | 底牌移入地主手牌后(含各家手牌数快照) |
DDZ_CARDS_PLAYED | 'DDZ_CARDS_PLAYED' | DdzCardsPlayedPayload | playCardsHandler 完成出牌后 |
DDZ_PLAYER_PASSED | 'DDZ_PLAYER_PASSED' | DdzPlayerPassedPayload | passHandler 累加 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
const DDZ_GAME_STARTED = 'DDZ_GAME_STARTED' as const发牌完成、阶段切换到 BIDDING 后由 handleGameStarted 发出。
Payload
无独立 Payload 接口——直接使用字面量对象 { playerIds: string[] }:
| 字段 | 类型 | 说明 |
|---|---|---|
playerIds | string[] | 按座位顺序排列的所有玩家 id |
示例
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
const DDZ_CARDS_DEALT = 'DDZ_CARDS_DEALT' as const发牌完成时发出(含初始发牌与 redeal 重新发牌)。
DdzCardsDealtPayload
interface DdzCardsDealtPayload {
handSize: number
bottomSize: number
bottomCardIds: string[]
}| 字段 | 类型 | 说明 |
|---|---|---|
handSize | number | 每位玩家手牌张数(17) |
bottomSize | number | 底牌张数(3) |
bottomCardIds | string[] | 3 张底牌 id 列表 |
示例
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
const DDZ_LANDLORD_CALLED = 'DDZ_LANDLORD_CALLED' as constcallLandlordHandler 处理 CALL_LANDLORD 后发出。
DdzLandlordCalledPayload
interface DdzLandlordCalledPayload {
playerId: string
call: boolean
}| 字段 | 类型 | 说明 |
|---|---|---|
playerId | string | 执行叫地主的玩家 id |
call | boolean | true 表示叫地主,false 表示不叫 |
示例
if (e.type === DDZ_LANDLORD_CALLED) {
const p = e.payload as DdzLandlordCalledPayload
console.log(`${p.playerId} ${p.call ? 'called' : 'passed'} landlord`)
}DDZ_LANDLORD_ROBBED
const DDZ_LANDLORD_ROBBED = 'DDZ_LANDLORD_ROBBED' as constrobLandlordHandler 处理 ROB_LANDLORD 后发出。复用 DdzLandlordCalledPayload,其中 call 字段即 rob 值。
Payload
复用 DdzLandlordCalledPayload:
| 字段 | 类型 | 说明 |
|---|---|---|
playerId | string | 抢地主的玩家 id |
call | boolean | 即 rob 值:true 表示抢地主,false 表示不抢 |
DDZ_LANDLORD_ASSIGNED
const DDZ_LANDLORD_ASSIGNED = 'DDZ_LANDLORD_ASSIGNED' as constrevealBottomCardsHandler 完成地主身份最终写入后发出。
DdzLandlordAssignedPayload
interface DdzLandlordAssignedPayload {
landlordId: string
farmerIds: string[]
bottomCardIds: string[]
}| 字段 | 类型 | 说明 |
|---|---|---|
landlordId | string | 最终地主玩家 id |
farmerIds | string[] | 农民玩家 id 列表(2 位) |
bottomCardIds | string[] | 3 张底牌 id 列表 |
示例
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
const DDZ_BOTTOM_CARDS_REVEALED = 'DDZ_BOTTOM_CARDS_REVEALED' as const底牌移入地主手牌后由 revealBottomCardsHandler 发出,附带各家手牌数快照。
DdzBottomCardsRevealedPayload
interface DdzBottomCardsRevealedPayload {
landlordId: string
bottomCardIds: string[]
/** 揭牌完成后,所有玩家(含地主)的手牌数,key 为玩家 id。 */
handSizesAfter: Record<string, number>
}| 字段 | 类型 | 说明 |
|---|---|---|
landlordId | string | 地主玩家 id |
bottomCardIds | string[] | 3 张底牌 id 列表 |
handSizesAfter | Record<string, number> | 揭牌完成后所有玩家(含地主)的手牌数,key 为玩家 id。地主应持 20 张,农民各 17 张 |
示例
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
const DDZ_CARDS_PLAYED = 'DDZ_CARDS_PLAYED' as constplayCardsHandler 完成出牌后发出,含识别出的牌型与主点数。
DdzCardsPlayedPayload
interface DdzCardsPlayedPayload {
playerId: string
cardIds: string[]
/** 牌型字面量,未识别时为 'UNKNOWN'。 */
combinationType: string
/** 牌型主点数(如三带一取三张的点数),无主点数时为 0。 */
mainRank: number
}| 字段 | 类型 | 说明 |
|---|---|---|
playerId | string | 出牌玩家 id |
cardIds | string[] | 打出的卡牌 id 列表 |
combinationType | string | 牌型字面量(如 'SINGLE'、'PAIR'、'BOMB'),未识别时为 'UNKNOWN' |
mainRank | number | 牌型主点数(如三带一取三张的点数),无主点数时为 0 |
示例
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
const DDZ_PLAYER_PASSED = 'DDZ_PLAYER_PASSED' as constpassHandler 累加 passCount 后发出。
DdzPlayerPassedPayload
interface DdzPlayerPassedPayload {
playerId: string
passCount: number
}| 字段 | 类型 | 说明 |
|---|---|---|
playerId | string | 过牌玩家 id |
passCount | number | 本轮累计过牌次数(含本次) |
DDZ_ROUND_RESET
const DDZ_ROUND_RESET = 'DDZ_ROUND_RESET' as const连续过牌触发回合重置后由 PassEffect.applyPassSideEffects 发出。
DdzRoundResetPayload
interface DdzRoundResetPayload {
/** 新回合首位出牌玩家 id(通常为上一手实际出牌者)。 */
starterId: string
}| 字段 | 类型 | 说明 |
|---|---|---|
starterId | string | 新回合首位出牌玩家 id(通常为上一手实际出牌者,无则取当前过牌玩家) |
发出条件
当 passCount >= N - 1(即另外两位玩家都过牌)时触发。事件 payload 的 starterId 即事件的 playerId 字段。
DDZ_BOMB_PLAYED
const DDZ_BOMB_PLAYED = 'DDZ_BOMB_PLAYED' as const玩家打出炸弹后发出(倍率已 ×2)。
DdzBombPlayedPayload
interface DdzBombPlayedPayload {
playerId: string
mainRank: number
multiplier: number
}| 字段 | 类型 | 说明 |
|---|---|---|
playerId | string | 出炸弹玩家 id |
mainRank | number | 炸弹四张点数 |
multiplier | number | 倍率已 ×2 后的当前倍率 |
DDZ_ROCKET_PLAYED
const DDZ_ROCKET_PLAYED = 'DDZ_ROCKET_PLAYED' as const玩家打出火箭(双王)后发出(倍率已 ×2)。
DdzRocketPlayedPayload
interface DdzRocketPlayedPayload {
playerId: string
multiplier: number
}| 字段 | 类型 | 说明 |
|---|---|---|
playerId | string | 出火箭玩家 id |
multiplier | number | 倍率已 ×2 后的当前倍率 |
结束阶段
DDZ_PLAYER_WON
const DDZ_PLAYER_WON = 'DDZ_PLAYER_WON' as const玩家清空手牌、确定获胜方后由 PlayCardsEffect.declareWin 发出。
DdzPlayerWonPayload
interface DdzPlayerWonPayload {
winnerIds: string[]
/** 获胜队伍:地主单独获胜为 'LANDLORD',任一农民清空手牌为 'FARMERS'。 */
winnerTeam: 'LANDLORD' | 'FARMERS'
multiplier: number
}| 字段 | 类型 | 说明 |
|---|---|---|
winnerIds | string[] | 获胜队伍全部玩家 id(地主胜出仅 1 人,农民胜出 2 人) |
winnerTeam | 'LANDLORD' | 'FARMERS' | 获胜队伍:地主单独获胜为 'LANDLORD',任一农民清空手牌为 'FARMERS' |
multiplier | number | 最终倍率 |
示例
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
const DDZ_GAME_FINISHED = 'DDZ_GAME_FINISHED' as const游戏结束。结构与 DdzPlayerWonPayload 一致。
DdzGameFinishedPayload
interface DdzGameFinishedPayload {
winnerIds: string[]
winnerTeam: 'LANDLORD' | 'FARMERS'
multiplier: number
}| 字段 | 类型 | 说明 |
|---|---|---|
winnerIds | string[] | 获胜队伍全部玩家 id |
winnerTeam | 'LANDLORD' | 'FARMERS' | 获胜队伍 |
multiplier | number | 最终倍率 |
与核心事件的关系
DoudizhuPlugin 还会发出 / 监听核心事件(不带 DDZ_ 前缀):
| 核心事件 | 角色 |
|---|---|
GAME_STARTED | 监听——触发 handleGameStarted 发牌 |
CARD_PLAYED | 监听——触发 handleCardsPlayed 判定胜负或推进轮次;同时 playCardsHandler 也会发出此事件 |
GAME_WON | declareWin 发出——核心胜负事件 |
GAME_FINISHED | declareWin 发出——核心结束事件 |
TURN_ENDED 等其它 | 由 TurnPlugin / DrawPlugin 等核心插件管理 |
斗地主专用事件(DDZ_*)通常与对应的核心事件成对发出,例如 CARD_PLAYED 与 DDZ_CARDS_PLAYED、GAME_WON 与 DDZ_PLAYER_WON。订阅者可按需选择监听哪一层。
完整对照表
| 事件常量 | Payload 接口 | 字段 |
|---|---|---|
DDZ_GAME_STARTED | (字面量) | playerIds: string[] |
DDZ_CARDS_DEALT | DdzCardsDealtPayload | handSize, bottomSize, bottomCardIds |
DDZ_LANDLORD_CALLED | DdzLandlordCalledPayload | playerId, call |
DDZ_LANDLORD_ROBBED | DdzLandlordCalledPayload(复用) | playerId, call(即 rob) |
DDZ_LANDLORD_ASSIGNED | DdzLandlordAssignedPayload | landlordId, farmerIds, bottomCardIds |
DDZ_BOTTOM_CARDS_REVEALED | DdzBottomCardsRevealedPayload | landlordId, bottomCardIds, handSizesAfter |
DDZ_CARDS_PLAYED | DdzCardsPlayedPayload | playerId, cardIds, combinationType, mainRank |
DDZ_PLAYER_PASSED | DdzPlayerPassedPayload | playerId, passCount |
DDZ_ROUND_RESET | DdzRoundResetPayload | starterId |
DDZ_BOMB_PLAYED | DdzBombPlayedPayload | playerId, mainRank, multiplier |
DDZ_ROCKET_PLAYED | DdzRocketPlayedPayload | playerId, multiplier |
DDZ_PLAYER_WON | DdzPlayerWonPayload | winnerIds, winnerTeam, multiplier |
DDZ_GAME_FINISHED | DdzGameFinishedPayload | winnerIds, winnerTeam, multiplier |
示例:完整事件订阅
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中重新导出,调用方可直接从包入口导入。