feat: integrate platform room entry, UI migration and erqiwang documentation

This commit is contained in:
2026-09-07 21:01:08 +08:00
parent 9c4510697c
commit 30a1b4114c
259 changed files with 84995 additions and 30201 deletions
@@ -0,0 +1,722 @@
# 二七王协议包列表
说明:`data` 均为发送方与接收方之间约定的 JSON 内容;成败判定只看推送包里的 `data.success`。
---
## 0. 通用约定(所有包适用,下文各表不再逐一重复)
### 0.0 术语:底牌 vs 埋牌底牌(先读这一条)
本项目于 2026-08-25 修订了这两个术语(`design.md` §1 已同步,本文亦已改用新称):
| 术语 | 指哪 8 张 | 旧称 |
| --- | --- | --- |
| **底牌** | 发牌时**没有发给玩家**、扣在桌面的 8 张 | ~~暗牌~~ |
| **埋牌底牌** | 庄家**埋牌**时从手里扣下的 8 张 | ~~底牌~~ |
**两批牌的字段名已拆分**(2026-08-25,原先共用 `bottomcards`):
| 字段名 | 含义 | 出现在 |
| --- | --- | --- |
| **`bottomcards`** | **底牌** | `shangzhuang`、`deskinfo.ChooseMain`、`deskinfo.BuryCards` |
| **`burycards`** | **埋牌底牌** | `maipai`、`deskinfo.PushCards` |
| `bottom.cards` | **埋牌底牌** | 结算包的 `bottom` 分组(分组名已表明是抠底相关,字段未改名) |
同一个包里不会同时出现这两个字段。**下发面**:`burycards` 与 `bottomcards`(非 70 分时)都**只发给庄家**,闲家两者皆无——已由 `test/test_leak.js` 的泄露审计覆盖。
### 0.0b 四类「公开信息」术语(都带亮/明字,别读混)
| 术语 | 谁公开给谁 | 给的是什么 | 触发 | 本文相关字段 |
| --- | --- | --- | --- | --- |
| **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后固定主牌达门槛(design §8.2) | `liangpai.cards` |
| **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(design §9) | `seatlist[seat][4]`、`baozhu` |
| **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + **任一**玩家报无主 → **三家均可点、随时可查**(design §9) | `mingpai.others[].zhucards` |
| **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(design §4) | `ancard3s` + `bottomcards` |
一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。
另有一组「底」字族专管发牌留桌的那 8 张:**摸底**(庄家摸起看,仅庄家)、**开底**(70 分时全场看 3 秒)、**查底牌**(对局中随时回看)、**扣底**(末轮翻开埋牌底牌计分)。
(「亮主」不是术语,是前端选主界面的标题文案,与上表无关。)
### 0.0c 算奖 = 冲关 + 傍王
| 术语 | 范围 | 相关字段 |
| --- | --- | --- |
| **冲关** | design §8.1,**只计庄家**。旧称"常规算奖",现统一叫冲关 | `chongguan`(奖数)、`cards`(冲关牌型) |
| **傍王** | design §8.3,可选规则,勾选后**庄闲都算**,每张王 1 奖 | `wang`(王数)、`bangwang`(开关) |
| **算奖** | **上位概念** = 冲关 + 傍王 | `naward`(总奖数 N)、`grade_aw`(算奖得分) |
说「**冲关**」指庄家那一份,说「**算奖**」指两者合计。界面上小局结算显示「冲关分」、按钮叫「冲关牌型」。
> ⚠️ `grade_aw` 是按合计后的 `N` 算出来的,**冲关与傍王的贡献事后无法拆分**。大局结算若要分别显示「冲关分」「傍王分」,须服务端在算钱时就分开代入公式——见前端清单 §7.5 **S-6**。
### 0.1 成败标志 `data.success`
**每一个「服务器 → 客户端」的包,`data` 都必带 `success`**(布尔):
- `success: true` —— 操作成功 / 正常推送。下文各包的字段表描述的都是这种情况,表里不再重复列 `success` 这一行。
- `success: false` —— 操作失败,见 0.2。
前端一律 `if (!data.success)` 判成败,**不看 `status` / `code`,也不写 `status` 兼容兜底**。
### 0.2 失败回包
服务端受理请求时,任一校验不通过都会**回一个失败包**(此前是静默丢弃、前端只能干等倒计时):
- **rpc 与请求包同名**(如 `chupai` 失败回 `chupai`,而不是 `chupai1/2/3`);
- **只回发给请求者**(`conmode`/`fromid` 取自请求包),其他两家收不到;
- `data` 只含 `success: false` 与 `errcode`,无业务字段。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| success | 布尔 | 恒为 `false` |
| errcode | 整数 | 失败原因,见下表 |
`errcode` 取值(服务端 `youle_erqiwang.ERR`,`mod.js`):
| 值 | 名称 | 含义 |
| --- | --- | --- |
| 1 | PLAYER | 玩家/房间/座位校验不通过(平台 `check_player` 返回 null) |
| 2 | NODESK | 牌桌或牌局不存在 |
| 3 | STEP | 当前阶段不允许该操作(如非出牌阶段发 `chupai`) |
| 4 | SEAT | 位置不符,或还没轮到该玩家操作 |
| 5 | PARAM | 参数非法:类型/范围/张数不对、牌不在手上、牌id 重复 |
| 6 | RULE | 规则不允许:叫分未更低、出牌不合法、投降条件不满足、房间模式禁止等 |
> **牌id 列表的入参约束**(`cards` 字段,`maipai`/`chupai`):必须是**非空数组**,元素必须是 `0 ~ 107` 的**整数**牌id,且**互不重复**。字符串形式的数字(如 `"5"`)、小数、越界值、重复值一律按 `PARAM` 拒绝,服务端不做类型兜底转换。
> **例外:`tishi` 成功时不回执**。该包是闲家给对家的主观提示、不含任何对局状态,服务端只转发给对家(design §11),发送者收不到成功包;只有失败才回给发送者。
### 0.3 `countdown` 只是展示用的秒数
多个包带 `countdown`(叫分 / 选主 / 埋牌 / 出牌倒计时,取自 `class.desk.js` 的四个常量)。它**只供客户端显示提醒,服务端不据此做任何事**:
- 服务端**没有**对应的定时器,倒计时归零后**不会**自动叫分 / 选主 / 埋牌 / 出牌,也不判负、不跳过该玩家;
- 轮到谁而谁不操作,牌局就停在该阶段一直等,`step` 与 `playproc` 都不变;
- 这是 design §11 确认过的规则(无超时托管),不是未实现的功能。牌局因此停住时,由玩家走平台的**房间解散**流程收场(平台回调子游戏 `get_disbandRoom` → 解散结算,见 §14)。
因此**客户端不要在倒计时归零时做任何乐观的界面推进**(不要自行跳阶段、不要清控制权),一切仍以收到服务端推送为准。
### 0.4 断线重连包不在此约定内
`deskinfo` 由平台组装在 `pack.data.deskinfo` 下(见文末「断线重连」),`data.success` 由**平台**填写,子游戏不注入。
### 0.5 `roomtype` 房间选项位串
**核对日期:2026-09-07。** 本节描述当前服务端实际编码,依据 [class.config.js](../../class.config.js) 的 `IDX_*`、`RESERVE_LEN`、`parse()`,以及 [test_config.js](../../test/test_config.js)。局数与扣卡由 [class.export.js](../../class.export.js) 的对应接口读取解析结果。
当前新前端发送约定为 **11 位字符串 = 前 5 位选项 + 后 6 位预留扩展位**,后 6 位当前填 `000000`。默认完整值为 `00000000000`。索引从 0 开始,对应 `charAt(0..4)`;前端不得把字符串转换为数字、数组或按界面排列顺序直接拼接。
| 当前传输位索引 | 含义 | '0' | '1' | 创建页要求 |
| --- | --- | --- | --- | --- |
| 0 | 局数 | 6 局(默认) | 12 局 | 单选必选 |
| 1 | 扣卡方式 | 房主扣卡(默认) | AA 每人扣卡 | 单选必选 |
| 2 | 傍王 | 关(默认) | 开 | 独立可选 |
| 3 | 爬坡 | 常规算子(默认) | 爬坡 | 独立可选 |
| 4 | 查牌模式 | 可查牌(默认) | 不查牌 | 单选必选 |
| 5~10 | 预留扩展位 | 当前发送填 0 | 当前无玩法定义 | 无控件 |
**设计与实现差异:** 规则设计者指定的前 5 位顺序为“局数、扣卡方式、查牌模式、傍王、爬坡”,而当前源码与测试仍按上表解析。创建页按设计顺序展示,编码层按已核验的服务器位索引构造参数;这不表示设计要求的传输位序已在服务器落地。若后续变更服务端位序,需要同步核验协议和兼容方案,不能仅更新文档就声称契约已改变。
局数、扣卡、查牌分别单选必选;傍王与爬坡允许均不勾选、任选其一或同时勾选,未选仍写对应位的 '0'。编码示例(均为当前服务端顺序):
- `00000000000`:6 局、房主扣卡、可查牌、不傍王、不爬坡。
- `10100000000`:12 局、房主扣卡、可查牌、傍王、不爬坡。
- `00001000000`:6 局、房主扣卡、不查牌、不傍王、不爬坡。
- `11011000000`:12 局、AA 扣卡、不查牌、不傍王、爬坡。
**实际解析兼容行为:** 服务端 `parse()` 接受至少 5 位的字符串,仅读取前 5 位;旧 5 位串及更长扩展串均可解析,不强制长度为 11。缺失、非字符串或不足 5 位时整体使用 `00000` 的语义;每个选项位只有字符 '1' 判为开启,其余字符判为关闭。预留位不校验内容,也不影响当前五项配置。此处记录的是来源实现,前端仍应在编码入口生成约定的合法 11 位串,不能依赖服务器容错掩盖错误。
`class.config.js` 是玩法配置的唯一解析入口;下游使用具名配置,不重新按下标解释。`multiple` / `bangwang` / `climb` / `baozhu` / `seatlist` / `liangpai` / `pushlist` 按相应配置取值或门控。源码 `cfg.climb` 行的行末注释写着“true=常规算子”,与位定义及算法分支不符;实际 true 表示启用爬坡,以执行逻辑为准。
对应的房卡与局数(`export.get_asetcount` / `get_needroomcard` / `get_needroomcard_joinroom`,design §10.1):
| 扣卡方式(位1) | 局数(位0) | 房主开房扣 | 加入者扣 |
| --- | --- | --- | --- |
| 房主扣卡 `'0'` | 6 局 | 2 张 | 0 |
| 房主扣卡 `'0'` | 12 局 | 4 张 | 0 |
| AA 每人 `'1'` | 6 局 | 1 张 | 1 张 |
| AA 每人 `'1'` | 12 局 | 2 张 | 2 张 |
### 0.6 「一手出牌」的顺序口径
同一份业务数据「某一手打出的牌」会出现在**三个下发位置**:
| 位置 | 字段 |
|---|---|
| 出牌推送 | `chupai1` / `chupai2` / `chupai3` 的 `data.cards` |
| 本轮进行态 | `playproc.cards[座位]`(`chupai*` 与重连包 `PushCards` 都带)|
| 重连出牌历史 | `PushCards.pushlist[轮次-1][座位]` |
**这三处的数组内容逐元素完全相等**——顺序统一为「按**本局主牌花色**从大到小」的**权威顺序**。
服务端只在落牌那一刻归一化一次(`class.paiju.js` 的 `order_playcards`),随后:
出牌推送读它、`playproc.cards` 存它、出牌历史 `playhistory` 归档它,下游一律**只读、不重排**。
因此前端**「增量回放」与「重连重建」得到的出牌历史必定一致**,两条路径可以复用同一份解析。
> ⚠️ **请求包里的 `cards` 顺序无意义**:那是玩家的点击顺序,同一手牌每次都可能不同。
> 服务端不采信、不回显、也不据它判定牌型(server dev-guide 04 §8「前端不是数据源」)。
> 一手单张时看不出差别;**一手多张(甩牌 / 对子 / 拖拉机)时才会显形**,尤其是同编码的一对牌
> ——排序本身区分不了它们,只有「出牌当场归档」这一个写入处才能保证两条路径给出同一个数组。
---
## 1. 发牌(fapai)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:fapai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| asetidx | 整数 | 当前局数 |
| asetcount | 整数 | 总局数 |
| cards | 数组 | 自己得到的牌id列表 |
| step | 整数 | **本局阶段**(发完牌恒为 `1` 叫分;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| seat | 整数 | **控制权**:当前等待叫分者的位置。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)|
| countdown | 整数 | 叫分倒计时 |
---
## 2. 叫分或不叫(jiaofen,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:jiaofen
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 叫分者的位置序号 |
| call | 整数 | 分数,0表示不叫 |
---
## 3. 叫分或不叫(jiaofen,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiaofen
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 叫分者的位置序号 |
| call | 整数 | 分数,0表示不叫 |
| currcall | 整数 | 当前叫到的分数 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| step | 整数 | **本局阶段**(叫分尚未结束,恒为 `1`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| nextseat | 整数 | **控制权**:下一个叫分者的位置序号,即本包之后「轮到谁」。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)|
| countdown | 整数 | 叫分倒计时 |
---
## 4. 上庄(shangzhuang)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:shangzhuang
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 叫分者的位置序号 |
| call | 整数 | 分数,0表示不叫 |
| banker | 整数 | 庄家的位置序号 |
| grade | 整数 | 庄家的叫分 |
| step | 整数 | **本局阶段**(叫分结束、进入选主/投降,恒为 `2`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,因为选主与投降都只由庄家做(服务端 `mod.xuanzhu`/`mod.touxiang` 的座位校验同样以 `banker` 为准)。<br>⚠️ **别和本包的 `seat` 混用**:`seat` 是**最后一个叫分者**,不是控制权。与重连包 `ChooseMain.seat` 同源同值 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| bottomcards | 数组 | 8 张**底牌**(发牌时没发给玩家、扣在桌面的 8 张)。**庄家恒有**;**闲家仅当 70 分坐庄时才有**(供 3 秒亮牌用),非 70 分时闲家没有该属性(底牌只有庄家可见,design §4)。<br>**顺序在发牌结束时即冻结**(服务端 `paiju.bottomcards`,按「还没有主牌」的口径排一次):底牌是在**选主之前**翻给庄家看的,那时主牌花色尚不存在,故不按主牌花色排。本包与重连包 `ChooseMain.bottomcards` / `BuryCards.bottomcards` 三处**同序**,客户端存一次即可全程复用(含「查底牌」回看)|
| ancard3s | 整数 | **开底**标志。仅 70 分坐庄时出现且为 `1`:表示庄家**摸底**之前,需将 `bottomcards` 这 8 张**底牌**向所有玩家翻开 3 秒(design §4/§7.1);非 70 分无此属性 |
| cards | 数组 | 拿了底牌后手上的牌(36 张,含底牌),庄家才有此属性,闲家没有该属性 |
| countdown | 整数 | 选主倒计时 |
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同为选主阶段(step2)的决策,见 touxiang 包与 design §4 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):按此刻累计捡分实时算出的判定倍率,**带符号**,与结算包 `aset.upgrade` 同口径——`3`/`2`/`1` = 庄家 大光/小光/过庄,`-N` = 闲家升 N 级,`0` = 叫分未定。上庄时捡分恒为 0,故必为 `3`。三家同值(捡分本就公开)。**不含扣底**——扣底要到末轮才产生。<br>顶部「抓分」角标只关心倍数大小,取 `Math.abs` 即可;**符号供客户端的判定动画分辨该播哪个**(不能取绝对值,否则 3 大光与 -3 升3级会撞在一起)|
---
## 5. 投降(touxiang,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:touxiang
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 庄家的位置序号。投降是**选主阶段(step 2)与选主互斥**的选择:`mod.touxiang` 仅在 `step==2`、`banker==seat`、`call==70` 时受理;点投降即直接结算,不选主、不埋牌、不出牌(design §4) |
---
## 6. 选主(xuanzhu,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:xuanzhu
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 选主者的位置序号 |
| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 |
---
## 7. 选主(xuanzhu,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:xuanzhu
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家的位置序号 |
| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 |
| step | 整数 | **本局阶段**(选主完成、进入埋牌,恒为 `3`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,埋牌只由庄家做(服务端 `mod.maipai` 的座位校验以 `banker` 为准)。与重连包 `BuryCards.seat` 同源同值 |
| countdown | 整数 | 埋牌倒计时 |
| cards | 数组 | 选主后自己手上的牌id列表 |
---
## 8. 埋牌(maipai,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:maipai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 埋牌者的位置序号 |
| cards | 数组 | 埋牌的id列表 |
---
## 9. 埋牌(maipai,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:maipai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| cards | 数组 | 埋牌后手上的牌,去掉了埋牌,庄家才有此属性,闲家没有该属性 |
| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),庄家才有此属性,闲家没有该属性。注意与 `shangzhuang.bottomcards`(**底牌**,发牌留桌 8 张)是两批不同的牌,见 §0.0。<br>**取服务端权威快照 `get_burycard()`,不是客户端请求包里 `cards` 的原序**:按本局主牌花色从大到小排好,与重连包 `PushCards.burycards` **同源同序** |
| seatlist | 数组 | 三家座位牌况,**仅可查牌模式下发**(不查牌无此属性,design §9),结构与门控同 `chupai1/2/3.seatlist` 与重连包 `PushCards.seatlist`。<br>埋牌完成时它是刚初始化的**空表**(每家 `[[0,0],[0,0],[0,0],[0,0],[-1,-1]]`)——下发它是为了让「埋牌完成 → 庄家首出」这段窗口内,增量路径与重连路径拿到同一张表,客户端无需为这段窗口特判 |
| playproc | json | **本轮进行态**(design §5.1),**恒有、三家同值**,结构与门控同 [`chupai1.playproc`](#11-第一个玩家出牌chupai1)/重连包 `PushCards.playproc`(同一个快照函数 `get_playproc()`)。`do_burycard` 内部已 `new_playround` 就地初始化好 round-1 的进行态,此包带的正是这份初值:`round=1`、`start`/`currseat` 均为即将首出的庄家、`cards` 全空、`shuai_demand` 为 `null`。<br>**可见性**:全部字段由桌面公开信息推出(此刻尚无一张牌打出),三家整体下发、不逐座位裁剪 |
| step | 整数 | **本局阶段**(埋牌完成、进入出牌,恒为 `5`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| seat | 整数 | **控制权**:出牌者的位置序号(即将首出的庄家)。服务端取自权威的 `playproc.currseat`,与重连包 `PushCards.playproc.currseat` 同源同值 |
| countdown | 整数 | 出牌倒计时 |
| liangpai | json | **亮牌**(design §8.2)。**只有闲家、且可查牌模式、且庄家达门槛时才有**;不达标或不查牌则无此属性。结构:`{ cards: [牌id...] }`——**庄家手中全部固定主牌的具体牌面**,按本局主牌序从大到小排好。<br>**固定主牌 = 双王 + 全部花色的 2 + 全部花色的 7**,**不含**主花色的普通牌 A/K/Q/J/10/9/8/6/5。<br>**门槛**(任一满足即下发):固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6。**不限叫分**。<br>亮出的是**固定的一份**,不随被哪条门槛触发而增减;也**不给数量统计**——数量前端自己数 `cards.length` 即可。<br>口径是庄家**埋牌后的静态快照**(排除已埋的 8 张,但包含之后已打出的牌),全局固定不随出牌缩水,故重连包里取值一致 |
---
## 10. 出牌(chupai,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:chupai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出牌的id列表 |
---
## 11. 第一个玩家出牌(chupai1)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai1
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 实际打出的牌id列表;**甩错时**(见 `shuaicuo`)为被强制打出的那一张最小主牌单张。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序——请求包里的 `cards` 顺序服务端不采信、不回显(见 [§0.6](#06-一手出牌的顺序口径))|
| shuaicuo | 整数 | 甩错标志,仅甩错时出现且为 `1`(design §5.4.5:甩牌未通过最大性判定,整套甩牌收回,本轮只强制打出最小一张、失去本轮甩牌资格);正常出牌无此属性 |
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
| count | 整数 | 出牌数量(甩错时为 1) |
| flower | 整数 | 出牌花色 |
| cardtype | 整数 | 出牌牌型:`>100` 单张(101 一张、102 两张…)、`>200` 对子(201 一对、202 两对…)、`>300` 拖拉机(302 两连对、303 三连对…),见 `class.pai.js` 顶部注释。**注意:cardtype 只能表达单一牌型**,而甩牌是单张/对子/拖拉机自由混搭(design §5.4.3),会被牌型推导压平成 1xx 或 2xx(例如「主K对 + 主5」得 103、「两连对 + 一散对」得 203)。**甩牌的真实结构请读 `shuai`,不要据 cardtype 反推** |
| shuai | json | 甩牌分量构成,**仅本次出牌是合法甩牌时才有**(非甩牌、以及甩错退化为单张时都没有该属性)。结构 `{ tractors: [连对数...], pairs: 独立对子数, singles: 单张数 }`,例如「主K对 + 主5」为 `{tractors:[],pairs:1,singles:1}`。与服务端跟牌时逐分量强制匹配用的 `shuai_demand` 同源(design §5.4.3/§5.4.4)|
| nextseat | 整数 | 下一个出牌者的位置序号 |
| countdown | 整数 | 下一个出牌者的出牌倒计时 |
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)|
| cardsinhand | 数组 | 出牌者出牌后手上剩下的牌id列表,只有出牌者才有此属性 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与结算包 `aset.upgrade` 同口径(`3`/`2`/`1` 庄家大光/小光/过庄,`-N` 闲家升 N 级,`0` 叫分未定)。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送。符号是「谁赢」的区分,客户端判定动画靠它分辨 |
| playproc | json | <a id="playproc-def"></a>**本轮进行态**(design §5.1)。**`chupai1/2/3` 三个包恒有,三家同值**;结构与重连包 [`PushCards.playproc`](#断线重连deskinfo) **完全一致**(服务端同一个快照函数 `get_playproc()`,前端增量回放与重连重建复用同一份解析)。<br>字段:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前该谁出、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮暂时最大者、`maxcard` 其牌编码、`cards` 本轮三家各自出的牌(**定长 3,下标 = 座位序号**,未出的位置为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 `null`)。<br>⚠️ **`chupai3` 带的是【下一轮】的进行态**(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`):本轮第三家一出完,服务端就地开了新一轮。这与「此刻断线重连拿到的 `PushCards.playproc`」完全相同——本轮那三手牌客户端已由 `chupai1/2/3` 各自的 `seat`+`cards` 收到,收牌动画后即清台。**唯一例外**:`chupai3` 打完最后一张牌时本包会转成 `jiesuan`(见 §14),那种情况下**不带** `playproc`。<br>**可见性**:全部字段都由桌面公开信息推出(`cards` 就是已摊在桌上的牌,`shuai_demand` 与 `chupai1.shuai` 等价且甩出的牌本身已公开),故三家整体下发、不逐座位裁剪 |
| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**(甩牌跟牌走逐分量匹配,见 design §5.4.4)、或算出的必出牌为空 |
---
## 12. 第二个玩家出牌(chupai2)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai2
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))|
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
| nextseat | 整数 | 下一个出牌者的位置序号 |
| countdown | 整数 | 出牌倒计时 |
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)|
| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 |
| playproc | json | **本轮进行态**,恒有、三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。此时 `cards` 已含首家与本家两手牌,`currseat` 指向第三家 |
| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**、或算出的必出牌为空 |
---
## 13. 第三个玩家出牌(chupai3)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai3
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))|
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
| nextseat | 整数 | 下一个出牌者的位置序号 |
| countdown | 整数 | 出牌倒计时 |
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)|
| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 |
| maxseat | 整数 | 本轮出牌谁最大,只有本轮最后一个玩家出牌后才有此属性 |
| grade | 整数 | 本轮闲家得分,只有本轮最后一个玩家出牌后且闲家有得分才有此属性 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,口径同 `aset.upgrade`。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送 |
| playproc | json | **本轮进行态**,三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。⚠️ 本包带的是**下一轮**的进行态(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`),与此刻重连拿到的 `PushCards.playproc` 一致。**牌局在本包打完(转为 `jiesuan`)时不带此属性** |
---
## 13.5 明牌(mingpai,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:mingpai
查看另外两家手中全部主牌的具体牌面(design §9)。服务端仅在**可查牌模式、出牌阶段(step5)、且已有【任一】玩家报无主**(`have_baofu()`)时受理——注意判定的是「场上有人报无主」而非「请求者本人报无主」,报无主那位与另外两位同样有权查看(design §9.3);不满足时按 0.2 回 `mingpai` 失败包(不查牌 / 未报无主 → `RULE`,非出牌阶段 → `STEP`)。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 请求者的位置序号 |
## 13.6 明牌(mingpai,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:mingpai (只回发给请求者)
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 请求者的位置序号 |
| others | 数组 | 另外两家各自未出的全部主牌,元素 `{ seat: 位置序号, zhucards: [主牌id列表] }` |
> "再点一次取消查看"是客户端的显示开关,无需再请求服务端。
## 13.7 出牌提示(tishi,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:tishi
闲家在出牌阶段向对家(另一闲家)发"踩/没分/有分"提示(design §11)。服务端仅在**出牌阶段(step5)、且发起者为闲家**(`seat != banker`)、**tip 合法(1/2/3)** 时受理;**不校验提示真实性**(玩家可主观发送);庄家无对家,不受理。不满足时按 0.2 回 `tishi` 失败包给发送者(庄家发 → `RULE`,非出牌阶段 → `STEP`,tip 非法或缺失 → `PARAM`)。**成功时不给发送者回执**,只转发给对家。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 发出提示的闲家位置序号 |
| tip | 整数 | 提示类型:1=踩(我能大过庄家)、2=没分(我手上没分了)、3=有分(我手上有分) |
## 13.8 出牌提示(tishi,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:tishi (**只转发给对家**,即另一闲家 `3 - banker - seat`)
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 发出提示的闲家位置序号 |
| tip | 整数 | 提示类型:1=踩、2=没分、3=有分 |
---
## 14. 结算(jiesuan)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiesuan
结算包由以下几个子结构拼装而成。三种结算来源的组成不同:
- **正常出牌结算**(`mod.chupai` 中最后一张牌出完,`get_paiju_account(0, ...)`):含 `chupai` + `bottom` + `aset`(末局再加 `account`)。
- **投降结算**(`mod.touxiang`,`get_paiju_account(1, ...)`):只含 `aset`(末局再加 `account`),无 `chupai`、无 `bottom`。
- **解散结算**(`export.get_disbandRoom`,`get_paiju_account(2, ...)`):只含 `aset` + `account`,无 `chupai`、无 `bottom`。**投递方式与上面两种不同,见 §14.1。**
上面这张表头(`route: erqiwang / rpc: jiesuan`)**只适用于前两种**——正常出牌结算与投降结算,由子游戏自己 `sendpack_toother` 广播,客户端在 `rpc == "jiesuan"` 的分支里按 `data.aset` 取值。
### 14.1 解散结算的投递方式(与前两种不同,客户端需单独处理)
解散**不是**由子游戏发包,而是平台在解散流程里回调 `youle_erqiwang.export.get_disbandRoom(o_room)`,把**返回值整个对象**塞进**房间路由**的解散包里下发(平台 `server_room/rpc.js`、`server_room/class.room.js`)。因此客户端收到的是:
```json
{
"app": "youle", "route": "room", "rpc": "free_room",
"data": {
"seats": [],
"deskfree": {
"rpc": "jiesuan",
"data": { "success": true, "aset": {}, "account": [] }
}
}
}
```
要点(照 §14 的表去 `data.aset` 取值会取空):
- **rpc 是平台的 `free_room`(route 也是 `room`),不是 `erqiwang/jiesuan`**;子游戏返回的 `rpc: "jiesuan"` 只是嵌在 `deskfree` 里的一个普通字段。
- **多一层 `data` 包装**:真实取值路径是 `data.deskfree.data.aset` 与 `data.deskfree.data.account`。
- 本包的成败标志有两层:外层 `data.success` 由**平台**填写(解散流程本身成功与否),子游戏结算自带的 `success` 在 `data.deskfree.data.success`。§0.1「每个包 `data` 必带 `success`」说的是子游戏自己发的包;本包外层归平台。
- `aset` / `account` 的字段结构与下面 §14 的表完全一致(`aset.multiple = 0`、`upgrade = 0`、各家 `grade = 0`,即解散局不结算子数与算奖;`account` 恒有,见 design §12.2「按当前累计分结算」)。
- 若解散发生在**开战后、首局牌发出前**的窗口内(本游戏首局由 `makewar` 延迟 1 秒创建),`get_disbandRoom` 返回 `null`,平台走「不带 `deskfree`」分支——客户端需容忍 `data.deskfree` 缺失。
**顶层字段(三种结算来源恒有)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| success | 布尔 | 恒 `true`(§0.1)|
| step | 整数 | **本局阶段**,结算包恒为 `6`。三种结算来源(正常/投降/解散)统一由 `get_paiju_account` 给出——解散在此之前 `step` 可能还停在 1/2/3/5,由它落定为 6。客户端**不得按 rpc 名硬编码**,一律读此字段 |
**chupai(出牌包,仅正常出牌结算存在)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号,投降和解散无此属性 |
| cards | 数组 | 出的牌id列表,投降和解散无此属性 |
| maxseat | 整数 | 本轮出牌谁最大,投降和解散无此属性 |
| grade | 整数 | 本轮闲家得分,投降和解散无此属性 |
| gradecards | 数组 | 本轮闲家得分分牌列表,投降和解散无此属性。**注意:当前源码中赋值语句被注释(`class.paiju.js`/`mod.js` 中相关行均被注释掉),该字段实际永远不会出现在下发的包里,属于失效字段** |
**bottom(抠底包,仅正常出牌结算存在;`cards` 恒有,其余仅闲家抠底时有)**
> 本分组的 `cards` 是**埋牌底牌**(庄家埋下的 8 张),不是发牌留桌的底牌。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| cards | 数组 | **埋牌底牌**(庄家埋下的 8 张),投降和解散无此属性 |
| multiple | 整数 | 闲家抠底倍数,闲家没抠底无此属性,投降和解散无此属性 |
| grade1 | 整数 | 埋牌底牌的分数,闲家没抠底无此属性,投降和解散无此属性 |
| grade2 | 整数 | 闲家抠底得分,闲家没抠底无此属性,投降和解散无此属性 |
**aset(单局结算包)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家的位置序号,-1:无庄 |
| call | 整数 | 庄家叫分,-1:无叫分 |
| multiple | 整数 | 基础子数 `get_base_bycall(call, climb)`(design §7.1/§7.3.1):常规算子 65→2、60→3、55→4、50 及以下→6、70打牌→2;投降固定 1、解散 0;无叫分 0 |
| flower | 整数 | 主牌花色,-1:无主牌花色 |
| grade | 整数 | 闲家捡分(含抠底) |
| upgrade | 整数 | 判定倍率(带符号,design §7.2.0):3大光 / 2小光 / 1过庄 / -N升N级(倒庄)/ -99投降 / 0解散或无判定 |
| bangwang | 整数 | 本局是否启用傍王规则 0否 1是(roomtype 位2) |
| climb | 整数 | 本局是否启用爬坡规则 0否 1是(roomtype 位3) |
| seatlist | 数组 | 玩家列表,元素结构见下 |
`seatlist` 数组元素结构:
```json
{
"cards": [], // 冲关牌型:参与冲关的牌(王 + 冲关组合牌)列表,供「冲关牌型」界面展示
"chongguan": 0, // 冲关奖数(design §8.1,旧称"常规算奖";只有庄家才计入自己的 N)
"wang": 0, // 手牌中的王数(傍王按此计奖)
"naward": 0, // 该家总奖数 N =(庄家?冲关奖数:0)+(傍王?王数:0)
"grade_aw": 0, // 算奖得分 = X×(2Ni−Nj−Nk),X 为每对子子数。= grade_cg + grade_bw
"grade_cg": 0, // 其中的【冲关】分量(design §8.1,不含傍王)
"grade_bw": 0, // 其中的【傍王】分量(design §8.3;未勾傍王时恒 0)
"grade_jf": 0, // 捡分子数得分
"grade": 0, // 本局总分 = grade_aw + grade_jf
"score": 0 // 累计得分
}
```
> 结算数值模型(design §7~§8):每「庄–闲」对子的基础金额 `X = multiple × |upgrade|`(投降 X=1、解散 X=0)。捡分子数:庄赢时两闲家各付庄家 X、庄家收 2X;闲赢(升级)时庄家各付两闲家 X。算奖:持有 N 奖的玩家从另外两人各多收 `X×N`,三家两两独立叠加(含闲–闲),即 `grade_aw = X×(2Ni−Nj−Nk)`。
>
> **算奖的两个分量**(供大局结算分项展示):`N = N_冲关 + N_傍王`(冲关只计庄家、傍王勾选后庄闲都算)。
> 服务端把两个分量**分别代入同一公式**各算一次,得到 `grade_cg` 与 `grade_bw`。
> 公式对 `N` 是线性的,因此恒有 **`grade_cg + grade_bw == grade_aw`**,且两个分量各自零和。
> 之所以必须在算钱时就拆,是因为两个 `N` 一旦相加就再也分不开——事后无法从 `grade_aw` 反推各自占比。
**account(大局结算包)**
> 仅当打到最后一局(`o_paiju.idx >= o_room.asetcount`)或房间中途解散(解散结算)时,`jiesuan` 包才会带上此分组;普通的中间局结算包没有 `account` 字段(`class.paiju.js` `get_paiju_account`)。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| account | 数组 | 玩家列表(长度 3,下标 = 座位序号),元素结构见下 |
```json
[
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 },
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 },
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }
]
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| score | 整数 | 累积总分(全部小局 `aset.seatlist[i].grade` 之和) |
| grades | 数组 | 每局总分列表,按局序 |
| grade_jf_total | 整数 | **基础分**累计:各局 `grade_jf`(捡分子数得分)之和 |
| grade_cg_total | 整数 | **冲关分**累计:各局 `grade_cg` 之和(design §8.1,**不含**傍王) |
| grade_bw_total | 整数 | **傍王分**累计:各局 `grade_bw` 之和(design §8.3;未勾傍王时恒 0) |
> 恒等式:`grade_jf_total + grade_cg_total + grade_bw_total == score`,供大局结算面板分项展示(基础分 / 冲关分 / 傍王分 / 总分)。
>
> ⚠️ **结构变更(2026-08-26)**:原为 `[[累积得分, [每局得分...]], ...]` 的二元数组,现改为**对象数组**并增加三项分解。数组下标语义不清,且前端尚未开工,此时改代价最小。
---
## 15. 准备(zhunbei,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:zhunbei
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 位置序号 |
---
## 16. 准备(zhunbei,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:zhunbei
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 位置序号 |
---
## 断线重连(deskinfo)
> 由平台在玩家进入房间/断线重连时回调 `youle_erqiwang.export.get_deskinfo(o_room, seat)` 生成并下发(服务器→客户端);下发的 app/route/rpc 由平台的进房/重连流程决定,不在本子游戏代码内固定。平台把它挂在 `pack.data.deskinfo` 下,**`data.success` 由平台填写、子游戏不注入**(见 0.4)。下列字段即该函数返回的 `deskinfo` 对象结构,按当前 `paiju.step` 只带对应阶段的分组。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| count | 整数 | 总局数 |
| idx | 整数 | 当前局数 |
| PlayerInfo | 数组 | 三个玩家目前的总积分 `[0,0,0]` |
| step | 整数 | 牌桌状态:1发完牌叫分 2选主/投降 3埋牌 5出牌 6结算(无独立投降阶段——投降在 step2 与选主互斥;埋牌后直接进入 step5 出牌) |
| MyCards | 数组 | 自己手上的牌 |
**CallRun(不在叫分阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 当前叫分位置 |
| countdown | 整数 | 叫分倒计时 |
| nowcall | 整数 | 当前叫分 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| call | 数组 | 三家叫分 `[null,0,65]`,null还未叫分,0不叫,>0叫了多少分 |
**ChooseMain(step 2 选主/投降阶段,不在此阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(选主与投降都只由庄家做)。字段名与 `CallRun.seat` 一致:**各阶段分组里的 `seat` 统一表示「轮到谁」**。与增量推送 `shangzhuang.nextseat` 同源同值;客户端据此设控制权,**不要自己用 `banker` 反推**(那是把「选主=庄家」这条规则搬到前端)|
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| countdown | 整数 | 选主倒计时 |
| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`BuryCards.bottomcards` **完全同序**(见 §4)|
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同一决策点,见 touxiang 包与 design §4 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。选主阶段一张牌都还没出、捡分恒为 0,故**必为 `3`**(大光)。三家同值。有此字段,重连后顶部「抓分」角标才不会掉回 0 |
> 选主阶段前端需在每个花色按钮上显示"该花色在庄家手中的对子数"(design §4/§11)——庄家的完整手牌由 `MyCards` 提供(庄家为 36 张),对子数由前端据此计算,服务端不额外下发。
**BuryCards(step 3 埋牌阶段,不在此阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(埋牌只由庄家做)。与增量推送 `xuanzhu.nextseat` 同源同值 |
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| flower | 整数 | 主牌花色 |
| countdown | 整数 | 埋牌倒计时 |
| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`ChooseMain.bottomcards` **完全同序**——**不**按本局主牌花色重排(见 §4)|
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。埋牌阶段同样一张牌未出、捡分恒为 0,故**必为 `3`**(大光)。三家同值 |
> 埋牌阶段无投降(投降是 step2 与选主互斥的选择,选主后即不可再投降)。
**PushCards(step 5 出牌阶段,不在此阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| flower | 整数 | 主牌花色 |
| countdown | 整数 | 出牌倒计时 |
| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),只有庄家有此属性。注意与 `ChooseMain`/`BuryCards.bottomcards`(**底牌**)是两批不同的牌,见 §0.0。与 `maipai.burycards` **同源同序**(同一个 `get_burycard()`,按本局主牌花色从大到小)|
| grade | 整数 | 当前的捡分分数 |
| gradecards | 数组 | 当前的捡分分牌,只有闲家有此属性 |
| playproc | json | **本轮进行态**,与 `chupai1/2/3.playproc` **同源同结构**(服务端同一个 `get_playproc()` 快照函数,见 [§11 `playproc`](#11-第一个玩家出牌chupai1)):`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(**定长 3**,下标=位置序号,未出为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 null,供跟牌逐分量强制匹配,design §5.4.4)。**两种查牌模式下恒有此属性**——`cards` 是当前这一轮桌面上的牌,不属于「查牌」,屏蔽了后出的人就无从跟牌(design §9 末尾)|
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是,**恒有此属性**;与 `chupai1/2/3` 的 `baozhu` **同源同值同门控**(同一个 `have_baofu()` + 同一个查牌位)。它是**余主公示**与“明牌”按钮的开关,重连必须一并恢复,否则重连后按钮凭空消失;**不查牌模式恒为 0**(design §9)|
| seatlist | 数组 | 三家座位牌况,与上文 `maipai` / `chupai1/2/3` 包的 `seatlist` 同名同结构(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`)。**仅可查牌模式下有此属性**(design §9)|
| liangpai | json | **亮牌**,结构同 maipai 包的 `liangpai`(`{ cards: [牌id...] }`);**仅可查牌模式、且请求者为闲家、且庄家达标时有**(供闲家重连后仍能看到,design §8.2)|
| pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次(从第 1 轮起,含进行中的当前轮),内层**恒为 3 个数组**、按位置序号存该轮各家出的牌id。**每一手与当时那个 `chupai1/2/3` 包的 `cards` 逐元素完全相等**(同一份数据、同一个顺序口径,见 [§0.6](#06-一手出牌的顺序口径)),因此前端“增量回放”与“重连重建”得到的出牌历史必定一致。**仅可查牌模式下有此属性**——往轮打出、已被收走的牌属于「查牌」范畴,不查牌模式一律不下发(design §9)。注意当前这一轮桌面上的牌由 `playproc.cards` 恢复,**两种模式下都有**,否则后出的人无从跟牌 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `chupai1/2/3` 同源同值,供重连后顶部「抓分」角标与判定动画状态立即正确 |
| mustcard | 数组 | **本轮跟牌的必出牌**(design §5.2)。**仅当 `playproc.currseat == 请求者座位` 时才有**,且只算请求者自己的手牌;未轮到本家、本轮首家、甩牌局面、必出牌为空时均无此属性 |
**Balance(不在结算阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| readystate | 数组 | 所有玩家的准备状态 |
| aset | json | 单局结算包,同上面结算包中的 `aset`,只有当自己的准备状态为0时才有此属性 |
| bottom | json | **抠底包**,结构同 §14 的 `bottom`,与该局 `jiesuan` 推送的 `data.bottom` **同源同值**(服务端在 `get_paiju_account` 末尾冻结的同一份快照)。**仅正常出牌结算才有**(投降/解散没有抠底这一步);同样受「自己的准备状态为 0」门控。<br>**为什么必须有**:结算面板还开着时断线重连/硬刷新,只恢复 `aset` 会让抠底明细整块空白——同一份数据两条路径给的不一样(server 红线「发全下发面」)。<br>**可见性**:埋牌底牌在本局结算时已随 `jiesuan` 广播给三家(design §11 结束亮底),此处不构成额外泄露 |
| account | json | **大局结算包**,结构同 §14 的 `account`,与该局 `jiesuan` 推送的 `data.account` **同源同值**。**仅末局或中途解散才有**,与 `jiesuan` 的取舍完全一致(有就带、没有就不带)|
---
## 战绩(大局列表)gameinfo1
> 非客户端收发包:大局结束/解散时由 `class.desk.js` `get_desk_account` 组装,经 `import.save_grade(o_room, o_gameinfo1, o_gameinfo2, 1)` 传给平台战绩服务持久化(服务器→平台)。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| roomcode | 整数 | 房号 |
| asetcount | 整数 | 实际局数 |
| createtime | 字符 | 开房时间 |
| makewartime | 字符 | 开战时间 |
| players | 数组 | 玩家列表,元素结构见下 |
`players` 数组元素结构:
```json
{
"seat": 0, // 座位
"playerid": 100001, // 玩家ID
"name": "", // 昵称
"avatar": "", // 头像
"score": 0 // 成绩
}
```
---
## 战绩(大局)gameinfo2
> 非客户端收发包:与 gameinfo1 同批,由 `import.save_grade` 的第 3 个参数传给平台战绩服务(服务器→平台)。
牌局列表,数组元素结构:
```json
{
"starttime": "", // 开始时间
"endtime": "", // 结束时间
"seatlist": [6, -6, 0], // 玩家成绩
"callproc": [], // 叫分过程,详情见代码中的注释
"banker": 0, // 庄
"call": 0, // 叫分
"flower": 0, // 主牌花色
"result": 0, // 牌局结果
"cards": [] // 发牌出牌情况,详情见代码中的注释
}
```