二七王:修复跟牌判定依赖客户端入参顺序(可翻转本轮胜负与结算)

can_followcard 尾段计算 cardvalue/noflower/nopair 时用的是入参原始数组
followcards(客户端提交顺序),而 get_pairlist/get_tuolaji_list 都按降序
相邻取对。端到端实测:闲家用主拖拉机毙牌且为末轮,降序提交时闲家赢下本轮
(捡分 40、扣底 ×4),把同一手牌打乱成 [51,50,105,104] 后对子漏判、
cardvalue 归 0,变成庄家赢、闲家 0 分大光、不扣底——同一手合法牌仅靠数组
顺序就能翻转胜负、捡分归属与最终结算,同源问题还能抹掉 noflower 缺门标志
污染 §9 下发给全场的牌况表。

改为在 min_ary_deduct 削减 _followcards 之前另存完整排序快照 _sortfollow,
尾段 8 处全部改用它(_followcards 会被削减、不可复用)。

顺带给 get_disbandRoom 加空守卫:平台在 makewar 后立刻置 battlestate=1,
而首局是延迟 1 秒创建的,这段窗口内解散会在 curr_paiju() 的 undefined 上
解引用抛异常、打断平台解散链路;现返回 null 走平台既有的「不带 deskfree」分支。

补 8 条回归(入参顺序无关性 5 条 + 端到端 1 条 + 解散空守卫 2 条),
已用「回退修复 → 用例转红」反验有效性;全套单测 401 项全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 12:18:04 +08:00
co-authored by Claude Opus 5
parent 685b629854
commit ce4c972b7b
4 changed files with 97 additions and 13 deletions
+32
View File
@@ -104,4 +104,36 @@ t.eq('混 拖·2副散+补2主 不压', beat(f([N9(1), N8(1), zK(1), zQ(1), zJ(1
// 首家副拖拉机:手中1对该副花色(数量仍不足) + 主牌 → 有对也压不过(混合非纯主拖)
t.eq('混 拖·副对(不足)+补2主 不压', beat(f([N9(1), N9(2), zK(1), zQ(1), zJ(1)], [N9(1), N9(2), zK(1), zQ(1)], 4, 2, 302)), false);
// ===================== 入参顺序无关性(服务端不得依赖客户端提交的数组顺序)=====================
// 回归:can_followcard 尾段曾用入参原始顺序的 followcards 算 cardvalue/noflower/nopair,
// 而 get_pairlist/get_tuolaji_list 按降序相邻取对——客户端只要打乱数组顺序,
// 就能把本该压过的主拖拉机算成牌面 0(改写本轮胜者/捡分/扣底),或抹掉缺门标志污染 §9 牌况表。
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
// 主拖拉机毙牌:有序 vs 乱序,结果必须完全一致且都判为「压过」
const HT = [zK(1), zK(2), zQ(1), zQ(2), oth(9)];
const ht1 = f(HT, [zK(1), zK(2), zQ(1), zQ(2)], 4, 2, 302); // 降序
const ht2 = f(HT, [zK(1), zQ(1), zK(2), zQ(2)], 4, 2, 302); // 乱序(对子被拆开摆放)
t.eq('序无关 主拖毙牌·有序压过', beat(ht1), true);
t.eq('序无关 主拖毙牌·乱序同样压过', beat(ht2), true);
t.eq('序无关 主拖毙牌·两种顺序结果全等', same(ht1, ht2), true);
// 主对子毙牌
const HP = [zK(1), zK(2), oth(9)];
t.eq('序无关 主对毙牌·两种顺序结果全等',
same(f(HP, [zK(1), zK(2)], 2, 2, 201), f(HP, [zK(2), zK(1)], 2, 2, 201)), true);
// 同花色拖拉机跟牌(非毙牌)的牌面值也不得随顺序变
const HS = [J(1), J(2), T(1), T(2), N8(1)];
t.eq('序无关 同花色跟拖·两种顺序结果全等',
same(f(HS, [J(1), J(2), T(1), T(2)], 4, 2, 302), f(HS, [J(1), T(1), J(2), T(2)], 4, 2, 302)), true);
// 混合出牌:3 张该副花色 + 1 主牌。把副牌摆在首尾也必须记为「已缺该花色」
const HM = [K(1), Q(1), J(1), zK(1)];
const hm1 = f(HM, [zK(1), K(1), Q(1), J(1)], 4, 2, 302); // 降序:主牌在首
const hm2 = f(HM, [K(1), zK(1), Q(1), J(1)], 4, 2, 302); // 乱序:首尾都是副牌
t.eq('序无关 混合出牌·有序记缺门', hm1.noflower, true);
t.eq('序无关 混合出牌·乱序同样记缺门', hm2.noflower, true);
t.eq('序无关 混合出牌·两种顺序结果全等', same(hm1, hm2), true);
process.exit(t.done('follow') ? 0 : 1);