diff --git a/server/games/erqiwang/docs/compliance/00-已知不一致-初步清单.md b/server/games/erqiwang/docs/compliance/00-已知不一致-初步清单.md new file mode 100644 index 0000000..5cf96b9 --- /dev/null +++ b/server/games/erqiwang/docs/compliance/00-已知不一致-初步清单.md @@ -0,0 +1,51 @@ +# 二七王 服务端实现 vs 设计文档:初步不一致清单 + +> 本文是首轮通读 `server/games/erqiwang/*.js` 与 `docs/design/design.md` 后记录的**初步**疑点清单,作为逐条深入核对的线索。完整、逐节的核对结论见同目录 [`01-design合规逐节核对.md`](./01-design合规逐节核对.md)。 +> +> 方向约定(与根 `CLAUDE.md` 一致):`design.md` 是玩法规则的唯一权威,代码与其冲突时**应改代码去符合规则**(除非规则标「待确认」);本清单只做记录,不代表已修复。 + +--- + +## 1. 算子(基础子数)与设计表格完全对不上 + +- 设计 §7.1 常规算子基础子数:65→2、60→3、55→4、50 及以下→6;70 投降→1、70 打牌→2。 +- 代码 `arith.get_multiple_bycall` 只返回 1/2/4/0(`call>60→1`、`call>40→2`、`call>0→4`),与设计四档数值、结构均不符。 + +## 2. 爬坡 / 傍王 房间选项未做成开关 + +- `arith.get_upgrade` 用 `halfcall = ceil(call/2)` 作小光/过庄分界与升级级距;设计常规算子该分界**固定 40**、级距**固定 40**。 +- 代码里**没有任何读取房间「爬坡 / 傍王」勾选的分支**:`grade_w`(傍王)无条件计算、`halfcall` 恒用。设计 §7.3 / §8.3 / §10.3 中这两项都是可选规则,需要开关。 + +## 3. 扣底倍数不符 + +- 设计 §6.3:单张主 ×1、主对子 ×2、两连对 ×4、三连对 ×6、N 连对 ×2N(线性)。 +- 代码 `arith.get_bottom_multiple`:单张 →2、对子 →4、两连对 →8、三连对 →16、四连对 →32、五连对 →64(近似指数),且单张即翻倍;六连对落空返回 `undefined`。 + +## 4. 「固定主牌 ≥10 张」给了算奖 + +- 代码 `arith.get_chongguan` 对 `王+2+7≥10` 追加 `total−9` 奖(「10 个老主判断」)。 +- 设计 §8.2 明确举例:固定主牌 ≥10 张只触发**亮牌**、**不触发任何算奖**。 + +## 5. 算奖归属模型与结算方式不同 + +- 设计 §8.1:常规算奖**只计庄家**;§8.3 傍王才庄闲都算(且需勾选);§8.4:算奖额外支付额 = `X × N`(`X` 为第 7 节算出的基础输赢子数)。 +- 代码:对**三家**都算「冲关」并两两结算,且叠加「冲关必叫 / 四王必踢」的**作废**规则(设计中无此机制);`grade_cg`、`grade_w` 是**扁平奖数差**(每奖记 1 子),**未乘 `X`**。 + +## 6. 叫分缺上限校验 + +- 设计 §4:叫分范围 5 ~ 70。 +- 代码 `mod.jiaofen` 只校验 `call%5==0` 与「后叫必须更低」,**缺 `call<=70` 上限**,首家可叫出 75、100 等非法分值。 + +## 7. 投降资格范围过宽 + +- 设计 §4.4 / §7.1:投降是 **70 分坐庄专属**分支。 +- 代码 `mod.touxiang` 用 `call < 65 → 拒绝`,即 **65 分也能投降**。 + +## 8. 70 分暗牌亮 3 秒等交互细节未实现 + +- 设计 §4.4 / §7.1:70 分坐庄,8 张暗牌需向两个闲家亮 3 秒(投降 / 打牌都要)。 +- 代码未见相关下发逻辑。 + +--- + +以上为初步清单。经逐节精查后,另发现若干**运行期代码缺陷**(如 `can_followcard` 引用未定义变量 `tuolaji_list` 会抛异常、`can_playcard` 的 14 张上限 `do_returnfalse` 漏写括号导致失效等)、以及**甩牌合法性 / 强制跟牌 / 局数扣卡 / 查牌模式**等更多不一致,详见 [`01-design合规逐节核对.md`](./01-design合规逐节核对.md)。 diff --git a/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md b/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md new file mode 100644 index 0000000..860a6aa --- /dev/null +++ b/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md @@ -0,0 +1,295 @@ +# 二七王 服务端实现 vs `design.md` 逐节合规核对 + +> 权威方向(同根 `CLAUDE.md`):`design.md` 是玩法规则的**唯一权威**。凡代码与规则冲突,除非规则项标注「待确认」,一律视为**代码缺陷**,应改代码去符合规则,**不得改规则迁就代码**。 +> +> 核对对象:`server/games/erqiwang/` 下 `mod.js`、`class.pai.js`、`class.paiju.js`、`class.arith.js`、`class.desk.js`、`class.export.js`、`class.import.js`。 +> +> 严重度标记: +> - 🟥 **严重**:影响发牌 / 计分 / 输赢 / 核心玩法正确性。 +> - 🟧 **中等**:可选规则缺失、边界校验缺失、局部规则偏差。 +> - 🟨 **轻微**:交互提示 / 展示细节 / 死代码。 +> - 🐞 **代码缺陷**:运行期异常或明显写错(无论是否直接违背规则)。 +> - ✅ **符合**。 + +--- + +## 整改进度 + +> 本轮已按 design.md 修正的项(均已用 Node 脚本对 design 判定表逐格验证,见"验证"列)。下方"结论摘要"与各节详评仍保留**原始不一致**描述以留痕;本节记录当前状态。 + +| 项 | 位置 | 整改内容 | 验证 | +| --- | --- | --- | --- | +| D1 崩溃 | `class.arith.js` `can_followcard` | 用 `get_pairlist`+`get_tuolaji_list(_followcards)` 校验拖拉机牌型,消除未定义 `tuolaji_list` | 语法通过 | +| D3 | `can_playcard` | `do_returnfalse;` → `do_returnfalse()`,恢复 14 张上限 | 语法通过 | +| D4 | `do_choiceflower` | 删除空循环死代码 | 语法通过 | +| §6.3 | `get_bottom_multiple` | 改为线性:单张 1、主对 2、N 连对 2N | ✅ 10 用例 | +| §7 算子 | `get_base_bycall`/`get_qvalue`/`get_upgrade`(新)+ `get_paiju_account` | 实现常规算子(大光×3、Q=40) 与爬坡(§7.3 分段) 两套,按爬坡位(roomtype 位3)切换;`X=multiple×|upgrade|` | ✅ 74 格逐档 | +| §8.1 | `get_chongguan` + `get_paiju_account` | 常规算奖只计庄家;连对链修复(传实际主花色);移除 ≥10 老主奖;去掉设计外的"冲关必叫/四王必踢"作废 | ✅ get_chongguan 用例 | +| §8 快照 | `get_seat_cards_award`(新) | 庄家用埋牌后 28 张(排除已埋)、闲家用发牌后 28 张 | 语法通过 | +| §8.3 傍王 | `get_paiju_account` | 改为按傍王位(roomtype 位2)开关,庄闲每王 1 奖,去作废 | ✅ 公式用例 | +| §8.4 | `get_paiju_account` | 算奖并入结算改为 `X×(2Ni−Nj−Nk)`(乘 X) | ✅ §8.4 举例 | +| §4.2 | `mod.jiaofen` | 增加叫分上限 `call<=70` | — | +| §4.4 投降 | `mod.touxiang` + `do_burycard` + `get_deskinfo` | 投降仅 70 分、在**选主阶段 step2** 与选主互斥、不选主不埋牌;结算基础 1、庄输 1 子/闲;算奖用庄家 **36 张**(发牌+暗牌,无主花色→无连对链);埋牌后 step 直接进 5、删除已废弃的投降 step4 与重连 Surrender 视图 | — | +| §10.1 | `get_asetcount`/`get_needroomcard` | 局数 6/12;房主扣卡 2/4 | — | +| §5.4 甩牌 | `arith`(新增 `trump_rank`/`decompose_trump`/`opp_can_beat_flush`)+ `can_playcard` + `do_playcard` + `mod.chupai` | 副牌绝对禁甩;最大性改为**按对手全部主牌**逐分量判定;甩错惩罚(收回、只打最小一张、下发 `shuaicuo`) | ✅ 分解/最大性/各路径用例 | +| roomtype | `class.config.js`(新)+ `export`/`paiju` | 数组下标改为**位串**(`"01011"`),`class.config.js` 唯一解析(SSOT) | ✅ parse/扣卡/局数用例 | +| §4 暗牌亮牌 | `shangzhuang` + `get_deskinfo` | 70 分坐庄把 8 张暗牌下发给所有玩家 + `ancard3s=1`(供摸牌前亮 3 秒);修正**非 70 分暗牌泄露给闲家**(现暗牌只发庄家,符合 §4"只有庄家可见");重连的暗牌一律仅庄家可见 | — | +| §8.2 亮牌 | `get_liangpai`(新)+ `mod.maipai` + `get_deskinfo` | 庄家埋牌后 28 张按阈值(固定主≥10/王≥3/7≥6/2≥6)统计,出牌开始向闲家亮(只亮数量/结构);可查牌模式才下发 | ✅ 阈值/统计用例 | +| §9 查牌 | `class.config.js`(nocheck) + `mod.chupai` + `get_deskinfo` + `mod.mingpai`(新) | 报无主的座位统计 `info`/`baozhu`/`PushCards.seatlist` 与亮牌均按 `cfg.nocheck` 门控;新增 `mingpai` RPC(可查牌+已报无主时查看他家全部主牌) | — | + +**尚未整改(多需与客户端/协议协同,或为较大的独立子系统)**: + +- 🟩 **§5.4 甩牌(已改大部)**:副牌绝对禁甩、最大性按对手全部主牌逐分量判定、甩错惩罚均已实现并验证。**剩余**:§5.4.4 跟甩牌时"拖拉机→对子→单张"的**分量优先级**未强约束——现状是跟牌方必须用主牌按张数跟、且合法甩牌恒不可反超(结果正确),但不强制它优先用主对/主拖拉机去对位。因该细节不改变本轮胜负归属,列为后续精化项。 +- 🟩 **§9 查牌 / §8.2 亮牌(服务端已完成)**:亮牌 `get_liangpai`(庄家埋牌后 28 张按阈值统计)在 `maipai`/重连下发给闲家;报无主的座位统计(`info`/`baozhu`/`PushCards.seatlist`)与亮牌均按查牌位 `cfg.nocheck` 门控(不查牌一律不下发);新增 `mingpai` RPC(可查牌 + 已报无主时查看他家全部主牌)。剩客户端展示(面板/明牌按钮)。 +- 🟩 **§4.4 70 分暗牌亮 3 秒(服务端已就绪)**:`shangzhuang` 对 70 分坐庄把 8 张暗牌下发给所有玩家并带 `ancard3s=1`,供客户端在庄家摸暗牌前亮 3 秒;顺带修正了**非 70 分把暗牌泄露给闲家**的问题(现非 70 分暗牌只发庄家,符合 §4"只有庄家可见")。剩下的 3 秒动画由客户端完成。 +- 🟧 **§11 闲家 3 提示(踩/没分/有分)**:交互展示,需客户端配合。 +- 🟧 **客户端建房配置**:`roomtype` 已改为位串(见下表:位0局数/位1扣卡/位2傍王/位3爬坡/位4查牌),需在 erqiwang 客户端建房界面按位勾选拼串;服务端已按该格式读取(`class.config.js`)。 + +### roomtype 位串约定(服务端已按此读取;解析入口 `class.config.js`) + +`roomtype` 为**定长数字位串(字符串)**,每一位(`charAt`)一个开关 `'0'/'1'`,便于前端逐项勾选。缺省 `"00000"` = 6局/房主扣卡/无傍王/常规算子/可查牌。解析对缺失/过短字符串按 `'0'` 兜底。 + +| 位 | 含义 | `'0'` | `'1'` | +| --- | --- | --- | --- | +| 0 | 局数 | 6局(缺省)| 12局 | +| 1 | 扣卡方式 | 房主扣卡(缺省)| AA每人扣卡 | +| 2 | 傍王 | 关(缺省)| 开 | +| 3 | 爬坡 | 常规算子(缺省)| 爬坡 | +| 4 | 查牌模式 | 可查牌(缺省)| 不查牌 | + +示例 `"10100"` = 12局 / 房主扣卡 / 傍王开 / 常规算子 / 可查牌。 + +--- + +## 结论摘要 + +| 设计章节 | 主题 | 结论 | +| --- | --- | --- | +| §2 | 牌局构成(92 张、去 3/4) | ✅ 符合 | +| §3 | 主牌顺序(牌编码) | ✅ 符合(副 7/副 2 跨花色成对存疑,见下) | +| §4.1-3 | 发牌 / 叫分 / 埋牌流程 | 🟧 缺叫分上限、投降资格过宽 | +| §4.4 | 70 分投降 / 打牌分支 | 🟥 投降资格 65 分即可、暗牌亮 3 秒未实现、投降结算数值错 | +| §4.6 | 坐庄轮换 | ✅ 符合 | +| §5.1-5.3 | 跟牌 / 毙牌 / 垫牌 / 拖拉机 | ✅ 基本符合(1 处运行期崩溃缺陷) | +| §5.4 | 甩牌 | 🟥 合法性判定模型、副牌禁甩、强制跟牌拆解、甩错惩罚均不符 | +| §6.1-6.2 | 分牌 / 捡分 | ✅ 符合 | +| §6.3 | 扣底倍数 | 🟥 倍数表完全不同 | +| §7 | 算子(基础子数 / 大光小光过庄 / 升级 / 爬坡) | 🟥 系统性不符,且无爬坡开关 | +| §8.1 | 常规算奖(只庄家 + 连对链) | 🟥 三家都算、含额外作废规则、连对链因传参失效、≥10 老主误给奖、庄家快照含底/埋牌牌 | +| §8.2 | 亮牌 | 🟧 未实现 | +| §8.3 | 傍王 | 🟥 无开关(恒开)、含设计外作废、且未乘 X | +| §8.4 | 算奖并入结算 | 🟥 扁平奖数(每奖 1 子),未按 `X × N` | +| §9 | 查牌 / 不查牌模式 | 🟧 无模式开关、明牌功能无服务端支持 | +| §10.1 | 扣卡 / 局数 | 🟥 局数 2/4(应 6/12)、房主扣卡数不符 | +| §10.2-10.3 | 查牌 / 傍王 / 爬坡选项 | 🟥 均无开关 | +| §11 | 交互提示 | 🟨 亮底牌 ✅;甩牌见 §5.4;闲家 3 提示无服务端支持 | + +--- + +## §2 牌局构成 — ✅ 符合 + +`class.paiju.js` `init_cards`:两副牌,点数 3、4 置 `dealowner=-1` 且不发(`if (k != 3 && k != 4) do_dealpai`);`tmpdeal` 共 `28×3 + 8 = 92` 个槽位。发出 92 张(11 种点数 ×4 ×2 + 4 王),去掉 16 张 3/4,与设计一致。分值 `5→5、10→10、K(13)→10` 亦一致(§6.1)。 + +--- + +## §3 主牌顺序 — ✅ 符合(1 处存疑) + +`arith.id_to_code` 以 `flower*100+number` 为底,`A(+13)`、`2(+2000)`、`7(+7000)`、主花色 `(+1000)`、王固定 `9553/9554`,得到 code 降序即设计主牌顺序: + +大王 9554 > 小王 9553 > 正 7(8xxx)> 副 7(7xxx)> 正 2(3xxx)> 副 2(2xxx)> 主花色普通牌 A…5(1xxx)> 副牌 A…5(<1000)。 + +`is_continuous` 正确实现全部相邻段(含 `小王↔正7`、`副2↔主A`、`8↔6`,以及「7 不与 6/8 连」——7 恒为主、不在普通链内)。 + +**存疑(待规则确认)**:不同花色的副 7(code 7107/7207/7307)、副 2(2107/2207/2307)编码互不相等。因此: + +- 排序上三家副 7 被强行分出大小;比大小时 `can_followcard` 又把落在 7xxx/2xxx 段的牌**归一为 7000/2000**(视作同级),二者存在不对称,但因跟牌只能跟同花色、毙牌只能用更大主牌,实测不产生错误赢家。 +- 成对判定 `get_pairlist` 要求 `code 相等`,故「副 7 对 / 副 2 对」只认**同花色两张**,跨花色的两张副 7 不算一对。设计 §3 把副 7 列为同一等级,但未明确「两张不同花色副 7 是否成对」——**需与规则设计者确认**。若规则要求跨花色副牌可成对,则此处需改。 + +--- + +## §4 开局与坐庄 + +### §4.1 发牌(每人 28 + 8 暗牌)— ✅ 符合 + +### §4.2 叫分 — 🟧 缺上限校验 + +- ✅ 步进 5:`mod.jiaofen` 校验 `call % 5 != 0` 拒绝。 +- ✅ 暂定庄家必须叫:首家 `currcall==0 && call==0` 被拒。 +- ✅ 后叫更低:`curr_call != 0 && call >= curr_call` 被拒。 +- ✅ 叫 5 立即上庄、两家不叫则上庄:`paiju.do_callgrade` 实现。 +- 🟧 **缺 `call <= 70` 上限**:`mod.jiaofen` 未限制上界,首家可叫 75、100 等(设计 §4 范围 5~70)。 + +### §4.3 埋牌 — ✅ 符合(选主/埋牌顺序已确认解决) + +庄家得暗牌(`get_seat_cards` 对庄家含 `dealowner==0`),`mod.maipai` 校验埋 8 张且都在庄家手上,`do_burycard` 置 `playround=0` 后由庄家开首轮。 + +✅ **顺序问题已解决**:复查时曾发现 design §4 列举顺序是「埋牌→选主」、而代码是「选主(step2)→埋牌(step3)」,二者相反。经与规则设计者确认,正确顺序为**先选主、后埋牌**(庄家知道主副后才好决定埋哪 8 张)——即**代码本就正确**,是 design §4 的列举笔误。已修正 design §4 的步骤顺序,并新增 design §12「完整牌局游玩流程」明确整局先后。代码无需改动。 + +### §4.4 70 分投降 / 打牌 — 🟥 多处不符 + +- 🟥 **投降资格过宽**:设计仅 **70 分**可投降;代码 `mod.touxiang` 判 `o_paiju.call < 65` 才拒绝,即 **65 分也能投降**。应改为「仅 `call == 70` 允许」。同样地 `shangzhuang.touxiang`、`get_deskinfo` 各阶段的 `touxiang` 字段都用 `call < 65` 判定,需一并改。 +- 🟥 **投降结算数值错**:设计投降为「基础子数 1、庄家每闲家输 1 子」。代码 `get_paiju_account(type=1)`:`upgrade=-99→-2`、`multiple=get_multiple_bycall(70)=1`,庄家 `grade_jf = -2 × 1 × 2 = -4`(每闲家 2 子)。金额翻倍且模型不对。 +- 🟨 **暗牌亮 3 秒未实现**:设计要求 70 分坐庄把 8 张暗牌向两闲家亮 3 秒(投降 / 打牌都要),代码无此下发。 + +### §4.6 坐庄轮换 — ✅ 符合 + +`class.export.js` `makewar` → `do_new_paiju(0)`:首局暂定庄家为 0 号座位(设计 §4.6)。`class.desk.js` `do_prepare`:`result==1(闲赢)||result==2(投降)` 时下一局 `firstseat=(banker+1)%3`(下家),否则连庄。两点均与设计一致。 + +--- + +## §5 出牌规则 + +### §5.1 / §5.2 跟牌 / 毙牌 / 垫牌 — ✅ 基本符合(含 1 处崩溃缺陷) + +- 每轮由上轮最大者先出、每局首轮庄家先出:`new_playround(maxseat)` / `do_burycard→new_playround(1, banker)`。✅ +- 跟同花色、缺门可毙(主牌压过)或垫(任意副牌不争):`arith.get_followcard`/`can_followcard` 按「同花色是否够 / 有无对子 / 有无拖拉机」给必出牌与可出牌,毙牌能否赢由 `cardvalue`(副 7/副 2 归一、跨花色垫牌记 0、主牌对/拖拉机才 >0)裁定。✅ 与 §5.2「毙牌必须用对应牌型」一致。 +- 🐞 **`can_followcard` 引用未定义变量 `tuolaji_list` 会抛异常**(`class.arith.js` 约 801 行,`get.cantype` 为拖拉机型 3xx 分支内 `if (tuolaji_list.length == 0)`,该变量在此作用域未赋值)。当跟牌方手中存在**多个**符合长度的候选拖拉机(`get_followcard` 返回 `cantype=startcardtype`)时命中此分支,`DoPack` 抛错、该次出牌链中断。需修复。 + +### §5.3 拖拉机定义 — ✅ 符合(见 §3 的 `is_continuous`) + +### §5.4 甩牌 — 🟥 原合法性 / 禁令 / 跟牌 / 惩罚多处不符(🟩 第 1/2/4 点已整改,见「整改进度」) + +> **整改状态**:下述第 1(副牌禁甩)、第 2(最大性按全手牌判定)、第 4(甩错惩罚)已实现并验证;第 3(强制跟牌分量优先级)为剩余精化项。以下保留原始不一致描述留痕。 + +1. 🟥 **副牌禁甩未落实**:设计 §5.4.1「所有副牌完全禁止甩牌,无论外面是否剩余该花色副牌」。代码 `can_playcard` 对副牌甩牌只要求「其他两家没有主牌 **且** 没有该副花色(`other_noflower`)」即**放行**(约 415-419、427-438 行),等于「对手缺门时允许甩副牌」,与绝对禁令冲突。 +2. 🟥 **甩牌合法性判定模型不符**:设计 §5.4.2 要求服务端**按全部手牌**判断「对手是否持有能压过甩牌任一分量的更大主牌 / 主对 / 更长主拖拉机」。代码改用出牌过程累积的 `seatlist[i][flower-1][0/1]`、`[4]` 等**报副标志**(`other_noflower`/`other_noflowerpair`),语义是「对手是否已被记录为该花色 / 主牌 / 对子出空」,既非「按全部手牌」也非「更大」而是「有没有」,与设计判定完全不同。 +3. 🟥 **强制跟牌拆解未按分量**:设计 §5.4.4 要求闲家把甩牌拆成各分量(拖拉机优先跟拖拉机、对子优先跟对子…)逐个匹配。代码把甩牌压成单一 `cardtype`(如 `102 两张单张甩`、`203 三对甩`),`get_followcard` 对甩牌型只要求「同花色、张数相等」,不校验对子 / 拖拉机结构(如多对甩分支 `cantype=100+startcount` 只按单张计数)。 +4. 🟥 **甩错惩罚未实现**:设计 §5.4.5 规定甩错要「收回甩牌、本轮只强制打出最小一张、失去甩牌资格」。代码把非法甩牌当作无效输入(`can_playcard` 返回 `result=false`→`mod.chupai` 直接 `return`),无任何惩罚流程。 +5. ✅ **报无主**:`do_playcard` 出牌后自动检测主牌出空并置 `seatlist[..][4][0]=0`,属自动判定(设计允许由服务端精确计算)。 + +--- + +## §6 捡分与扣底 + +### §6.1 分牌 / §6.2 捡分 — ✅ 符合 + +`do_playcard` 仅在 `maxseat != banker` 时累计本轮分;`get_jian_grade` 汇总 `playowner != banker` 的分牌。庄家赢的轮分牌作废、不计入。与设计一致。 + +### §6.3 扣底 — 🟥 倍数表完全不同 + +- ✅ 触发条件「闲家用主牌赢下最后一轮」:`get_bottom_account` 仅在 `maxseat != banker` 时调 `get_bottom_multiple`,后者首末张非主(code<1000)即返回 0(不扣底),与「非主牌赢末轮不算扣底」一致。 +- 🟥 **倍数不符**: + + | 闲家扣底牌型 | 设计倍数 | 代码 `get_bottom_multiple` | + | --- | --- | --- | + | 单张主 | 1 | 2 | + | 主对子 | 2 | 4 | + | 两连对 | 4 | 8 | + | 三连对 | 6 | 16 | + | 四连对 | 8 | 32 | + | 五连对 | 10 | 64 | + | 六连对 | 12 | `undefined`(🐞 落空返回未定义) | + + 代码为近似 `2^k`、且单张即翻倍;设计为线性 `2N`(单张不翻倍)。 + +--- + +## §7 结算:子数与升级 — 🟥 系统性不符 + +设计的四层模型(§7.0):`基础子数(由叫分定) × 判定倍率(大光×3/小光×2/过庄×1/升N级×N) = X`(每「庄–闲」对子的基础金额)。常规算子分界 `Q` 固定 40、升级级距固定 40;勾选爬坡才改用 §7.3 的梯度子数与分段 `Q`。 + +代码实际实现(`get_paiju_account` + `get_multiple_bycall` + `get_upgrade`): + +- `multiple = get_multiple_bycall(call)`:`call>60→1`、`>40→2`、`>0→4`、`≤0→0`。 +- `upgrade = get_upgrade(call, grade)`:大光→4、小光→2、过庄→1、升级→`-(floor((grade-call)/halfcall)+1)`;分界与级距用 `halfcall = ceil(call/2)`。 +- 每对子金额 = `multiple × upgrade`,庄家收两家(×2)、闲家各付(×-1)。 + +**问题**: + +1. 🟥 **基础子数不符**:代码 `multiple∈{1,2,4}` 与设计常规算子 `{65:2, 60:3, 55:4, 50↓:6}` 数值、档位都不同。 +2. 🟥 **大光倍率错**:代码大光 `=4`,设计大光 `×3`。(小光 ×2、过庄 ×1 与设计一致。) +3. 🟥 **小光 / 过庄分界与升级级距错**:代码用 `halfcall=ceil(call/2)`;设计常规算子固定 40。例如叫 65:代码分界 35(35~64 记过庄),设计分界 40(40~64 才过庄,35~39 仍是小光)。 +4. 🟥 **无爬坡开关**:设计 §7.3 是「勾选爬坡才生效」的可选梯度;代码无任何房间选项分支,只有一套公式。巧合的是 `halfcall` 对低档(40/35→20、30/25→15、20/15→10、10/5→5)恰等于爬坡的 `Q`,但对高档(65→35≠40、60→30≠40、50→25≠40)不等,且基础子数 `{1,2,4}` 与常规 `{2,3,4,6}`、爬坡 `{2,3,4,6,7,8,…}` 都不符——即代码既不是常规算子、也不是爬坡,而是第三套数值。 + +**常规算子每对子最终子数对照(代码 vs 设计)**: + +| 叫分 | 判定 | 设计 | 代码 | +| --- | --- | --- | --- | +| 65 | 大光 / 小光 / 过庄 / 升1 / 升2 | 6 / 4 / 2 / 2 / 4 | 4 / 2 / 1 / 1 / 2 | +| 60 | 同上 | 9 / 6 / 3 / 3 / 6 | 8 / 4 / 2 / 2 / 4 | +| 55 | 同上 | 12 / 8 / 4 / 4 / 8 | 8 / 4 / 2 / 2 / 4 | +| 50 | 同上 | 18 / 12 / 6 / 6 / 12 | 8 / 4 / 2 / 2 / 4 | +| 40 | 大光 / 小光 /(无过庄)/ 升1 / 升2 | 18 / 12 / — / 6 / 12 | 16 / 8 /(误记过庄 4)/ 4 / 8 | + +(叫 40 一行还暴露分界问题:设计 `call≤40` 无过庄档,代码却在 grade 20~39 记出「过庄」。代码升级子数 = `multiple(=4) × 级数`,故升 1 级 4 子、升 2 级 8 子。) + +> **70 分打牌**同样落在本套公式里且同样不符:代码 `multiple = get_multiple_bycall(70) = 1`(设计打牌基础子数应为 2),`halfcall = 35`(设计 `Q = 40`);大光 4×1=4、小光 2、过庄 1,而设计应为大光 6、小光 4、过庄 2。 + +--- + +## §8 算奖规则 — 🟥 归属、开关、数值均不符 + +### §8.1 常规算奖(只庄家) — 🟥 + +- ✅ 基础组合计数正确:`get_chongguan` 中 `3 王→1、4 王→3`、`6/7/8 个 7→1/2/3`、`6/7/8 个 2→1/2/3`。 +- 🟥 **只应计庄家,代码却算三家**:`get_paiju_account` 对 0/1/2 号位都调 `get_chongguan` 并两两结算(`grade_cg = cg×2 − 另两家`)。设计 §8.1「常规算奖只认庄家身份,不看闲家手牌」。 +- 🟥 **设计外的作废规则**:`do_obsolete_with_call` 引入「冲关必叫」「四王必踢(`call>60`)」,设计中不存在。 +- 🟥 **连对链算奖失效(传参 bug)**:设计 §8.1「三 / 四王在有正 7 及以后连续对子时每对 +1 奖」。但 `get_chongguan` 在正常 / 投降结算被以 `mainflower = -1` 调用(`get_paiju_account` 中 `if (type != 2) _flower = -1;`)。此时 `id_to_code` 不加 `+1000`,正 7 与副 7 都落到 7xxx、主花色普通对子不被识别为主,`is_continuous(小王, 正7)` 因 `is_zheng7` 只认 8xxx 而返回假——连对链一对也加不上。即该奖项**实质未生效**。 +- 🟥 **≥10 老主误给奖**:`if (_total >= 10) re.count += _total - 9`(「10 个老主判断」)。设计 §8.2 明示固定主牌 ≥10 只亮牌、**不算奖**。 +- 🟥 **庄家算奖手牌快照错(含底牌与已埋牌)**:设计 §8「算奖依据静态初始手牌——庄家是**埋牌完成后的那 28 张**,闲家是发牌后的 28 张」。代码 `get_paiju_account` 用 `get_seat_cards_owner(seat)` 取牌喂给 `get_chongguan`,而该函数按 `dealowner ∈ {seat+1, 0}` 取牌、**不排除 `playround==0`(已埋)**:庄家因此拿到「28 张发牌 + 8 张底牌 = 36 张」,把**已埋进底牌的 8 张也计入算奖**,闲家则是正确的 28 张。庄家的常规算奖与傍王都因此可能虚高(例如把埋掉的王/7/2 仍算进去)。应改为庄家取「埋牌后保留的 28 张」(排除 `playround==0`)。 + +### §8.2 亮牌 — 🟧 未实现 + +设计 §8.2 的四档亮牌门槛(固定主 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6,向闲家亮统计信息,受「不查牌」模式抑制)在服务端无对应计算与下发。 + +### §8.3 傍王(可选) — 🟥 + +- 🟥 **无开关(恒开)**:`grade_w`(傍王得分)在 `get_paiju_account` 中无条件计算。设计 §8.3 / §10.3 傍王需勾选才生效。 +- 🟥 **含设计外作废**:`grade_w` 复用 `obsolete` 置 0 逻辑;设计傍王「每张王算一奖、庄闲都算」,无作废概念。 + +### §8.4 算奖并入结算 — 🟥 未乘 X + +- 设计 §8.4:某玩家 `N` 奖,则另外两人各额外付 `X × N`(`X` 为 §7 第三层的每对子子数)。 +- 代码:`grade_cg`、`grade_w` 是**扁平奖数两两差**(`cg×2 − 另两家`),每奖等价 1 子,**未乘 `X`**。举例设计(X=6、庄家 1 奖)应从每闲家收 6 子;代码只收 1 子/家。 +- ✅ 结构上「所有两两之间(含闲–闲)都结算」这一点与设计一致,仅倍率错。 + +--- + +## §9 牌局查看模式 — 🟧 + +- 报无主后,服务端确有记录他家剩余主牌数 / 对子数(`seatlist[..][4]` 与花色标志),并通过 `chupai*`/`PushCards` 的 `info`、`seatlist`、`baozhu` 下发,供客户端展示——方向正确。 +- 🟧 **无「可查牌 / 不查牌」模式开关**:`baozhu` 恒发、`seatlist` 恒带,未按房间设置抑制(设计 §9 / §10.2 的不查牌模式应屏蔽这些信息)。 +- 🟧 **「明牌」(查看他家具体主牌)无服务端支持**:服务端不会向闲家下发他家的具体主牌牌面。 + +--- + +## §10 房间设置选项 — 🟥 + +### §10.1 扣卡方式 / 局数 — 🟥 + +- 🟥 **局数不符**:`export.get_asetcount` 返回 `roomtype[0]==1→2`、`==2→4`。设计 §10.1 为 **6 局 / 12 局**。(疑为占位 / 测试值,与 `do_playcard` 中被注释的 `round==2` 测试残留同源。) +- 🟥 **房主扣卡数不符**:`get_needroomcard` 房主档(`roomtype[2]==1`)返回 1/2;设计房主扣卡为 6 局 2 张 / 12 局 4 张。AA 档(`roomtype[2]==2`)返回 1/2,与设计 AA「每人 1 张 / 2 张」一致。 + +### §10.2 查牌模式 — 🟥 无开关(见 §9) + +### §10.3 附加规则(傍王 / 爬坡,可同时勾选)— 🟥 均无开关(见 §7.4、§8.3) + +--- + +## §11 牌局交互提示 — 🟨 + +- ✅ 牌局结束亮底牌:`get_bottom_account` 结算恒带 `bottom.cards`。 +- 甩牌相关提示:见 §5.4(甩牌本身实现不符)。 +- 🟨 **闲家 3 提示(踩 / 没分 / 有分)无服务端支持**:设计 §11 要求闲家出牌时提供这三种对家提示;服务端未计算 / 下发对应信号(当前仅有 `grade` 等原始信息,需客户端或后续补充)。 + +--- + +## 附录:运行期代码缺陷汇总(与规则合规相对独立,但都需修) + +| # | 位置 | 现象 | 影响 | +| --- | --- | --- | --- | +| D1 | `class.arith.js` `can_followcard`(约 801 行) | 引用未赋值的 `tuolaji_list.length` | 跟牌方有多个候选拖拉机时抛异常、出牌中断 🐞 | +| D2 | `class.arith.js` `get_bottom_multiple`(六连对分支) | 六连对时无返回值 | 返回 `undefined`,扣底倍数计算异常 🐞 | +| D3 | `class.arith.js` `can_playcard`(约 276 行) | `do_returnfalse;` 漏写 `()`,语句空转 | 「一次最多 14 张」上限失效,超量出牌被当作合法 🐞 | +| D4 | `class.paiju.js` `do_choiceflower`(约 417-421 行) | `for` 循环体为空、取出 `o_card` 后无操作 | 死代码(主牌重编码实际在 `id_to_code` 动态完成),无害但应清理 🟨 | + +> D1~D3 均为真实缺陷;虽多数属「代码写错」而非「规则理解错」,但会导致对应规则(跟拖拉机校验、扣底、出牌张数上限)无法正确执行,需按缺陷修复处理。 + +--- + +## 总体判断 + +- **流程骨架**(发牌、叫分推进、选主埋牌、逐轮出牌收束、断线重连快照、战绩存储)实现完整且大体正确。 +- **牌型与大小体系**(编码、排序、相邻、跟牌 / 毙牌 / 垫牌的必出可出计算)设计良好、基本符合规则,主要缺陷是 D1 崩溃与甩牌子系统。 +- **不符合集中在「算分 / 算奖 / 房间选项」三块**:§6.3 扣底倍数、§7 算子全套、§8 算奖归属与并入方式、§10 局数 / 扣卡 / 各类开关——这几处需按 `design.md` 重做,是后续整改的重点。 +- 结合数值特征(`multiple` 只有 1/2/4、局数 2/4、扣底 2^k、含「冲关必叫 / 四王必踢」等设计外机制),**服务端很可能是在早期或另一套规则版本上实现的,与当前 `design.md` 已系统性脱节**,而非个别笔误。 diff --git a/server/games/erqiwang/docs/design/design.md b/server/games/erqiwang/docs/design/design.md index e64a634..4b42c9c 100644 --- a/server/games/erqiwang/docs/design/design.md +++ b/server/games/erqiwang/docs/design/design.md @@ -30,6 +30,7 @@ 9. [牌局查看模式](#9-牌局查看模式) 10. [房间设置选项](#10-房间设置选项) 11. [牌局交互提示](#11-牌局交互提示) +12. [完整牌局游玩流程](#12-完整牌局游玩流程) --- @@ -99,12 +100,16 @@ - **暂定庄家必须叫分,不能"不叫"**:叫分从暂定庄家开始,他必须报出一个 5 ~ 70 之间的具体分数,不存在"暂定庄家弃权、三家都不叫"这种情况;起始叫分确定之后,后面的玩家才可以选择"叫出更低的分数"或"不叫"。 - 若某玩家直接叫出 5 分,则无人能再压过,立即确定为庄家。 - 若一家叫分后,另外两家都选择"不叫",则该叫分玩家成为本局庄家。 -3. 庄家确定后可以查看暗牌(只有庄家可见),将 8 张暗牌摸到手上,同时从手牌中选出 8 张牌重新扣下("埋牌"),作为"底牌",供最后判断"扣底"使用。 -4. **若叫分为 70 分**:埋牌完成后,庄家必须立即在"投降"与"打牌"之间二选一(埋牌步骤本身不受这个选择影响,两种情况都要先正常埋牌): - - **选择投降**:不再进行下面的选主牌步骤,本局直接结束,按 7.1 节"70 分坐庄 · 投降"的固定结果结算(基础子数 1 个,庄家直接输)。 - - **选择打牌**:进入下一步,正常选主牌花色、开始出牌,按 7.1 / 7.2 节"70 分坐庄 · 打牌"的完整判定流程结算。 - - 叫分低于 70 分时没有这个分支,直接进入下一步。 -5. 庄家选定一个花色作为本局主牌(见第 3 节)。 +3. 庄家确定后摸暗牌(摸完后庄家共 36 张): + - **非 70 分坐庄**:桌面 8 张暗牌**只有庄家可见**,庄家查看后摸入手牌。 + - **70 分坐庄**:在庄家摸暗牌**之前**,先把这 8 张暗牌**向所有玩家(含两个闲家)亮出 3 秒**,然后才由庄家把它们摸入手牌。 +4. **选主 / 投降(同一决策点,二选一,互斥)**: + - **选主**:庄家在 4 个花色中选定一个作为本局主牌(见第 3 节)→ 进入下一步埋牌、随后出牌。 + - **投降**:仅当**叫分为 70 分**时,此决策点才额外提供"投降"选项;选择投降表示放弃本局,**直接结束、不再选主、不埋牌、不出牌**,按 7.1 节"70 分坐庄 · 投降"的固定结果结算(基础子数 1 个,庄家直接输;算奖仍照常,见 8.4 节)。选主与投降互斥——**选了花色就等于放弃投降、正常打牌;选了投降就不再选主**。 + - 叫分 < 70 分时没有投降选项,只能选主。 + - (70 分坐庄的 8 张暗牌向所有玩家亮 3 秒,发生在上一步"庄家摸暗牌之前",见步骤 3。) + - **前端表现**:此阶段界面显示 4 个花色选主按钮,且**每个花色按钮上要显示该花色在庄家手中有多少对**(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮。 +5. **埋牌**(仅"选主/打牌"路径):庄家从 36 张里选出 8 张牌重新扣下("埋牌"),作为"底牌",供最后判断"扣底"使用;埋牌后庄家保留 28 张。(**本局先选主、后埋牌**——庄家先定主牌花色,知道主副后再决定埋哪 8 张。) 6. 坐庄轮换规则(决定下一局的"暂定庄家"): - 第一局的暂定庄家为最0号座位的玩家。 - 若庄家本局获胜(庄赢),下一局暂定庄家仍为该玩家(连庄)。 @@ -259,7 +264,7 @@ 3. **判定结果 → 子数倍率**:过庄 / 小光 / 大光 / 升级第几级,各自对应一个子数倍率,乘上第 1 层的基础子数,得到这一局的**子数结算结果 `X`**(前三层,即"算子",到此为止)。`X` 是庄家与**每一个**闲家之间"一对一"结算的基础金额:庄家赢(过庄 / 小光 / 大光)时,两个闲家各输给庄家 `X` 子;闲家赢(升级)时,庄家各输给两个闲家 `X` 子。 4. **算奖 → 结算倍率**:第 8 节算出的"奖数"(8.1 常规算奖 + 8.2 傍王,各玩家分别求和)**是"持有奖数的这个人"从其余两名玩家那里各自多收一笔钱的独立结算线,作用对象是这个人本身,不是整场结算的统一系数**——某玩家 P 这一局总共有 `N_P` 奖,则**另外两名玩家(不分庄闲)都要各自额外付给 P:`X × N_P` 子**。三名玩家可能同时都持有各自的奖数,会产生最多 3 条互相独立的额外支付线,与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。算奖的计算过程本身不受叫分、过庄 / 小光 / 大光、升级影响(只看手牌结构),详见 8.3 节的完整举例。 -> **例外**:70 分坐庄选择投降时不走第 1~3 层模型——直接固定基础子数 1 个、庄家输掉,不比较捡分、不判定过庄 / 小光 / 大光,见 7.1 末尾说明;但第 4 层算奖仍然照常叠加(庄家埋牌后的手牌结构依然存在,见 8.4 节)。 +> **例外**:70 分坐庄选择投降时不走第 1~3 层模型——直接固定基础子数 1 个、庄家输掉,不比较捡分、不判定过庄 / 小光 / 大光,见 7.1 末尾说明;但第 4 层算奖仍然照常叠加(投降是选主阶段的选择、未选主也未埋牌,按庄家"发牌 + 暗牌"共 36 张、无主牌花色计算,见 8.4 节)。 **查表索引**:本节按叫分档位给出了完整判定表,位置如下: @@ -285,7 +290,7 @@ - **选择投降**:基础子数按 **1 个**算,庄家直接输 1 个子(此时局面还没有真正出牌捡分,不经过 7.2 的大光 / 小光 / 升级判定,是一次性的固定结算)。 - **选择打牌**:基础子数按 **2 个**算(与 65 分相同),之后**照样按 7.2 节的方法,用捡分总数判定大光 / 小光 / 过庄 / 升级**,不是不细分——完整判定表见 7.2.1。 -70 分叫庄时,发牌时桌面留下的那 8 张暗牌(见第 4 节;不是埋牌埋下的底牌)需要向两个闲家亮出来 3 秒,投降和打牌两种情况都适用。 +70 分叫庄时,发牌时桌面留下的那 8 张暗牌(见第 4 节;不是埋牌埋下的底牌)需要**在庄家把它们摸入手牌之前、向所有玩家亮出 3 秒**,之后庄家才摸入手中;投降和打牌两种情况都适用(因为亮牌发生在选主 / 投降决策之前)。 ### 7.2 捡分 vs 叫分:庄家的过庄 / 小光 / 大光,闲家的升级(常规算子) @@ -522,7 +527,7 @@ **8.1 常规算奖只认庄家的身份,不看闲家的手牌是否达标**:即使某位闲家的手牌结构凑巧也满足 8.1 表里的条件(比如手里也有 3 张王),这份常规算奖也**不会**计入这位闲家——常规算奖从规则上就只针对庄家一人。闲家想要获得算奖,只能靠 8.3 傍王(且必须房间勾选了这条可选规则)。 -**算奖依据的手牌快照时间点**:不管是 8.1(只看庄家)还是 8.3 傍王(庄闲都算),都按**静态初始手牌**计算——庄家是埋牌完成后的那一刻手牌,闲家是发牌完成后的那 28 张手牌;这个快照全程固定,不随之后的出牌、被吃、被打出而改变。 +**算奖依据的手牌快照时间点**:不管是 8.1(只看庄家)还是 8.3 傍王(庄闲都算),都按**静态初始手牌**计算——庄家是埋牌完成后的那一刻手牌(28 张),闲家是发牌完成后的那 28 张手牌;这个快照全程固定,不随之后的出牌、被吃、被打出而改变。**例外**:70 分投降局庄家未选主也未埋牌,其快照为"发牌 + 暗牌"共 36 张(无主牌花色),见 8.4 节。 **算奖(8.1 / 8.3)与亮牌(8.2)是两个互不干涉的独立概念**:算奖是结算阶段"该给多少额外结算分"的规则;亮牌只是出牌开始前"要不要向对手公开一部分手牌统计信息"的展示规则。两者各自有自己的触发条件,只是恰好都以庄家埋牌后的手牌结构为依据,因此部分门槛数值相同——但达成亮牌的条件不代表一定触发算奖,反之亦然,具体对照见 8.2 末尾的说明。 @@ -588,7 +593,7 @@ 以下三点已确认: - **庄家倒庄(闲家达标升级)时,8.1 常规算奖仍然生效**:算奖只看庄家埋牌后的手牌结构,与本局谁输谁赢无关。 -- **70 分投降时,仍叠加算奖倍率**:投降只是跳过了第 1~3 层的叫分 / 捡分判定,庄家埋牌后的手牌结构依然存在,8.1(以及勾选了傍王时的 8.3)照常计算并叠加。 +- **70 分投降时,仍叠加算奖倍率**:投降只是跳过了第 1~3 层的叫分 / 捡分判定(此时 `X = 1`)。投降是选主阶段的选择,庄家**未选主、未埋牌**,故算奖按庄家"发牌 + 暗牌"共 36 张手牌计算——因没有选定主牌花色,**没有连对链**,只有三 / 四王、六~八个 7、六~八个 2 这些组合,以及(勾选傍王时)按王数的傍王奖计入;8.1(及勾选傍王时的 8.3)照常计算并叠加。 - **算奖对象的身份限制会带进 `N` 的计算**:8.1 只算庄家、8.3 傍王勾选后才不分庄闲(见第 8 节开头说明),把这名玩家实际适用的分量求和,才是他自己的 `N`;"其余两人向他支付 `X × N`"这条支付规则本身不因身份而改变,但闲家的 `N` 从一开始就不可能包含 8.1 的分量。 --- @@ -625,9 +630,62 @@ ## 11. 牌局交互提示 +- 选主阶段:4 个花色选主按钮上各显示"该花色在庄家手中的对子数"(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮——选主与投降是同一决策点的互斥选择(见第 4 节)。 - 首家出主牌时可以甩牌,甩牌的生效条件、跟牌规则、甩错惩罚详见 5.4 节。 - 牌局结束需要亮出底牌。 - 闲家在出牌时需要有 3 个提示选项: 1. **踩**:提示对家"我能大过庄家"。 2. **没分**:提示对家"我手上没分了"。 3. **有分**:提示对家"我手上有分"。 + +--- + +## 12. 完整牌局游玩流程 + +本节把前面各节的规则串成一局(一个"小局")从头到尾的完整流程,供整体理解与实现参照。这里只给出**步骤与先后顺序**,每一步的具体细则以其对应章节为准(避免与细则重复而产生分歧)。 + +### 12.1 一个小局的完整流程 + +1. **开局准备**:房间按第 10 节的设置(局数、扣卡方式、傍王、爬坡、查牌)建好后开战。第一局的暂定庄家为 0 号座位;之后每局按 4.6 的轮庄规则确定暂定庄家。 + +2. **发牌**(第 2 节):92 张牌洗匀,三家各摸 28 张,剩余 8 张扣在桌面作为"暗牌"。 + +3. **叫分坐庄**(4.2):由暂定庄家起、按逆时针依次叫分。暂定庄家必叫(5~70 分、步进 5,不能"不叫");其后每家只能叫比当前更低的分,或选择"不叫";有人叫出 5 分即立即坐庄,或一家叫分后另外两家都"不叫"也即坐庄。叫分越低表示庄家对"压住闲家捡分"越有信心,对应的基础子数越高(第 7 节)。 + +4. **看暗牌**:庄家坐定后摸暗牌(摸完共 36 张)。非 70 分:8 张暗牌仅庄家可见。**70 分坐庄:庄家摸暗牌之前,先把这 8 张暗牌向所有玩家亮 3 秒,再由庄家摸入手牌**(4.4 / 第 4 节)。 + +5. **选主 / 投降(同一决策点,二选一,互斥)**(第 4 节): + - **选主**:庄家选定一门花色为本局主牌(大王、小王、所有花色的 2、7 恒为主牌)→ 进入下一步埋牌。前端此阶段每个花色按钮上要显示该花色在庄家手中的对子数;70 分坐庄时并列显示投降按钮。 + - **投降**:仅 70 分坐庄才有此选项;选投降即放弃本局,**不选主、不埋牌、不出牌**,直接按 7.1"70 分 · 投降"结算(算奖仍按下方 8 计,但用庄家 36 张、无主牌花色)。选花色就等于放弃投降、正常打牌。 + - (70 分的 8 张暗牌亮 3 秒发生在步骤 4"摸暗牌之前"。) + +6. **埋牌**(第 4 节,仅"选主/打牌"路径):庄家从 36 张里选 8 张重新扣下作为"底牌"(供最后"扣底"判断),保留 28 张。(**先选主、后埋牌**。) + +7. **出牌对局**(第 5、6 节): + - 第一轮固定由庄家先出;此后每轮由上一轮牌面最大的一方先出。 + - 首家出什么花色 / 牌型,其余两家按 5.1~5.3 跟牌(同花色不够时可用主牌"毙牌"抢权,或"垫牌"不争)。 + - 首家出主牌时可"甩牌"(5.4):一次性打出多组主牌组合;服务端按全场手牌判定"最大性",甩错则按 5.4.5 收回、只强制打出最小一张。副牌一律不能甩。 + - 每轮比大小定出胜者:闲家赢下的那一轮,牌面上的分牌(5→5 分、10→10 分、K→10 分)计入闲家捡分;庄家赢的轮分牌作废(6.1~6.2)。 + - 过程中:有玩家主牌出空需"报无主",可查牌模式下为全体展示他家主牌数量 / 对子结构(第 9 节);庄家手牌达到 8.2 的门槛时触发"亮牌"。 + +8. **末轮扣底**(6.3):所有手牌出完后,若最后一轮由闲家**用主牌**赢下,则庄家埋的 8 张底牌翻开,其中的分按闲家赢牌的牌型翻倍(单张 ×1、主对 ×2、N 连对 ×2N)后计入闲家捡分。 + +9. **小局结算**(第 7、8 节,两部分同时结算): + - **算子**:闲家总捡分(含扣底)与庄家叫分比较 → 判定庄家的**过庄 / 小光 / 大光**,或闲家的**升级第几级** → 结合叫分档的基础子数,得到庄家与每个闲家"一对一"的结算子数 `X`(勾选爬坡则改用 7.3 的梯度与分段)。 + - **算奖**:以静态初始手牌(正常局:庄家埋牌后 28 张、闲家发牌后 28 张;**70 分投降局**:庄家用发牌+暗牌共 36 张、无主牌花色)计算奖数 `N` —— 常规算奖(8.1)**只计庄家**;勾选傍王(8.3)则庄闲每张王各算一奖。持有 `N` 奖的玩家从另外两人各多收 `X × N`(8.4)。 + - 以上两部分加总即各家本局盈亏;牌局结束需亮出底牌(第 11 节)。 + +10. **进入下一局**(4.6):庄家本局赢则连庄(暂定庄家仍是他);庄家输(闲家捡分达标 / 庄家投降)则暂定庄家顺延到其下家。回到第 2 步开新的一小局。 + +### 12.2 大局与结束 + +按房间设定的局数(6 局或 12 局,见第 10 节)打满全部小局后,进行大局结算(累计各家总分并记录战绩);若中途解散,则按当前累计分结算。 + +### 12.3 阶段速览 + +``` +建房(§10) → 发牌(§2) → 叫分坐庄(§4.2) → 看暗牌 → 选主(§3) → 埋牌(§4) + → [70分:投降/打牌决策(§4.4)] → 出牌对局(§5,§6.1-6.2) → 末轮扣底(§6.3) + → 小局结算=算子(§7)+算奖(§8) → 轮庄(§4.6) → 下一局 …… 打满局数 → 大局结算 +``` + diff --git a/server/games/erqiwang/docs/protocol/packet_protocol.md b/server/games/erqiwang/docs/protocol/packet_protocol.md index 4ddc377..46ee054 100644 --- a/server/games/erqiwang/docs/protocol/packet_protocol.md +++ b/server/games/erqiwang/docs/protocol/packet_protocol.md @@ -42,7 +42,7 @@ | seat | 整数 | 叫分者的位置序号 | | call | 整数 | 分数,0表示不叫 | | currcall | 整数 | 当前叫到的分数 | -| multiple | 整数 | 倍数 | +| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)| | nextseat | 整数 | 下一个叫分者的位置序号 | | countdown | 整数 | 叫分倒计时 | @@ -58,11 +58,12 @@ | call | 整数 | 分数,0表示不叫 | | banker | 整数 | 庄家的位置序号 | | grade | 整数 | 庄家的叫分 | -| multiple | 整数 | 倍数 | -| bottomcards | 数组 | 底牌 | -| cards | 数组 | 拿了底牌后手上的牌,包括底牌,庄家才有此属性,闲家没有该属性 | +| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)| +| bottomcards | 数组 | 8 张暗牌。**庄家恒有**;**闲家仅当 70 分坐庄时才有**(供 3 秒亮牌用),非 70 分时闲家没有该属性(暗牌只有庄家可见,design §4)| +| ancard3s | 整数 | 仅 70 分坐庄时出现且为 `1`:表示庄家把暗牌摸入手前,需将 `bottomcards` 这 8 张暗牌向所有玩家亮出 3 秒(design §4/§7.1);非 70 分无此属性 | +| cards | 数组 | 拿了暗牌后手上的牌(36 张,含暗牌),庄家才有此属性,闲家没有该属性 | | countdown | 整数 | 选主倒计时 | -| touxiang | 整数 | 是否允许投降 0:不允许 1:允许 | +| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同为选主阶段(step2)的决策,见 touxiang 包与 design §4 | --- @@ -76,7 +77,7 @@ | playerid | 整数 | 玩家id | | gameid | 字符 | 游戏id | | roomcode | 整数 | 房间号 | -| seat | 整数 | 庄家的位置序号(`mod.js` 中校验 `o_paiju.banker != seat`,只有庄家能投降;选主/埋牌/投降阶段均可发起) | +| seat | 整数 | 庄家的位置序号。投降是**选主阶段(step 2)与选主互斥**的选择:`mod.touxiang` 仅在 `step==2`、`banker==seat`、`call==70` 时受理;点投降即直接结算,不选主、不埋牌、不出牌(design §4) | --- @@ -131,8 +132,9 @@ | --- | --- | --- | | cards | 数组 | 埋牌后手上的牌,去掉了埋牌,庄家才有此属性,闲家没有该属性 | | bottomcards | 数组 | 埋的牌,庄家才有此属性,闲家没有该属性 | -| seat | 整数 | 出牌者的位置序号 | +| seat | 整数 | 出牌者的位置序号(即将首出的庄家)| | countdown | 整数 | 出牌倒计时 | +| liangpai | json | 庄家亮牌信息(design §8.2),**只有闲家、且可查牌模式、且庄家埋牌后手牌达标时才有**。结构(各字段按是否达标出现):`zhu` 主牌总数、`zhupair` 主对子数、`zhutuo` 主拖拉机组数(固定主牌王+2+7 总数 ≥10 时有这三项);`wang` 王数(王≥3 时);`qi` 7 数(7≥6 时);`er` 2 数(2≥6 时)。都不达标则无此属性 | --- @@ -158,14 +160,15 @@ | 参数名 | 类型 | 说明 | | --- | --- | --- | | seat | 整数 | 出牌者的位置序号 | -| cards | 数组 | 出的牌id列表 | -| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) | -| count | 整数 | 出牌数量 | +| cards | 数组 | 实际打出的牌id列表;**甩错时**(见 `shuaicuo`)为被强制打出的那一张最小主牌单张 | +| shuaicuo | 整数 | 甩错标志,仅甩错时出现且为 `1`(design §5.4.5:甩牌未通过最大性判定,整套甩牌收回,本轮只强制打出最小一张、失去本轮甩牌资格);正常出牌无此属性 | +| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,**仅可查牌模式下发**(不查牌无此属性,design §9)。5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) | +| count | 整数 | 出牌数量(甩错时为 1) | | flower | 整数 | 出牌花色 | | cardtype | 整数 | 出牌牌型 | | nextseat | 整数 | 下一个出牌者的位置序号 | | countdown | 整数 | 下一个出牌者的出牌倒计时 | -| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0` 即为 1),供客户端在可查牌模式下展示他家主牌数量/对子结构 | +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,供客户端展示他家主牌数量/对子结构并出现"明牌"按钮;**不查牌模式恒为 0**(design §9)| | cardsinhand | 数组 | 出牌者出牌后手上剩下的牌id列表,只有出牌者才有此属性 | --- @@ -178,10 +181,10 @@ | --- | --- | --- | | seat | 整数 | 出牌者的位置序号 | | cards | 数组 | 出的牌id列表 | -| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) | +| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,**仅可查牌模式下发**(不查牌无此属性,design §9)。5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) | | nextseat | 整数 | 下一个出牌者的位置序号 | | countdown | 整数 | 出牌倒计时 | -| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0` 即为 1),供客户端在可查牌模式下展示他家主牌数量/对子结构 | +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,供客户端展示他家主牌数量/对子结构并出现"明牌"按钮;**不查牌模式恒为 0**(design §9)| | cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 | --- @@ -194,16 +197,43 @@ | --- | --- | --- | | seat | 整数 | 出牌者的位置序号 | | cards | 数组 | 出的牌id列表 | -| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) | +| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,**仅可查牌模式下发**(不查牌无此属性,design §9)。5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) | | nextseat | 整数 | 下一个出牌者的位置序号 | | countdown | 整数 | 出牌倒计时 | -| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0` 即为 1),供客户端在可查牌模式下展示他家主牌数量/对子结构 | +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,供客户端展示他家主牌数量/对子结构并出现"明牌"按钮;**不查牌模式恒为 0**(design §9)| | cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 | | maxseat | 整数 | 本轮出牌谁最大,只有本轮最后一个玩家出牌后才有此属性 | | grade | 整数 | 本轮闲家得分,只有本轮最后一个玩家出牌后且闲家有得分才有此属性 | --- +## 13.5 明牌(mingpai,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:mingpai + +查看另外两家手中全部主牌的具体牌面(design §9)。服务端仅在**可查牌模式、出牌阶段(step5)、且已有玩家报无主**(`have_baofu()`)时受理;否则忽略。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 请求者的位置序号 | + +## 13.6 明牌(mingpai,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:mingpai (只回发给请求者) + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 请求者的位置序号 | +| others | 数组 | 另外两家各自未出的全部主牌,元素 `{ seat: 位置序号, zhucards: [主牌id列表] }` | + +> "再点一次取消查看"是客户端的显示开关,无需再请求服务端。 + +--- + ## 14. 结算(jiesuan) 发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiesuan @@ -239,27 +269,31 @@ | --- | --- | --- | | banker | 整数 | 庄家的位置序号,-1:无庄 | | call | 整数 | 庄家叫分,-1:无叫分 | -| multiple | 整数 | 叫分倍数 `get_multiple_bycall(call)`:call>60→1,call>40→2,call>0→4;无叫分(call≤0)→0 | +| 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 | 整数 | 升级级数:0中途解散 1过庄 2小光 4大光 -1倒庄 <-1升几级 -99投降 | +| 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": [], // 王牌或冲关牌列表 - "obsolete": 0, // 冲关作废标志 0不作废 1作废 - "chongguan": 0, // 冲关数 - "grade_cg": 0, // 冲关得分 - "grade_w": 0, // 傍王得分 - "grade_jf": 0, // 捡分得分 - "grade": 0, // 本局总分 + "cards": [], // 算奖相关牌(王/冲关牌)列表 + "chongguan": 0, // 常规算奖奖数(只有庄家才计入自己的 N) + "wang": 0, // 手牌中的王数(傍王按此计奖) + "naward": 0, // 该家总奖数 N =(庄家?常规算奖:0)+(傍王?王数:0) + "grade_aw": 0, // 算奖得分 = X×(2Ni−Nj−Nk),X 为每对子子数 + "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)`。 + **account(大局结算包)** > 仅当打到最后一局(`o_paiju.idx >= o_room.asetcount`)或房间中途解散(解散结算)时,`jiesuan` 包才会带上此分组;普通的中间局结算包没有 `account` 字段(`class.paiju.js` `get_paiju_account`)。 @@ -311,7 +345,7 @@ | count | 整数 | 总局数 | | idx | 整数 | 当前局数 | | PlayerInfo | 数组 | 三个玩家目前的总积分 `[0,0,0]` | -| step | 整数 | 牌桌状态:1发完牌叫分 2选主 3埋牌 4投降 5出牌 6结算 | +| step | 整数 | 牌桌状态:1发完牌叫分 2选主/投降 3埋牌 5出牌 6结算(无独立投降阶段——投降在 step2 与选主互斥;埋牌后直接进入 step5 出牌) | | MyCards | 数组 | 自己手上的牌 | **CallRun(不在叫分阶段无此属性)** @@ -321,58 +355,50 @@ | seat | 整数 | 当前叫分位置 | | countdown | 整数 | 叫分倒计时 | | nowcall | 整数 | 当前叫分 | -| multiple | 整数 | 倍数 | +| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)| | call | 数组 | 三家叫分 `[null,0,65]`,null还未叫分,0不叫,>0叫了多少分 | -**ChooseMain(不在选主阶段无此属性)** +**ChooseMain(step 2 选主/投降阶段,不在此阶段无此属性)** | 参数名 | 类型 | 说明 | | --- | --- | --- | | banker | 整数 | 庄家 | | call | 整数 | 叫分 | -| multiple | 整数 | 倍数 | +| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)| | countdown | 整数 | 选主倒计时 | -| bottomcards | 数组 | 底牌 | -| touxiang | 整数 | 是否允许投降 0:不允许 1:允许 | +| bottomcards | 数组 | 8 张暗牌,**只有庄家有此属性**(暗牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)| +| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同一决策点,见 touxiang 包与 design §4 | -**BuryCards(不在埋牌阶段无此属性)** +> 选主阶段前端需在每个花色按钮上显示"该花色在庄家手中的对子数"(design §4/§11)——庄家的完整手牌由 `MyCards` 提供(庄家为 36 张),对子数由前端据此计算,服务端不额外下发。 + +**BuryCards(step 3 埋牌阶段,不在此阶段无此属性)** | 参数名 | 类型 | 说明 | | --- | --- | --- | | banker | 整数 | 庄家 | | call | 整数 | 叫分 | -| multiple | 整数 | 倍数 | +| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)| | flower | 整数 | 主牌花色 | | countdown | 整数 | 埋牌倒计时 | -| bottomcards | 数组 | 底牌 | -| touxiang | 整数 | 是否允许投降 0:不允许 1:允许 | +| bottomcards | 数组 | 8 张暗牌,**只有庄家有此属性**(暗牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)| -**Surrender(不在投降阶段无此属性)** +> 埋牌阶段无投降(投降是 step2 与选主互斥的选择,选主后即不可再投降)。 + +**PushCards(step 5 出牌阶段,不在此阶段无此属性)** | 参数名 | 类型 | 说明 | | --- | --- | --- | | banker | 整数 | 庄家 | | call | 整数 | 叫分 | -| multiple | 整数 | 倍数 | -| flower | 整数 | 主牌花色 | -| countdown | 整数 | 出牌倒计时 | -| bottomcards | 数组 | 埋的牌,只有庄家有此属性 | -| touxiang | 整数 | 是否允许投降 0:不允许 1:允许 | - -**PushCards(不在出牌阶段无此属性)** - -| 参数名 | 类型 | 说明 | -| --- | --- | --- | -| banker | 整数 | 庄家 | -| call | 整数 | 叫分 | -| multiple | 整数 | 倍数 | +| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)| | flower | 整数 | 主牌花色 | | countdown | 整数 | 出牌倒计时 | | bottomcards | 数组 | 埋的牌,只有庄家有此属性 | | grade | 整数 | 当前的捡分分数 | | gradecards | 数组 | 当前的捡分分牌,只有闲家有此属性 | | playproc | json | 当前出牌情况 `o_paiju.playproc`:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(数组下标=位置序号) | -| seatlist | 数组 | 三家座位牌况,元素结构同上文 chupai 包的 `info`(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`) | +| seatlist | 数组 | 三家座位牌况,元素结构同上文 chupai 包的 `info`(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`)。**仅可查牌模式下有此属性**(design §9)| +| liangpai | json | 庄家亮牌信息,结构同 maipai 包的 `liangpai`;**仅可查牌模式、且请求者为闲家、且庄家达标时有**(供闲家重连后仍能看到亮牌,design §8.2)| | pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次,内层 3 个数组按位置序号存该轮各家出的牌id(已按大小排序) | **Balance(不在结算阶段无此属性)**