# 成都麻将功能整理

本文基于当前项目 `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 人基础发牌和回合流转
- 已具备碰/杠/胡/过等核心动作
- 已具备部分成都胡型与番数计算
- 但尚未实现完整成都特色流程与结算

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