DoudizhuPlugin
斗地主游戏插件主入口,装配 Rule / ActionHandler / EventHandler / setup 为一个 GamePlugin,通过 GameContext 与 Engine 交互。
概述
DoudizhuPlugin 是一个 GamePlugin 对象(非类),通过 engine.use(DoudizhuPlugin) 装配到 GameEngine。它负责:
- 在
createGame时创建斗地主所需的三个 zone(deck / bottom / discard)并洗牌生成 54 张牌; - 注册 9 条 Rule(阶段、轮次、牌权、所属权、牌型、PASS、叫 / 抢地主);
- 注册 5 个 ActionHandler(
PLAY_CARDS/PASS/CALL_LANDLORD/ROB_LANDLORD/REVEAL_BOTTOM_CARDS); - 监听 5 个事件(
GAME_STARTED/CARD_PLAYED/DDZ_LANDLORD_CALLED/DDZ_LANDLORD_ROBBED/DDZ_PLAYER_PASSED),将副作用委托给effects/*中的纯函数。
所有 ActionHandler 为纯函数:输入 (action, ctx),输出 { state, events },通过 stateUtils.* 做不可变更新。
插件字段
| 名称 | 类型 | 说明 |
|---|---|---|
id | string | 插件标识符,固定为 'doudizhu' |
name | string | 人类可读名称 'Doudizhu (斗地主)' |
version | string | 插件版本 '1.0.0' |
setup | (ctx: GameContext) => void | createGame 时调用一次(回放会再次执行,故需幂等) |
rules | Rule[] | 注册到 RuleEngine 的规则数组,顺序决定短路顺序 |
actions | ActionHandler[] | 注册到 ActionExecutor 的处理器数组 |
events | { type: string; handle: PluginEventHandler }[] | 注册到 EventBus 的事件处理器数组 |
setup 行为
setup(ctx: GameContext): void在 createGame 时调用一次(回放会再次执行,故需幂等):
- 若不存在则创建三个 zone:
ddz-deck(type:'deck')ddz-bottom(type:'custom')ddz-discard(type:'discard')
- 通过
createShuffledDoudizhuDeck(ctx.random)构建并加入 54 张斗地主卡牌到 deck zone(已存在则跳过); - 初始化所有斗地主专用变量到默认值:
DDZ_VAR_PHASE = DoudizhuPhase.DEALINGDDZ_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 = undefinedDDZ_VAR_PASS_COUNT = 0、DDZ_VAR_MULTIPLIER = 1
- 将斗地主状态不变式
doudizhuStateCheck注册到核心StateValidator(注册名DDZ_STATE_CHECK_NAME = 'doudizhu')。
注册的 Rule
按短路顺序排列:
| 序号 | Rule id | 名称 | 适用 Action |
|---|---|---|---|
| 1 | ddz-game-not-won | Doudizhu Game Not Won | *(全部) |
| 2 | ddz-game-started | Doudizhu Game Started | *(全部) |
| 3 | ddz-phase | Doudizhu Phase | *(全部) |
| 4 | ddz-player-turn | Doudizhu Player Turn | *(全部) |
| 5 | ddz-player-owns-cards | Doudizhu Player Owns Cards | PLAY_CARDS |
| 6 | ddz-combination-valid | Doudizhu Combination Valid | PLAY_CARDS |
| 7 | ddz-pass | Doudizhu Pass | PASS |
| 8 | ddz-call-landlord | Doudizhu Call Landlord | CALL_LANDLORD |
| 9 | ddz-rob-landlord | Doudizhu Rob Landlord | ROB_LANDLORD |
注册顺序决定了 RuleEngine 的短路顺序——阶段、轮次、牌权、所属权、牌型等基础校验必须先于业务规则(叫 / 抢地主)执行。详见 rules.md。
注册的 ActionHandler
playCardsHandler(PLAY_CARDS)
type: PLAY_CARDS_ACTION
execute(action: Action<PlayCardsAction>, ctx: GameContext): ActionHandlerResult行为:
- 将
payload.cardIds中的每张卡牌从玩家手牌 zone 移动到弃牌堆ddz-discard; - 读取
CombinationRule预检阶段写入DDZ_VAR_PENDING_COMBINATION的牌型,避免重复检测;缺失时(说明 Rule 被旁路)则保持 state 不变且不发出任何 events——Engine 会将其视为未产生 events 的 Action; - 命中后更新
DDZ_VAR_LAST_COMBINATION/DDZ_VAR_LAST_PLAYER_ID,并重置DDZ_VAR_PASS_COUNT = 0; - 炸弹 / 火箭触发倍率 ×2,并按需追加
DDZ_BOMB_PLAYED/DDZ_ROCKET_PLAYED事件; - 始终发出核心
CARD_PLAYED事件与斗地主专用DDZ_CARDS_PLAYED事件。
passHandler(PASS)
type: PASS_ACTION
execute(action: PassAction, ctx: GameContext): ActionHandlerResult行为:累加 DDZ_VAR_PASS_COUNT 并发出 DDZ_PLAYER_PASSED 事件。连续 PASS 触发回合重置的逻辑由 PassEffect 负责,此处只更新计数。
callLandlordHandler(CALL_LANDLORD)
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)
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)
type: REVEAL_BOTTOM_CARDS_ACTION
execute(_action: RevealBottomCardsAction, ctx: GameContext): ActionHandlerResult系统派发(无 playerId),故被 PhaseRule / PlayerTurnRule 豁免,同时会被 ActionHistory 记录以便 ReplayEngine 忠实重放。流程:
- 读取
DDZ_VAR_LANDLORD_CANDIDATE_ID作为最终地主(缺失时抛错REVEAL_BOTTOM_CARDS: no landlord candidate set); - 按座位顺序推导另两位玩家为农民;
- 将 3 张底牌从
ddz-bottom移入地主手牌(确保 zone 存在); - 终局写入
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设为地主; - 发出
DDZ_LANDLORD_ASSIGNED与DDZ_BOTTOM_CARDS_REVEALED事件(含各家手牌数快照)。
注册的 EventHandler
handleGameStarted(GAME_STARTED)
{ type: 'GAME_STARTED', handle: handleGameStarted }洗牌已在 setup 完成,此处负责发牌并进入 BIDDING 阶段:
- 按座位顺序确保每位玩家拥有手牌 zone;
- 每位玩家发
DOUDIZHU_HAND_SIZE(17)张,剩DOUDIZHU_BOTTOM_SIZE(3)张进入ddz-bottom; - 记录底牌 id 到
DDZ_VAR_BOTTOM_CARD_IDS; - 重置叫分相关变量并切换
DDZ_VAR_PHASE = BIDDING;首位叫分人为最低座位号玩家(TurnPlugin 的 GAME_STARTED 处理已把currentPlayerId设到首位); - 发出
DDZ_CARDS_DEALT与DDZ_GAME_STARTED。
handleCardsPlayed(CARD_PLAYED)
{ type: 'CARD_PLAYED', handle: handleCardsPlayed }若出牌玩家手牌已空则宣告胜利(调用 declareWin),否则派发 END_TURN 推进轮次。胜负判定与终局事件由 PlayCardsEffect.declareWin 完成。
handleLandlordCalled(DDZ_LANDLORD_CALLED)
{ type: DDZ_LANDLORD_CALLED, handle: handleLandlordCalled }委托 CallLandlordEffect.applyCallLandlordSideEffects 处理叫 / 不叫的副作用。
handleRobLandlord(DDZ_LANDLORD_ROBBED)
{ type: DDZ_LANDLORD_ROBBED, handle: handleRobLandlord }委托 RobLandlordEffect.applyRobLandlordSideEffects 处理抢 / 不抢的副作用。
handlePlayerPassed(DDZ_PLAYER_PASSED)
{ type: DDZ_PLAYER_PASSED, handle: handlePlayerPassed }委托 PassEffect.applyPassSideEffects 处理过牌后的轮次推进 / 重置。
重新导出的符号
DoudizhuPlugin.ts 末尾重新导出以下符号,供调用方(测试、示例)使用:
| 符号 | 来源 | 说明 |
|---|---|---|
DoudizhuCard | cards/DoudizhuCard.ts | 斗地主卡牌类型(Card<DoudizhuCardData>) |
asDoudizhuCard | cards/DoudizhuCard.ts | 将通用 Card 收窄为 DoudizhuCard 的类型守卫 |
DoudizhuPhase | state/DoudizhuPhase.ts | 斗地主阶段枚举 |
DoudizhuTeam | state/DoudizhuTeam.ts | 斗地主队伍枚举 |
teamOf | state/DoudizhuState.ts | 根据玩家 id 解析所属队伍 |
示例
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的副作用在候选人确定后派发,不应由客户端直接派发。