Files
erqiwang_youle/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md
T
joywayerandClaude Opus 5 b6aeba412f 二七王:把「无超时托管」确认为规则,写入 design §11
服务端四个阶段只下发 countdown、没有任何定时器,此前被本轮核对记
为待办 D3(等规则设计者定义超时默认动作)。规则设计者已确认:
**维持现状,不需要超时动作**。

这属于规则本身如此、而非实现缺口。若不固化下来,下一轮核对会再次
把它当缺陷提出、甚至有人去补一套「超时自动出牌」,反而违背规则。
故写进 design.md 作为权威约定:

- design §11 新增一条:倒计时只作展示、不触发任何自动操作——不自
  动叫分/选主/埋牌/出牌,也不判负、不跳过;轮到谁而谁不操作,牌局
  就停在这一步等待。并加醒目说明:这是确认过的规则、不是待实现的
  缺口,牌局停住由玩家走房间解散流程收场(按当前累计分结算,
  §12.2),后续核对不要为它补超时托管。
  (解散路径已核实:平台 server_room 侧有 freeroom 解散倒计时,并
  回调子游戏 export.get_disbandRoom → get_paiju_account(2) 出解散
  结算包,与 §12.2 一致。)
- 协议文档新增 0.3「countdown 只是展示用的秒数」:说明服务端无定
  时器、归零不做任何事,并要求客户端不得在归零时做乐观界面推进
  (原 0.3 顺延为 0.4)。
- 合规 01:D3 由「🟧 中等/未修」改为「✅ 规则如此」,并说明 server
  dev-guide「服务端自动操作复用真人链路」约束的是「若要做代打就必
  须复用真人链路」,本局不做代打故不适用;「尚未处置」小节改为
  「无」,11 项全部收口。
- 合规 02:撤掉「仍无自动化覆盖」中的超时项,改为说明不需要超时用
  例,并反向标注:若将来有人给服务端加了超时代打,那才是违反
  design §11、应由核对拦下。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 19:05:24 +08:00

