diff --git a/server/games/erqiwang/docs/compliance/02-测试计划.md b/server/games/erqiwang/docs/compliance/02-测试计划.md index 4ba5804..4342e07 100644 --- a/server/games/erqiwang/docs/compliance/02-测试计划.md +++ b/server/games/erqiwang/docs/compliance/02-测试计划.md @@ -146,13 +146,13 @@ ## 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 全部服务端可验证章节均有正/反/边界单测: - **§2 构成**:`test_deal` 92张/去3-4/三家28+底8/分值。 - **§3 编码/排序**:`test_arith` order_cards 降序=设计顺序、is_continuous 相邻链逐段+反例。 - **§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**:捡分(结算集成间接)、扣底倍数+触发。 - **§7**:算子常规+爬坡逐档、每档分界+升3级、结算集成(大光/过庄/升级/爬坡/傍王/投降)。 - **§8**:算奖 get_chongguan、亮牌 get_liangpai(含 6个7/固定主9-10边界)、算奖并入 X×N(傍王对照)。 diff --git a/server/games/erqiwang/test/test_arith.js b/server/games/erqiwang/test/test_arith.js index 4902bac..1fbe3fc 100644 --- a/server/games/erqiwang/test/test_arith.js +++ b/server/games/erqiwang/test/test_arith.js @@ -102,6 +102,18 @@ t.eq('甩牌 副牌禁甩 无shuaicuo', r.shuaicuo, undefined); t.eq('甩牌 主单张(非甩)可出', play([big(1)], [[A1], [K1]]).result, true); t.eq('甩牌 副对子(非甩)可出', play([fK1, fK2], [[], []]).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 ============ 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, fA, fB], { tractors: [], pairs: 1, singles: 1 }), false); 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 主牌顺序 / 相邻链 ============ // order_cards 结果应为 design §3 从大到小:大王>小王>正7>副7>正2>副2>主A>主K…主5>副牌 diff --git a/server/games/erqiwang/test/test_paiju.js b/server/games/erqiwang/test/test_paiju.js index 7355556..9e83c2d 100644 --- a/server/games/erqiwang/test/test_paiju.js +++ b/server/games/erqiwang/test/test_paiju.js @@ -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: {} }); 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);