二七王测试:补全甩牌/跟甩牌全部情形

回应"甩牌与跟甩牌测试是否完整"的核对,补齐缺口:
- 甩牌首出(§5.4):甩错由主对分量/拖拉机分量/双对之一被压引发(原仅测单张分量);
  纯双单张、纯双对(不连)、单张+拖拉机、单+对+拖三分量混合的合法;副牌禁甩·对手非缺门
- 甩错惩罚实际执行(§5.4.5):do_playcard 收回甩牌、只强制打出最小一张、其余留手;合法甩牌整套打出
- 跟甩牌(§5.4.4):拖拉机分量退化到单张(对子也不够)、多组拖拉机分量、混合demand逐分量对位
- 集成:首出合法甩牌→跟牌方拆散主对(违反§5.4.4)→ do_playcard 拒;正确对位→接受

全部单测 226 项断言全绿(arith 101 / paiju 31 等)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 01:34:02 +08:00
co-authored by Claude Opus 4.8
parent 339e6e99e1
commit c29aec140c
3 changed files with 58 additions and 2 deletions
@@ -146,13 +146,13 @@
## 5. 当前状态小结 —— ✅ DoD 达成,P0/P1/P2 全部完成 ## 5. 当前状态小结 —— ✅ DoD 达成,P0/P1/P2 全部完成
**共 204 项断言全绿**(`test_arith` 88 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_follow` 17 / `test_paiju` 22 / `test_rpc` 34)。 **共 226 项断言全绿**(`test_arith` 101 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_follow` 17 / `test_paiju` 31 / `test_rpc` 34)。
design 全部服务端可验证章节均有正/反/边界单测: design 全部服务端可验证章节均有正/反/边界单测:
- **§2 构成**:`test_deal` 92张/去3-4/三家28+底8/分值。 - **§2 构成**:`test_deal` 92张/去3-4/三家28+底8/分值。
- **§3 编码/排序**:`test_arith` order_cards 降序=设计顺序、is_continuous 相邻链逐段+反例。 - **§3 编码/排序**:`test_arith` order_cards 降序=设计顺序、is_continuous 相邻链逐段+反例。
- **§4**:叫分坐庄 do_callgrade、`mod.jiaofen` 入参校验、投降 touxiang 条件、70分上庄暗牌亮牌下发、坐庄轮换 do_prepare。 - **§4**:叫分坐庄 do_callgrade、`mod.jiaofen` 入参校验、投降 touxiang 条件、70分上庄暗牌亮牌下发、坐庄轮换 do_prepare。
- **§5**:正常跟牌/毙牌/垫牌(§5.1/5.2)、甩牌+甩错(§5.4)、跟甩牌分量拆解(§5.4.4)。 - **§5**:正常跟牌/毙牌/垫牌(§5.1/5.2);**甩牌全面覆盖(§5.4)**——合法(纯单/纯对/单+拖/单+对+拖三混)、甩错(单张/主对/拖拉机分量各自被压、双对之一被压)、副牌禁甩(对手缺门与非缺门)、甩错惩罚**实际执行**(do_playcard 只打最小一张、其余留手);**跟甩牌全面覆盖(§5.4.4)**——对子必出、拖拉机必出/退化对子/退化到单张、多组拖拉机、混合demand逐分量对位、主牌最大化、无主全垫,以及"首出甩牌→跟牌违规→do_playcard 拒"的集成。
- **§6**:捡分(结算集成间接)、扣底倍数+触发。 - **§6**:捡分(结算集成间接)、扣底倍数+触发。
- **§7**:算子常规+爬坡逐档、每档分界+升3级、结算集成(大光/过庄/升级/爬坡/傍王/投降)。 - **§7**:算子常规+爬坡逐档、每档分界+升3级、结算集成(大光/过庄/升级/爬坡/傍王/投降)。
- **§8**:算奖 get_chongguan、亮牌 get_liangpai(含 6个7/固定主9-10边界)、算奖并入 X×N(傍王对照)。 - **§8**:算奖 get_chongguan、亮牌 get_liangpai(含 6个7/固定主9-10边界)、算奖并入 X×N(傍王对照)。
+18
View File
@@ -102,6 +102,18 @@ t.eq('甩牌 副牌禁甩 无shuaicuo', r.shuaicuo, undefined);
t.eq('甩牌 主单张(非甩)可出', play([big(1)], [[A1], [K1]]).result, true); t.eq('甩牌 主单张(非甩)可出', play([big(1)], [[A1], [K1]]).result, true);
t.eq('甩牌 副对子(非甩)可出', play([fK1, fK2], [[], []]).result, true); t.eq('甩牌 副对子(非甩)可出', play([fK1, fK2], [[], []]).result, true);
t.eq('甩牌 主拖拉机(单分量不受最大性约束)', play([K1, K2, Q1, Q2], [[big(1), big(2)], []]).result, true); t.eq('甩牌 主拖拉机(单分量不受最大性约束)', play([K1, K2, Q1, Q2], [[big(1), big(2)], []]).result, true);
// —— 甩牌首出补全:各分量被压导致甩错 + 各牌型组合合法(design §5.4.2/§5.4.3)——
r = play([big(1), K1, K2], [[A1, id(2, 1, 1)], []]); // 大王单+主K对;对手主A对
t.eq('甩牌 甩错·主对被更大主对压', r.shuaicuo, true);
r = play([big(1), Q1, Q2, id(1, 1, 11), id(2, 1, 11)], [[A1, id(2, 1, 1), K1, K2], []]); // 大王+主QJ两连对;对手主AK两连对
t.eq('甩牌 甩错·拖拉机被同长更大主拖压', r.shuaicuo, true);
t.eq('甩牌 合法·纯双单张(大王+主K)', play([big(1), K1], [[P5], []]).result, true);
t.eq('甩牌 合法·纯双对不连(主K对+主9对)', play([K1, K2, id(1, 1, 9), id(2, 1, 9)], [[P5, P6], []]).result, true);
r = play([K1, K2, id(1, 1, 9), id(2, 1, 9)], [[id(1, 1, 11), id(2, 1, 11)], []]); // 主9对被主J对压
t.eq('甩牌 甩错·双对之一被压', r.shuaicuo, true);
t.eq('甩牌 合法·单张+拖拉机', play([big(1), K1, K2, Q1, Q2], [[P5, P6], []]).result, true);
t.eq('甩牌 合法·单+对+拖三混', play([big(1), id(1, 1, 10), id(2, 1, 10), K1, K2, Q1, Q2], [[P5, P6], []]).result, true);
t.eq('甩牌 副禁甩·对手非缺门仍禁', play([fK1, f9], [[fK2, id(1, 2, 10)], []]).result, false);
// ============ §5.4.4 跟甩牌强制分量拆解 flush_follow_ok ============ // ============ §5.4.4 跟甩牌强制分量拆解 flush_follow_ok ============
const J1 = id(1, 1, 11), N9a = id(1, 1, 9), N9b = id(2, 1, 9), P8 = id(1, 1, 8); const J1 = id(1, 1, 11), N9a = id(1, 1, 9), N9b = id(2, 1, 9), P8 = id(1, 1, 8);
@@ -120,6 +132,12 @@ t.eq('跟牌 有两对却打5散→拒', ffo(hand6, [K1, N9a, P6, J1, P8], { tra
t.eq('跟牌 主牌最大化(有主用尽)', ffo([K1, Q1, fA, fB], [K1, Q1, fA], { tractors: [], pairs: 1, singles: 1 }), true); t.eq('跟牌 主牌最大化(有主用尽)', ffo([K1, Q1, fA, fB], [K1, Q1, fA], { tractors: [], pairs: 1, singles: 1 }), true);
t.eq('跟牌 留主牌垫副→拒', ffo([K1, Q1, fA, fB], [K1, fA, fB], { tractors: [], pairs: 1, singles: 1 }), false); t.eq('跟牌 留主牌垫副→拒', ffo([K1, Q1, fA, fB], [K1, fA, fB], { tractors: [], pairs: 1, singles: 1 }), false);
t.eq('跟牌 无主全垫副', ffo([fA, fB, fC, fD], [fA, fB, fC], { tractors: [], pairs: 1, singles: 1 }), true); t.eq('跟牌 无主全垫副', ffo([fA, fB, fC, fD], [fA, fB, fC], { tractors: [], pairs: 1, singles: 1 }), true);
// —— 跟甩牌补全:拖分量退化到单张 / 多组拖拉机 / 混合demand(design §5.4.4)——
t.eq('跟牌 拖退化·1对不够必打+其余单', ffo([K1, K2, N9a, P8, P6, id(1, 1, 5)], [K1, K2, N9a, P8, P6], { tractors: [2], pairs: 0, singles: 1 }), true);
t.eq('跟牌 拖退化·有1对却拆散→拒', ffo([K1, K2, N9a, P8, P6, id(1, 1, 5)], [K1, N9a, P8, P6, id(1, 1, 5)], { tractors: [2], pairs: 0, singles: 1 }), false);
t.eq('跟牌 双拖·两组2连拖必打', ffo([K1, K2, Q1, Q2, id(1, 1, 10), id(2, 1, 10), N9a, N9b], [K1, K2, Q1, Q2, id(1, 1, 10), id(2, 1, 10), N9a, N9b], { tractors: [2, 2], pairs: 0, singles: 0 }), true);
t.eq('跟牌 混合·拖+对+单各对位', ffo([K1, K2, Q1, Q2, N9a, N9b, P8, id(1, 1, 5)], [K1, K2, Q1, Q2, N9a, N9b, P8], { tractors: [2], pairs: 1, singles: 1 }), true);
t.eq('跟牌 混合·有对却拆散→拒', ffo([K1, K2, Q1, Q2, N9a, N9b, P8, id(1, 1, 5)], [K1, K2, Q1, Q2, N9a, P8, id(1, 1, 5)], { tractors: [2], pairs: 1, singles: 1 }), false);
// ============ §3 主牌顺序 / 相邻链 ============ // ============ §3 主牌顺序 / 相邻链 ============
// order_cards 结果应为 design §3 从大到小:大王>小王>正7>副7>正2>副2>主A>主K…主5>副牌 // order_cards 结果应为 design §3 从大到小:大王>小王>正7>副7>正2>副2>主A>主K…主5>副牌
+38
View File
@@ -86,4 +86,42 @@ t.eq('扣底 庄家赢不扣底', bo3.data.bottom.multiple, undefined);
const bo4 = P.get_bottom_account(mkBottomPaiju(1, [id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)], [[id(1, 4, 13), 10]], 1), { data: {} }); const bo4 = P.get_bottom_account(mkBottomPaiju(1, [id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)], [[id(1, 4, 13), 10]], 1), { data: {} });
t.eq('扣底 两连对拖赢 ×4', bo4.data.bottom.multiple, 4); t.eq('扣底 两连对拖赢 ×4', bo4.data.bottom.multiple, 4);
// ============ §5.4.5 甩错惩罚执行 do_playcard ============
const A = require('../class.arith.js');
// 造首出场景:庄家(0)手牌与两闲家主牌,step5 本轮首出
function mkFirstPlay(flower, dealMap) {
const cards = [];
for (let i = 0; i < 108; i++) cards[i] = { id: i, dealowner: 99, playround: -1, playowner: -1, score: 0, flower: A.id_to_flower(i), number: A.id_to_number(i) };
for (const [cid, owner] of dealMap) cards[cid].dealowner = owner;
const sl = () => [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]];
const o = { step: 5, banker: 0, flower, cards, seatlist: [sl(), sl(), sl()], playproc: {} };
o.method = { have_baofu: () => o.seatlist.some(s => s[4][0] == 0) };
P.new_playround(o, 1, 0);
return o;
}
// 庄家甩 大王+主K对;seat1 持主A对(能压主K对) → 甩错:收回甩牌、只强制打出最小一张(主K)、其余留手
const fp = mkFirstPlay(1, [[big(1), 1], [id(1, 1, 13), 1], [id(2, 1, 13), 1], [id(1, 1, 1), 2], [id(2, 1, 1), 2]]);
const re = P.do_playcard(fp, [big(1), id(1, 1, 13), id(2, 1, 13)]);
t.eq('甩错执行 shuaicuo=true', re.shuaicuo, true);
t.eq('甩错执行 只打1张且为主K', re.cards.length === 1 && A.id_to_number(re.cards[0]) === 13, true);
t.eq('甩错执行 大王未出(留手)', fp.cards[big(1)].playround, -1);
t.eq('甩错执行 打出的主K已出', fp.cards[re.cards[0]].playround > 0, true);
// 对照:合法甩牌 do_playcard 正常打出整套、不甩错
const fp2 = mkFirstPlay(1, [[big(1), 1], [big(2), 1], [id(1, 1, 13), 1], [id(1, 1, 5), 2], [id(1, 1, 6), 3]]); // 大王对+主K单;对手仅小主牌
const re2 = P.do_playcard(fp2, [big(1), big(2), id(1, 1, 13)]);
t.eq('合法甩牌 do_playcard 非甩错', !!re2.shuaicuo, false);
t.eq('合法甩牌 整套3张打出', re2.cards.length, 3);
// §5.4.4 集成:首出合法甩牌 → 跟牌方违反分量拆解 → do_playcard 拒;正确对位 → 接受
const fpf = mkFirstPlay(1, [
[big(1), 1], [id(1, 1, 12), 1], [id(2, 1, 12), 1], // 庄家 大王+主Q对(合法甩牌)
[id(1, 1, 11), 2], [id(2, 1, 11), 2], [id(1, 1, 9), 2], [id(1, 1, 8), 2] // seat1 主J对+主9+主8
]);
const lead = P.do_playcard(fpf, [big(1), id(1, 1, 12), id(2, 1, 12)]);
t.eq('甩牌集成 首出合法(记录shuai_demand)', lead.result && !lead.shuaicuo, true);
const badF = P.do_playcard(fpf, [id(1, 1, 11), id(1, 1, 9), id(1, 1, 8)]); // 拆散主J对
t.eq('甩牌集成 跟牌拆散主对→拒', badF.result, false);
const goodF = P.do_playcard(fpf, [id(1, 1, 11), id(2, 1, 11), id(1, 1, 9)]); // 用主J对对位
t.eq('甩牌集成 跟牌用主对→接受', goodF.result, true);
process.exit(t.done('paiju') ? 0 : 1); process.exit(t.done('paiju') ? 0 : 1);