Files
mahjong-web/chengdu-mahjong-features.md
wsy182 be9bd8c76d feat(game): 添加成都麻将房间配置和桌面牌面显示功能
- 在房间创建接口中添加总回合数配置选项
- 实现桌面弃牌区域的可视化展示,区分各玩家的弃牌和组合
- 添加缺门标识显示,帮助玩家识别缺门牌组起始位置
- 优化牌面操作状态管理,增加弃牌等待状态和超时处理机制
- 更新样式布局适配新的桌面牌面区域,调整墙体和桌面对齐方式
- 修复多处牌面状态同步问题,确保游戏流程中的界面一致性
2026-03-29 23:56:32 +08:00

14 KiB

成都麻将功能整理

本文基于当前项目 mahjong-server 的实际代码实现整理,目的是给前端或测试同学做逐项对照测试。这里写的是“当前代码已经实现/暴露出来的功能”,不是传统成都麻将的完整规则说明。

1. 当前玩法范围

1.1 当前后端只支持成都麻将

  • 创建房间时如果不传 game_type,默认就是 chengdu
  • 如果传入其他玩法,服务端直接返回错误:only chengdu mahjong is supported currently
  • 虽然引擎里预留了 xueliuhongzhong 的规则工厂,但房间服务层目前只允许创建 chengdu

1.2 当前牌集范围

  • 只使用三门牌:万(W)、条(T)、筒(B)
  • 每门 1-9 各 4 张,共 108
  • 当前不带字牌、风牌、箭牌、花牌
  • 成都玩法配置里明确 HasHongZhong() == false

2. 房间功能

2.1 创建房间

HTTP 接口:

  • POST /api/v1/game/mahjong/room/create

请求体:

{
  "name": "房间名",
  "game_type": "chengdu",
  "total_rounds": 8,
  "max_players": 4
}

实现规则:

  • 必须登录鉴权
  • 创建者自动成为房主
  • 创建者自动进入房间,座位索引固定为 0
  • max_players 为空时,使用配置默认值
  • 当前只允许 4 人房
  • 房间名为空时,自动生成类似 成都麻将-xxxxxxxx
  • 初始房间状态为 waiting

返回重点字段:

  • room_id
  • name
  • game_type
  • owner_id
  • max_players
  • player_count
  • players
  • status

2.2 房间列表

HTTP 接口:

  • GET /api/v1/game/mahjong/room/list

查询参数:

  • status
  • game_type
  • page
  • size

实现规则:

  • 需要登录
  • 支持按房间状态筛选
  • 支持按玩法类型筛选
  • 支持分页
  • size 超过配置上限会被裁剪

2.3 加入房间

HTTP 接口:

  • POST /api/v1/game/mahjong/room/join

请求体:

{
  "room_id": "xxx"
}

实现规则:

  • 需要登录
  • 只能加入 waiting 状态房间
  • 人数满 4 人后不可继续加入
  • 已在房间内再次加入,不会重复加人
  • 如果玩家名之前为空,再次进入时会补写玩家名

加入房间成功后,除了 HTTP 返回房间摘要,还会额外推送 WS 事件:

  • room_joined
  • room_member_joined(仅首次加入触发)
  • room_player_update

2.4 查询房间详情

HTTP 接口:

  • GET /api/v1/game/mahjong/room/:room_id

实现规则:

  • 需要登录
  • 必须是房间内玩家才能看该房间详情
  • 返回 summary + public

其中 public 可用于前端牌桌展示,包含:

  • 当前阶段 phase
  • 当前轮到谁 current_turn_player
  • 是否必须先摸牌 need_draw
  • 最近弃牌人 last_discard_by
  • 最近弃牌 last_discard_tile
  • 牌墙剩余数 wall_count
  • 本局胡牌玩家 winners
  • 本局分数 scores
  • 当前响应窗口 pending_claim
  • 每个玩家的公开状态

2.5 离开房间

离开房间是走 WS 动作,不是 HTTP 接口。