335 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 二七王 服务端实现 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 判定表逐格验证,见"验证"列)。下方"结论摘要"与各节详评仍保留**原始不一致**描述以留痕;本节记录当前状态。
> **二次全量复核结论(限定范围)**:对整改后代码做了一次独立对抗性复核(§7/§8 算子算奖、§5 出牌甩牌、§4 开局投降暗牌阶段机、§2/§3/§9/§10 编码查牌房间,逐条比对 + Node 实测)。结果:**就 design.md 的玩法规则维度而言,服务端符合**,且未发现整改引入的符号/下标/边界错、无 step4 死代码残留、投降 36 张快照与 X×N 算奖均正确。
> - 复核时发现的唯一未落地项 **§5.4.4 强制跟牌分量拆解已于本轮修复**:合法甩牌在 `can_playcard` 记录分量需求 `shuai_demand`(拖拉机长度/对子/单张),存入 `playproc`;跟牌时 `do_playcard` 调 `arith.flush_follow_ok` 在"同花色凑张数、主牌最大化"之上追加"拖拉机→对子→单张"的强制分量匹配(能凑同长主拖必凑、能凑主对必凑,不得拆散去垫)。已用 Node 正反面/边界用例验证。
>
> ⚠️ **该结论只覆盖"玩法规则是否符合 design.md"这一个维度**。第三轮核对(见下方「第三轮核对」)在**平台接入、客户端入参校验、下发面一致性、协议红线**四个维度上又发现 1 项阻断级、3 项严重、若干中等缺陷——它们不属于 design.md 的玩法条款,所以逐节核对没有覆盖到,但同样会让服务端跑不起来或被利用。**"符合 design.md" ≠ "实现完整正确"**,后续复核不要再把前者当成后者。
| 项 | 位置 | 整改内容 | 验证 |
| --- | --- | --- | --- |
| 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`/`flush_follow_ok`/`remove_pairs`)+ `can_playcard` + `do_playcard` + `mod.chupai` | 副牌绝对禁甩;最大性改为**按对手全部主牌**逐分量判定;甩错惩罚(收回、只打最小一张、下发 `shuaicuo`);**§5.4.4 强制跟牌分量拆解**(拖拉机→对子→单张优先级,`shuai_demand`+`flush_follow_ok`) | ✅ 分解/最大性/各路径/跟牌分量用例 |
| 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 强制跟牌分量拆解(`flush_follow_ok`:拖拉机→对子→单张优先级)均已实现并用 Node 验证。
- 🟩 **§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 提示(踩/没分/有分)**:`mod.tishi` 服务端转发给对家已实现;剩客户端展示。
- 🟧 **客户端建房配置**:`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局 / 房主扣卡 / 傍王开 / 常规算子 / 可查牌。
---
## 第三轮核对(平台接入 / 入参 / 下发面 / 协议红线)
> 前两轮只沿 design.md 逐节核对玩法规则,因此**整类问题在核对范围之外**。本轮补上这些维度,结论如下(均已修复并有单测回归)。
| # | 严重度 | 位置 | 问题 | 处置 |
| --- | --- | --- | --- | --- |
| A1 | 🟥 阻断 | `server/youle/app.js` | 只加载 `games2/jinxianmahjong/mod.js`,**从未加载 `games/erqiwang/mod.js`**,全仓平台侧对 erqiwang 零引用 → `youle_erqiwang` 模块从不创建、`pack.route="erqiwang"` 无法命中三层路由,**整个子游戏运行时不可达** | 已在 app.js 补注册(server dev-guide README 允许触碰的唯一平台文件) |
| B1 | 🟥 严重 | `class.paiju.js` `check_cards_inhand` | 不查重复牌id:`do_playcard([方块K,方块K])` 实测 `result=true, cardtype=201`——同一张牌被判成一对,可伪造拖拉机/甩牌分量,且该分牌 score 被重复累加;`maipai` 传 8 个相同 id 实测**只埋下 1 张**,庄家带 35 张进入出牌阶段 | 新增 `check_cards_valid`(非空数组 / 0~107 整数 / 无重复)作为唯一入参校验入口 |
| B2 | 🟥 严重 | `mod.js` `maipai`/`chupai` | 越界 id 或非数组入参在解引用处抛异常,中断 `DoPack`;且 `cards.length` 读在校验之前 | 校验前置于 length 判断之前 |
| B3 | 🟥 严重 | `arith.get_multiple_bycall` + 6 个调用点 | 写死 `climb=false`:**爬坡房对局全程界面显示常规算子子数,与结算 `aset.multiple` 不符**(叫 45 分:界面 6、结算 7) | 删除该兼容包装,全部改调 `get_base_bycall(call, cfg.climb)`,与结算同源 |
| C1 | 🟥 红线 | 全部下发包 + 所有失败分支 | **全仓无 `data.success`**;失败一律裸 `return`、不回任何包,前端只能干等倒计时。违反 server dev-guide「成败唯一是 data.success、主动推送必须自带」与 client dev-guide「发包只请求、收包才表现」 | 成功包统一补 `success:true`;新增 `ERR` 码表 + `do_sendfail`,每条失败分支回 `success:false + errcode` 给请求者 |
| D1 | 🟧 中等 | `arith.is_continuous` | `is_8`/`is_6` 只判 `code % 100`、不校验花色:`is_continuous(♥8=308, ♣6=206)` 实测为 `true`。`get_chongguan` 是唯一扫描整手牌(含各花色副牌)的调用点,连对链走到主 8 后会被任意花色的一对 6 错误接续、多算奖(design §8.1 要求 A~5 段必须是主花色) | 追加「编码相差 2」约束(同花色 8/6 恒差 2) |
| D2 | 🟧 中等 | `export.get_deskinfo` pushlist | ① 内层循环上界误用轮次数当座位数,实测 5 轮时内层被撑成长度 5 且含 `undefined`;② 排序传的是上个循环泄漏的 `pai.flower`(=最后一张牌大王的花色 5),等于按「无主牌」排序 | 内层固定 3、改用 `paiju.flower`,并对缺失轮次加守卫 |
| D3 | ✅ 规则如此 | `class.desk.js` | 下发了 `countdown`,但服务端无任何定时器做超时托管,玩家不操作则牌局停住等待 | **规则设计者确认:维持现状、不做超时动作**。已写入 design §11「倒计时只作展示,不触发任何自动操作」并注明不是缺口;牌局停住由玩家走房间解散流程收场(按当前累计分结算,design §12.2)。server dev-guide「服务端自动操作复用真人链路」约束的是「若要做代打就必须复用真人链路」,本局不做代打,故不适用 |
| D4 | 🟨 轻微 | `paiju.do_playcard` | 报无主后只更新出牌者自己那一格,B/C 要等各自轮到出牌才补上,界面最多滞后两次出牌(design §9 要求「为全体三人显示另外两家」) | 改为一旦 `have_baofu()` 成立即按实际手牌刷新三个座位 |
| D5 | 🟨 轻微 | `can_playcard` / `chupai1` | `cardtype` 只能表达单一牌型,甩牌被压平成 1xx/2xx(「主K对+主5」→103),客户端连"这是一次甩牌"都看不出来 | `chupai1` 增发 `shuai` 分量构成(与 `shuai_demand` 同源) |
| D6 | 🟧 中等 | `export.get_deskinfo` | 不查牌模式下 `pushlist`(已打出牌历史)仍照发。规则设计者已确认:**不可查牌模式必须屏蔽出牌历史,只有可查牌模式才能查看** | 已按确认修复:`pushlist` 按 `cfg.nocheck` 门控;design §9 同步改写为无歧义表述(四项功能整体开关 + 「当前轮桌面牌不属于查牌、两种模式都可见」的边界) |
| D7 | 🟨 轻微 | `check_cards_inhand` | 牌已出/已埋时 `return;`(undefined)而非 `false` | 已改为 `return false` |
### 尚未处置
**无。** 本轮发现的 11 项已全部收口:
- A1/B1/B2/B3/C1/D1/D2/D4/D5/D7 共 10 项为缺陷,已改代码 + 补单测;
- D6(不查牌是否屏蔽出牌历史)原为规则歧义,已由规则设计者拍板「屏蔽」,改 design §9 + 代码 + 单测;
- D3(超时托管)原列为待外部输入,已由规则设计者拍板 **维持现状、不做超时动作**,属**规则如此、并非缺陷**,已写入 design §11 固化,避免后续核对反复把它当缺口重提。
---
## 结论摘要
> **注**:下表是**首轮核对**的初始发现快照(保留以记录起点)。其中 §4.4、§5.4、§6.3、§7、§8.1-8.4、§9、§10、§11 等标 🟥/🟧/🟨 的项**后续均已修复并验证**——当前逐节状态以本文档后面的分节详情及全套单测(270 项全绿)为准。
| 设计章节 | 主题 | 结论(首轮快照,非当前状态) |
| --- | --- | --- |
| §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(最大性按全手牌判定)、第 3(强制跟牌分量优先级,`flush_follow_ok`)、第 4(甩错惩罚)**均已实现并验证**。以下保留原始不一致描述留痕。
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 扣卡方式 / 局数 — ✅ 已整改(下述为原始不一致留痕)
> **现状(已改)**:`get_asetcount` 按位串返回 6/12;`get_needroomcard` 房主 6局2张/12局4张、AA 6局1张/12局2张,均符合 §10.1。以下为原始(数组下标版)不一致记录。
- 🟥(原始)**局数不符**:旧 `export.get_asetcount` 返回 `roomtype[0]==1→2`、`==2→4`。设计 §10.1 为 **6 局 / 12 局**。
- 🟥(原始)**房主扣卡数不符**:旧 `get_needroomcard` 房主档返回 1/2;设计房主扣卡为 6 局 2 张 / 12 局 4 张。
### §10.2 查牌模式 — 🟥 无开关(见 §9)
### §10.3 附加规则(傍王 / 爬坡,可同时勾选)— 🟥 均无开关(见 §7.4、§8.3)
---
## §11 牌局交互提示 — 🟩
- ✅ 牌局结束亮底牌:`get_bottom_account` 结算恒带 `bottom.cards`。
- ✅ 选主对子数 / 70 分投降按钮门控:随 `shangzhuang`/`xuanzhu` 阶段包下发(见 §4)。
- ✅ 甩牌相关提示:见 §5.4(甩牌实现已核对一致)。
- ✅ **闲家 3 提示(踩 / 没分 / 有分)**:`mod.tishi`(design §11)——闲家在出牌阶段(step5)发 踩/没分/有分,服务端不校验真实性、只 `SendPack` 转发给对家(另一闲家 `3-banker-seat`);庄家不参与。协议见 packet_protocol §13.7/13.8,测试见 `test_rpc`。
---
## 附录:运行期代码缺陷汇总(与规则合规相对独立,但都需修)
| # | 位置 | 现象 | 影响 |
| --- | --- | --- | --- |
| 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` 已系统性脱节**,而非个别笔误。