服务端四个阶段只下发 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>
33 KiB
二七王 服务端实现 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门控(不查牌一律不下发);新增mingpaiRPC(可查牌 + 已报无主时查看他家全部主牌)。剩客户端展示(面板/明牌按钮)。 - 🟩 §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(甩错惩罚)均已实现并验证。以下保留原始不一致描述留痕。
- 🟥 副牌禁甩未落实:设计 §5.4.1「所有副牌完全禁止甩牌,无论外面是否剩余该花色副牌」。代码
can_playcard对副牌甩牌只要求「其他两家没有主牌 且 没有该副花色(other_noflower)」即放行(约 415-419、427-438 行),等于「对手缺门时允许甩副牌」,与绝对禁令冲突。 - 🟥 甩牌合法性判定模型不符:设计 §5.4.2 要求服务端按全部手牌判断「对手是否持有能压过甩牌任一分量的更大主牌 / 主对 / 更长主拖拉机」。代码改用出牌过程累积的
seatlist[i][flower-1][0/1]、[4]等报副标志(other_noflower/other_noflowerpair),语义是「对手是否已被记录为该花色 / 主牌 / 对子出空」,既非「按全部手牌」也非「更大」而是「有没有」,与设计判定完全不同。 - 🟥 强制跟牌拆解未按分量:设计 §5.4.4 要求闲家把甩牌拆成各分量(拖拉机优先跟拖拉机、对子优先跟对子…)逐个匹配。代码把甩牌压成单一
cardtype(如102 两张单张甩、203 三对甩),get_followcard对甩牌型只要求「同花色、张数相等」,不校验对子 / 拖拉机结构(如多对甩分支cantype=100+startcount只按单张计数)。 - 🟥 甩错惩罚未实现:设计 §5.4.5 规定甩错要「收回甩牌、本轮只强制打出最小一张、失去甩牌资格」。代码把非法甩牌当作无效输入(
can_playcard返回result=false→mod.chupai直接return),无任何惩罚流程。 - ✅ 报无主:
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)。
问题:
- 🟥 基础子数不符:代码
multiple∈{1,2,4}与设计常规算子{65:2, 60:3, 55:4, 50↓:6}数值、档位都不同。 - 🟥 大光倍率错:代码大光
=4,设计大光×3。(小光 ×2、过庄 ×1 与设计一致。) - 🟥 小光 / 过庄分界与升级级距错:代码用
halfcall=ceil(call/2);设计常规算子固定 40。例如叫 65:代码分界 35(3564 记过庄),设计分界 40(4064 才过庄,35~39 仍是小光)。 - 🟥 无爬坡开关:设计 §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已系统性脱节,而非个别笔误。