实现规则:

  • 只有 waiting 状态下允许离开
  • playing 状态下离开会失败
  • 普通玩家离开后,剩余玩家座位索引会重新整理
  • 房主离开后,房主身份自动转移给当前列表中的第一个玩家
  • 如果最后一个玩家离开,房间会直接删除

对应事件:

  • room_left
  • room_member_left
  • room_owner_changed(房主变更时)
  • room_player_update
  • room_closed(最后一人离开时)

3. 开局和基础对局流程

3.1 开始游戏

开始游戏走 WS 动作:

  • type = "start_game"

实现规则:

  • 只有房主能开始
  • 只有满 4 人时才能开始
  • 房间状态从 waiting 变成 playing
  • 内部固定按当前房间玩家顺序创建 4 个玩家
  • 庄家固定为座位 0
  • 发牌后庄家 14 张,其余玩家 13 张

开始后会推送:

  • 广播 room_state
  • 给每个玩家单独推送 my_hand

3.2 基础轮转规则

  • 庄家开局先打牌,不需要先摸
  • 非当前操作者不能操作
  • 非庄家进入自己回合后,必须先 draw 才能 discard
  • 每次打牌后,系统检查其他三家是否可 hu / peng / gang
  • 如果没人可响应,则轮到下家摸牌
  • 如果所有可响应玩家都 pass,也轮到出牌者下家摸牌

3.3 牌墙耗尽

  • 当需要摸牌但牌墙为空时,直接流局结束
  • 打完牌后如果后续无人响应,且牌墙为空,也直接结束
  • 摸到最后一张牌后,如果牌墙已空且无人可胡,也会结束
  • 结束后阶段 phase = over
  • 房间状态同步变为 finished

4. 对局动作功能

当前对局动作都通过 WS -> ws-gateway -> grpc -> mahjong-game 转发执行。

支持的动作类型:

  • draw
  • discard
  • peng
  • gang
  • hu
  • pass

4.1 摸牌 draw

触发条件:

  • 必须轮到当前玩家
  • 当前状态必须 need_draw = true

效果:

  • 从牌墙头部摸 1 张
  • 手牌加 1
  • need_draw = false
  • 记录最近摸牌玩家
  • 如果是杠后补牌,会记录 LastDrawFromGang = true
  • 如果摸到的是最后一张,会记录 LastDrawIsLastTile = true

4.2 出牌 discard

触发条件:

  • 必须轮到当前玩家
  • 当前不能处于“必须先摸牌”的状态
  • 必须指定要打出的牌,并且该牌确实在玩家手里

效果:

  • 从手牌移除该张牌
  • 追加到玩家弃牌区 out_tiles
  • 记录 last_discard_by
  • 记录 last_discard_tile
  • 然后生成一个 pending_claim 响应窗口,给其他玩家判断能否 hu / peng / gang

4.3 碰牌 peng

触发条件:

  • 当前必须存在 pending_claim
  • 该玩家必须在响应窗口中,并且 CanPeng = true
  • 玩家手里要有与目标弃牌同牌面的 2 张牌

效果:

  • 手牌移除 2 张同牌
  • 明牌区新增一组 3 张碰牌
  • 当前回合转给碰牌玩家
  • 碰牌后不需要先摸,直接由碰牌玩家出牌

4.4 杠牌 gang

当前只实现两种杠:

  • 明杠:别人打出的牌,你手里正好有 3 张相同牌
  • 暗杠:当前轮到自己时,手里本来就有 4 张相同牌

实现效果:

  • 组成 4 张杠牌放入明牌区
  • 杠后立即补 1 张牌
  • 杠后补牌来自牌墙头部
  • 补牌后当前回合仍归杠牌玩家

当前未见实现:

  • 碰后补杠
  • 抢杠流程本身

说明:

  • 代码里有 QiangGangHu 计分位,但当前动作流程没有真正把“抢杠胡窗口”建出来

4.5 胡牌 hu

支持两类:

  • 自摸胡:当前轮到自己时胡自己的手牌
  • 点炮胡:在 pending_claim 窗口里胡别人刚打出的牌

共同前提:

  • 必须满足成都规则的胡牌校验
  • 必须满足“缺一门”

