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

33 KiB
Raw Blame History

二七王 服务端实现 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
§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(3564 记过庄),设计分界 40(4064 才过庄,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 已系统性脱节,而非个别笔误。