# 成都麻将功能整理 本文基于当前项目 `mahjong-server` 的实际代码实现整理,目的是给前端或测试同学做逐项对照测试。这里写的是“当前代码已经实现/暴露出来的功能”,不是传统成都麻将的完整规则说明。 ## 1. 当前玩法范围 ### 1.1 当前后端只支持成都麻将 - 创建房间时如果不传 `game_type`,默认就是 `chengdu` - 如果传入其他玩法,服务端直接返回错误:`only chengdu mahjong is supported currently` - 虽然引擎里预留了 `xueliu`、`hongzhong` 的规则工厂,但房间服务层目前只允许创建 `chengdu` ### 1.2 当前牌集范围 - 只使用三门牌:万(`W`)、条(`T`)、筒(`B`) - 每门 `1-9` 各 4 张,共 `108` 张 - 当前不带字牌、风牌、箭牌、花牌 - 成都玩法配置里明确 `HasHongZhong() == false` ## 2. 房间功能 ### 2.1 创建房间 HTTP 接口: - `POST /api/v1/game/mahjong/room/create` 请求体: ```json { "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` 请求体: ```json { "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 消息结构大致如下: ```json { "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)` 当前只设置了 `Type` 和 `PlayerID` - 没有把 `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 人基础发牌和回合流转 - 已具备碰/杠/胡/过等核心动作 - 已具备部分成都胡型与番数计算 - 但尚未实现完整成都特色流程与结算 如果你是拿它和“标准成都麻将产品需求”对照,当前更像“成都麻将基础可玩内核 + 房间联机骨架”,还不是完整商用品规。