胡牌后效果:

  • 玩家 HasHu = true
  • winners 增加该玩家
  • 按规则计算本次番数并累计到 scores
  • 成都玩法当前不是血流玩法,所以有人胡后本局直接结束
  • phase = over
  • 房间状态会被服务层更新为 finished

4.6 过牌 pass

触发条件:

  • 当前必须有 pending_claim
  • 该玩家必须在当前响应窗口里

效果:

  • 从当前 pending_claim.options 删除该玩家
  • 如果还有其他玩家待响应,则继续等待
  • 如果所有玩家都过,则轮到弃牌者下家摸牌

5. 成都麻将胡牌判定规则

5.1 缺一门

当前代码里,“缺一门”的实现方式是:

  • 手牌 + 副露中最多只能出现两种花色
  • 如果出现万、条、筒三门同时存在,则不能胡

注意:

  • 这里只做了“结果校验”
  • 没有单独实现“开局定缺”流程
  • 也没有前端/协议层的“选缺门”动作

这意味着当前代码更接近“胡牌时校验是否缺一门”,而不是完整的成都定缺流程。

5.2 支持的胡型

当前规则代码支持以下胡型:

  • 平胡:1
  • 对对胡:3
  • 七对:8
  • 龙七对:16
  • 清一色:8
  • 清对:16
  • 清七对:32
  • 清龙七对:64

5.3 支持的附加番

  • 杠上开花:+2
  • 海底捞月:+2
  • 抢杠胡:+2

5.4 胡牌组合方式

代码可识别:

  • 标准胡:4 组面子 + 1 对将
  • 七对
  • 龙七对

说明:

  • 对对胡通过“全部由刻子/杠 + 1 对将”识别
  • 清一色通过“所有牌同一花色”识别

6. 分数处理方式

当前代码的分数逻辑比较简单,适合先做基础功能联调,不等同于完整成都麻将结算。

已实现:

  • 每次胡牌时,按番型计算一个整数分数
  • 这个整数直接累计到 state.scores[winnerID]
  • 例如平胡就加 1,清龙七对就加 64

当前未实现或未体现:

  • 输家扣分分摊
  • 自摸三家付、点炮单家付
  • 杠分结算
  • 查花猪
  • 查大叫
  • 退税
  • 荒庄查叫
  • 局数/圈风/连庄
  • 最终总结算

因此,当前 scores 更准确说是“本局胡牌番数累计”,不是正式货币化结算结果。

7. 前端可收到的核心事件

7.1 房间相关事件

  • room_joined
  • room_member_joined
  • room_player_update
  • room_left
  • room_member_left
  • room_owner_changed
  • room_closed

7.2 对局相关事件

  • room_state
  • my_hand

7.3 room_state 关键字段

建议重点校验:

  • room_id
  • phase
  • status
  • current_turn_player
  • need_draw
  • last_discard_by
  • last_discard_tile
  • wall_count
  • winners
  • scores
  • pending_claim
  • players

其中每个公开玩家对象包含:

  • player_id
  • hand_count
  • melds
  • out_tiles
  • has_hu

7.4 my_hand 关键字段

  • room_id
  • hand

说明:

  • my_hand 是定向事件,只发给对应玩家
  • 前端应该只用它渲染自己的真实手牌
  • 其他玩家只看 hand_count

8. 建议的 WebSocket 动作格式

从网关协议看,客户端 WS 消息结构大致如下:

{
  "type": "start_game",
  "roomId": "room-xxx",
  "requestId": "req-001",
  "trace_id": "trace-001",
  "payload": {}
}

通用字段:

  • type
  • roomId
  • requestId
  • trace_id
  • payload

房间相关动作:

  • join_room
  • leave_room
  • start_game

对局动作:

  • draw
  • discard
  • peng
  • gang
  • hu
  • pass

9. 当前代码中的已知限制/缺口

这一段非常重要,测试时请单独对照。

9.1 没有“定缺”流程

  • 成都麻将通常有“定缺”
  • 当前代码没有开局选缺门动作
  • 只有胡牌时做“最多两门花色”的校验

9.2 没有“换三张”

  • 项目内未看到换三张流程
  • 没有对应状态、动作、事件

9.3 不是血战到底,也不是血流成河

  • 成都规则实现里 IsBloodFlow() == false
  • 一旦有人胡牌,本局直接结束
  • 不支持“一炮多响后继续”或“胡后继续打”

9.4 结算远未完整

  • 当前只给赢家加一个番数值
  • 没有完整输赢结算模型
  • 没有杠分、查叫、花猪、退税等成都特色结算

9.5 discard 动作当前存在接线缺口

这一点从代码看是高风险问题:

  • 引擎层 discard 必须拿到具体 tile
  • 但服务层 toEngineAction(msg) 当前只设置了 TypePlayerID
  • 没有把 msg.Payload 里的牌对象解析到 types.Action.Tile

这意味着:

  • 如果前端通过现有 WS 通道发送 discard,很可能会因为缺少牌对象而失败
  • 报错大概率是 discard tile is required

建议测试时优先验证这个点。

9.6 抢杠胡只有计分标志,没有完整流程

  • 规则层支持 QiangGangHu 加番
  • 但当前核心流程没有看到“补杠被抢”的动作分支和响应窗口
  • 所以这个番目前更像预留能力,不一定能在真实流程里触发

9.7 优先级仲裁较简化

项目内 engine/README.md 还明确写了后续建议:

  • 需要补充更明确的优先级处理 hu > gang > peng

当前代码行为是:

  • 打牌后把所有可操作选项放进 pending_claim
  • 谁先发起动作、且在 options 中合法,谁就能执行

因此如果多个玩家同时都可响应,前端和测试要特别注意是否存在“先到先得”而不是严格优先级裁决。

10. 建议测试清单

10.1 房间层

  • 创建房间成功,默认玩法为 chengdu
  • chengdu 玩法创建失败
  • 非 4 人房创建失败
  • 房主自动入房
  • 房间列表分页正常
  • 房间未满时可加入
  • 房间满员后加入失败
  • 对局开始后不能再加入
  • 房间内玩家可查看详情
  • 非房间内玩家查询详情失败
  • 普通玩家可离开等待中的房间
  • 房主离开后房主转移
  • 最后一人离开后房间关闭
  • 游戏进行中离开房间失败

10.2 开局与轮转

  • 房主才能开始游戏
  • 必须满 4 人才能开始
  • 开局庄家 14 张,其余 13 张
  • 开始后收到 room_state
  • 每位玩家都能收到自己的 my_hand
  • 庄家首回合直接出牌
  • 非庄家必须先摸再打
  • 轮转顺序按座位顺延

10.3 响应动作

  • 弃牌后正确生成 pending_claim
  • 可碰玩家能执行 peng
  • 碰后当前回合归碰牌玩家
  • 手牌与副露数量变化正确
  • 明杠成功后会补牌
  • 暗杠成功后会补牌
  • 所有可响应玩家都 pass 后轮到下家摸牌

10.4 胡牌与流局

  • 平胡可胡且加 1
  • 对对胡可胡且加 3
  • 七对可胡且加 8
  • 龙七对可胡且加 16
  • 清一色可胡且加 8
  • 清对可胡且加 16
  • 清七对可胡且加 32
  • 清龙七对可胡且加 64
  • 杠上开花附加 2
  • 海底捞月附加 2
  • 三门齐全时不能胡
  • 点炮胡后本局直接结束
  • 自摸后本局直接结束
  • 牌墙为空时本局流局结束

10.5 高风险专项

  • WS discard 是否因未传/未解析 tile 失败
  • 多家同时可响应时,是否符合预期优先级
  • QiangGangHu 是否实际上无法走通

11. 结论

当前项目里的“成都麻将”更准确地说,是一个:

  • 已具备房间管理
  • 已具备 4 人基础发牌和回合流转
  • 已具备碰/杠/胡/过等核心动作
  • 已具备部分成都胡型与番数计算
  • 但尚未实现完整成都特色流程与结算

如果你是拿它和“标准成都麻将产品需求”对照,当前更像“成都麻将基础可玩内核 + 房间联机骨架”,还不是完整商用品规。