From 339e6e99e167199551769a9c44c3dec75c64f28d Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 5 Jul 2026 01:24:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E4=B8=83=E7=8E=8B=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=EF=BC=9A=E5=AE=8C=E6=88=90=20P2=20=E7=94=A8=E4=BE=8B=EF=BC=8C?= =?UTF-8?q?=E8=BE=BE=E6=88=90=E6=B5=8B=E8=AF=95=E8=AE=A1=E5=88=92=20DoD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_deal.js(新):§2 发牌构成——92张/去3-4共16/三家各28+底8/分值(§6.1) - test_arith.js:§3 编码排序(order_cards 降序=设计顺序、is_continuous 相邻链逐段+反例)、 §7 每档小光/过庄分界+升3级补齐 - test_paiju.js:§8.2 亮牌更多边界(6个7仅qi、固定主9→null、固定主10仅zhu) - test_config.js:§10 直接调 export.get_asetcount/get_needroomcard(_joinroom) - test_rpc.js/_rpc.js:§12 端到端(真实驱动 叫分→上庄→选主→埋牌→出牌ready)、 §9 maipai 亮牌门控 + get_deskinfo PushCards 门控 - 02-测试计划.md/README:P2 与 DoD 标记达成 全部单测 204 项断言全绿(node server/games/erqiwang/test/run.js) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../erqiwang/docs/compliance/02-测试计划.md | 38 +++++++++++-------- server/games/erqiwang/test/README.md | 3 +- server/games/erqiwang/test/_rpc.js | 2 + server/games/erqiwang/test/test_arith.js | 27 +++++++++++++ server/games/erqiwang/test/test_config.js | 10 +++++ server/games/erqiwang/test/test_deal.js | 28 ++++++++++++++ server/games/erqiwang/test/test_paiju.js | 3 ++ server/games/erqiwang/test/test_rpc.js | 23 +++++++++++ 8 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 server/games/erqiwang/test/test_deal.js diff --git a/server/games/erqiwang/docs/compliance/02-测试计划.md b/server/games/erqiwang/docs/compliance/02-测试计划.md index d9bfca7..4ba5804 100644 --- a/server/games/erqiwang/docs/compliance/02-测试计划.md +++ b/server/games/erqiwang/docs/compliance/02-测试计划.md @@ -116,12 +116,12 @@ 7. ✅ **§4.2 `mod.jiaofen` 入参校验**——>70/非5倍/首家必叫/后叫更低 全部拒,合法接受。 8. ✅ **§4.6 坐庄轮换 `do_prepare`**——庄赢连庄、闲赢/投降下家。 -### P2 · 补强既有(价值递减) -9. §2 牌局构成专测(92张/去3-4/分值)、§3 编码/排序(id_to_code 顺序、is_continuous 全链)专测。 -10. §9 `mod.maipai` 亮牌 / `get_deskinfo` PushCards 门控(同 chupai 的 nocheck 模式)。 -11. §8.2 亮牌更多阈值组合与 9/10 边界;§7 算子每档分界与升3级补齐。 -12. §12 完整一局端到端(发牌→叫分→选主→埋牌→出牌→结算→轮庄)。 -13. §10 改为直接调 `export.get_asetcount`/`get_needroomcard`(替代 config 复刻)。 +### P2 · 补强既有 —— ✅ 已完成 +9. ✅ §2 牌局构成专测(`test_deal`)、§3 编码/排序专测(`test_arith`:order_cards + is_continuous 链)。 +10. ✅ §9 `mod.maipai` 亮牌门控 / `get_deskinfo` PushCards 门控(`test_rpc` 端到端驱动后验证)。 +11. ✅ §8.2 亮牌更多阈值(6个7/固定主9-10边界)、§7 算子每档分界+升3级(`test_arith`)。 +12. ✅ §12 端到端(`test_rpc` 真实驱动到出牌就绪)。 +13. ✅ §10 直接调 `export.get_asetcount`/`get_needroomcard`/`get_needroomcard_joinroom`(`test_config`)。 --- @@ -135,23 +135,29 @@ --- -## 4. 完成定义(DoD) +## 4. 完成定义(DoD)—— ✅ 已达成 -- 覆盖矩阵中所有 ❌/🟡 项补到 ✅,每条规则至少含正/反/边界; +- ✅ 覆盖矩阵中所有 ❌/🟡 项补到 ✅(详见 §5 现状),每条规则至少含正/反/边界; - `node test/run.js` 全绿、退出码 0; - 新增用例遵守测试纪律:不改正式代码去迁就测试、不软化断言、失败先用证据裁根因; - 本计划与 `01-design合规逐节核对.md` 的结论一致(自动化测试成为人工审计的可回归证据)。 --- -## 5. 当前状态小结 +## 5. 当前状态小结 —— ✅ DoD 达成,P0/P1/P2 全部完成 -**共 156 项断言全绿**(`test_arith` 69 / `test_callgrade` 6 / `test_config` 12 / `test_desk` 6 / `test_follow` 17 / `test_paiju` 19 / `test_rpc` 27)。 +**共 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)。 -已覆盖(✅)——**P0 + P1 规则关键项全部完成**: -- **L1/L2**:§5.1/5.2 正常跟牌毙牌垫牌、§5.4 甩牌全套、§5.4.4 跟甩牌、§6.3 扣底倍数+触发、§7 算子(常规+爬坡)、§7/§8 结算集成(大光/过庄/升级/爬坡/傍王/算奖参与/投降)、§8.1 算奖 get_chongguan、§8.2 亮牌、§4.2 叫分坐庄 do_callgrade、§4.6 坐庄轮换 do_prepare、§10.1 位串/局数扣卡。 -- **L3 RPC**(`_rpc.js` 脚手架:装配 mod.js + 捕获下发包):§9 明牌 mingpai、§9 出牌 chupai 的 info/baozhu 查牌门控、§4 投降 touxiang 条件、§4.2 `mod.jiaofen` 入参校验、§4 70分上庄的暗牌亮牌下发(真实 jiaofen 驱动到上庄,验证 bottomcards/ancard3s 门控)。 +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)。 +- **§6**:捡分(结算集成间接)、扣底倍数+触发。 +- **§7**:算子常规+爬坡逐档、每档分界+升3级、结算集成(大光/过庄/升级/爬坡/傍王/投降)。 +- **§8**:算奖 get_chongguan、亮牌 get_liangpai(含 6个7/固定主9-10边界)、算奖并入 X×N(傍王对照)。 +- **§9**:查牌门控(chupai info/baozhu、maipai liangpai、deskinfo PushCards seatlist)、明牌 mingpai。 +- **§10**:位串解析、局数/扣卡(直接调 export 接口)。 +- **§12**:端到端(叫分→上庄→选主→埋牌→出牌ready,真实 RPC 驱动)。 -剩余(P2,价值递减): -- §9 `mod.maipai` 亮牌下发 / `get_deskinfo` PushCards 门控——**与 chupai 同一 `cfg.nocheck` 门控模式、且 get_liangpai 已单测**,属同构补强。 -- §2 牌局构成专测、§3 编码/排序专测、§8.2 亮牌更多阈值、§7 算子每档分界与升3级、§12 完整一局端到端。 +> 说明:§12 覆盖到"出牌准备就绪"(各阶段串接正确);完整 28 墩逐墩对打需一个合法出牌选择器,属后续增强,非规则合规缺口(逐墩规则已由 §5/§6 单测覆盖)。 diff --git a/server/games/erqiwang/test/README.md b/server/games/erqiwang/test/README.md index fb9ce53..fae4bb1 100644 --- a/server/games/erqiwang/test/README.md +++ b/server/games/erqiwang/test/README.md @@ -28,11 +28,12 @@ node server/games/erqiwang/test/test_arith.js # 单独跑某一组 ## 覆盖 -- `test_arith.js`:扣底倍数(§6.3)、算子常规+爬坡逐档(§7)、算奖 get_chongguan(§8.1)、甩牌分解/最大性/合法性(§5.4)、跟甩牌强制分量拆解(§5.4.4)。 +- `test_arith.js`:主牌排序/相邻链(§3)、扣底倍数(§6.3)、算子常规+爬坡逐档+分界+升3级(§7)、算奖 get_chongguan(§8.1)、甩牌分解/最大性/合法性(§5.4)、跟甩牌强制分量拆解(§5.4.4)。 - `test_follow.js`:正常跟牌/毙牌/垫牌,单张/对子/拖拉机的必出与缺门毙牌数量对应(§5.1/5.2)。 - `test_callgrade.js`:叫分坐庄 do_callgrade(叫5立即/两家不叫/后叫更低)(§4.2)。 - `test_config.js`:roomtype 位串解析、局数/扣卡映射(§10.1)。 - `test_paiju.js`:亮牌 get_liangpai(§8.2)、结算 get_paiju_account 集成(§7/§8,大光/过庄/升级/爬坡/傍王/投降)、扣底触发 get_bottom_account(§6.3)。 +- `test_deal.js`:发牌构成——92张/去3-4/三家28+底8/分值(§2/§6.1)。 - `test_desk.js`:坐庄轮换 do_prepare(庄赢连庄/闲赢·投降下家)(§4.6)。 - `test_rpc.js`(L3,用 `_rpc.js` 装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 info/baozhu 查牌门控(§9)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。 diff --git a/server/games/erqiwang/test/_rpc.js b/server/games/erqiwang/test/_rpc.js index e0af0cc..fdbf4f5 100644 --- a/server/games/erqiwang/test/_rpc.js +++ b/server/games/erqiwang/test/_rpc.js @@ -22,6 +22,8 @@ function setup(roomtype, o_paiju, seatlist) { const o_desk = { paiju_list: [o_paiju], o_room: null, + seatlist: [[0, []], [0, []], [0, []]], + prepare: [0, 0, 0], method: { curr_paiju: () => o_paiju, get_countdown_jiaofen: () => 15, get_countdown_xuanzhu: () => 20, diff --git a/server/games/erqiwang/test/test_arith.js b/server/games/erqiwang/test/test_arith.js index a68fb7b..4902bac 100644 --- a/server/games/erqiwang/test/test_arith.js +++ b/server/games/erqiwang/test/test_arith.js @@ -121,4 +121,31 @@ t.eq('跟牌 主牌最大化(有主用尽)', ffo([K1, Q1, fA, fB], [K1, Q1, fA], 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); +// ============ §3 主牌顺序 / 相邻链 ============ +// order_cards 结果应为 design §3 从大到小:大王>小王>正7>副7>正2>副2>主A>主K…主5>副牌 +const big1 = big(1), small1 = small(1), Z7 = id(1, 1, 7), F7 = id(1, 2, 7), Z2 = id(1, 1, 2), F2 = id(1, 2, 2), zA = id(1, 1, 1), zK = id(1, 1, 13), z5 = id(1, 1, 5); +t.eq('排序 主牌降序=设计顺序', A.order_cards(MF, [z5, F2, Z2, big1, F7, Z7, small1, zA, zK]), [big1, small1, Z7, F7, Z2, F2, zA, zK, z5]); +// is_continuous 相邻链逐段(design §3/§5.3) +const cont = (a, b) => A.is_continuous(A.id_to_code(MF, a), A.id_to_code(MF, b)); +t.eq('相邻 大王-小王', cont(big(1), small(1)), true); +t.eq('相邻 小王-正7', cont(small(1), Z7), true); +t.eq('相邻 正7-副7', cont(Z7, F7), true); +t.eq('相邻 副7-正2', cont(F7, Z2), true); +t.eq('相邻 正2-副2', cont(Z2, F2), true); +t.eq('相邻 副2-主A', cont(F2, zA), true); +t.eq('相邻 主A-主K', cont(zA, zK), true); +t.eq('相邻 主8-主6(6/8可连)', cont(id(1, 1, 8), id(1, 1, 6)), true); +t.eq('相邻 主6-主5', cont(id(1, 1, 6), z5), true); +t.eq('相邻 正7-主6 不连(7不入普通序)', cont(Z7, id(1, 1, 6)), false); +t.eq('相邻 主8-正7 不连', cont(id(1, 1, 8), Z7), false); + +// ============ §7 算子:每档小光/过庄分界 + 升3级(补强)============ +t.eq('常规65 g39小光', judge(65, 39, false), '小光/4'); +t.eq('常规65 g40过庄(分界)', judge(65, 40, false), '过庄/2'); +t.eq('常规65 g64过庄', judge(65, 64, false), '过庄/2'); +t.eq('常规65 g65升1', judge(65, 65, false), '升1级/2'); +t.eq('常规65 g104升1', judge(65, 104, false), '升1级/2'); +t.eq('常规65 g105升2', judge(65, 105, false), '升2级/4'); +t.eq('常规65 g145升3', judge(65, 145, false), '升3级/6'); + process.exit(t.done('arith') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_config.js b/server/games/erqiwang/test/test_config.js index cb778ef..fc62173 100644 --- a/server/games/erqiwang/test/test_config.js +++ b/server/games/erqiwang/test/test_config.js @@ -25,4 +25,14 @@ t.eq('房主12局', needroomcard("10000"), 4); t.eq('AA6局', needroomcard("01000"), 1); t.eq('AA12局', needroomcard("11000"), 2); +// --- 直接调用 export 的接口(替代复刻映射) --- +const exp = require('../class.export.js').new(); +t.eq('export 局数6', exp.get_asetcount("00000"), 6); +t.eq('export 局数12', exp.get_asetcount("10000"), 12); +t.eq('export 房主6局2张', exp.get_needroomcard("00000"), 2); +t.eq('export 房主12局4张', exp.get_needroomcard("10000"), 4); +t.eq('export AA6局1张', exp.get_needroomcard("01000"), 1); +t.eq('export 加入·房主0张', exp.get_needroomcard_joinroom("00000"), 0); +t.eq('export 加入·AA6局1张', exp.get_needroomcard_joinroom("01000"), 1); + process.exit(t.done('config') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_deal.js b/server/games/erqiwang/test/test_deal.js new file mode 100644 index 0000000..c78c3c6 --- /dev/null +++ b/server/games/erqiwang/test/test_deal.js @@ -0,0 +1,28 @@ +// §2 牌局构成:两副去3/4共92张、三家各28+底8、分值。期望取自 design §2/§6.1 +require('./_shim'); +const P = require('../class.paiju.js'); +const t = require('./_assert')(); + +const pj = P.new({ paiju_list: [] }, 0); // 真实发牌(min_random 由 shim 提供) + +// dealowner 统计:-1 去掉的3/4;0 底牌;1/2/3 三家 +const cnt = {}; +for (const c of pj.cards) cnt[c.dealowner] = (cnt[c.dealowner] || 0) + 1; + +t.eq('发牌 三家各28', [P.get_seat_cards_deal(pj, 0).length, P.get_seat_cards_deal(pj, 1).length, P.get_seat_cards_deal(pj, 2).length], [28, 28, 28]); +t.eq('发牌 底牌8', P.get_bottomcards(pj).length, 8); +t.eq('发牌 总发出92(28×3+8)', (cnt[0] || 0) + (cnt[1] || 0) + (cnt[2] || 0) + (cnt[3] || 0), 92); +t.eq('去掉的3/4共16', cnt[-1], 16); +t.eq('无3/4被发出', pj.cards.filter(c => (c.number === 3 || c.number === 4) && c.dealowner !== -1).length, 0); +t.eq('牌对象总数108', pj.cards.length, 108); + +// 分值 (§6.1):5→5、10→10、K(13)→10、其余0 +const scoreOf = n => pj.cards.find(x => x.number === n).score; +t.eq('分值 5→5', scoreOf(5), 5); +t.eq('分值 10→10', scoreOf(10), 10); +t.eq('分值 K(13)→10', scoreOf(13), 10); +t.eq('分值 A(1)→0', scoreOf(1), 0); +t.eq('分值 6→0', scoreOf(6), 0); +t.eq('分值 大王→0', scoreOf(54), 0); + +process.exit(t.done('deal') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_paiju.js b/server/games/erqiwang/test/test_paiju.js index e29ccb2..7355556 100644 --- a/server/games/erqiwang/test/test_paiju.js +++ b/server/games/erqiwang/test/test_paiju.js @@ -24,6 +24,9 @@ t.eq('亮牌 3王', lp([big(1), small(1), small(2)]), { wang: 3 }); t.eq('亮牌 4王+6个7(固定主10)', lp([big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]), { zhu: 10, zhupair: 5, zhutuo: 1, wang: 4, qi: 6 }); t.eq('亮牌 6个2', lp([id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 3, 2), id(2, 3, 2)]), { er: 6 }); t.eq('亮牌 2王2个7(不达标)', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7)]), null); +t.eq('亮牌 6个7(仅qi)', lp([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]), { qi: 6 }); +t.eq('亮牌 固定主9不达标→null', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2)]), null); +t.eq('亮牌 固定主10边界(仅zhu)', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2)]), { zhu: 10, zhupair: 4, zhutuo: 1 }); // ============ §7/§8 结算 get_paiju_account 集成 ============ // 受控 o_paiju/o_desk/o_room:控制庄家算奖手牌(dealowner=1)与闲1捡分(playowner=1,score) diff --git a/server/games/erqiwang/test/test_rpc.js b/server/games/erqiwang/test/test_rpc.js index df030e9..8d0dd08 100644 --- a/server/games/erqiwang/test/test_rpc.js +++ b/server/games/erqiwang/test/test_rpc.js @@ -103,4 +103,27 @@ const xian5 = sz5.find(m => m.data.cards === undefined); t.eq('上庄5 闲家无暗牌', xian5 && xian5.data.bottomcards, undefined); t.eq('上庄5 无ancard3s', xian5 && xian5.data.ancard3s, undefined); +// ===================== §12 端到端(叫分→上庄→选主→埋牌→出牌ready) + §9 maipai/deskinfo 门控 ===================== +const E = require('../class.export.js').new(); +function driveToPlay(roomtype) { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup(roomtype, pj); + mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); // 0上庄65 + mod.xuanzhu(pack(0, { flower: 1 })); // 选主 方块 + mod.maipai(pack(0, { cards: P.get_seat_cards(pj, 0).slice(0, 8) })); // 埋任意8张 + return { pj, ...c }; +} +const e = driveToPlay("00000"); +t.eq('端到端 叫分→选主→埋牌→step5', e.pj.step, 5); +t.eq('端到端 庄家首出(currseat=0)', e.pj.playproc.currseat, 0); +t.eq('端到端 选主flower=1', e.pj.flower, 1); +t.eq('端到端 埋牌后庄家保留28张', P.get_seat_cards(e.pj, 0).length, 28); +// §9 maipai 亮牌门控:不查牌 → 闲家包无 liangpai +const en = driveToPlay("00001"); +const mpXian = en.sent.filter(m => m.rpc === 'maipai').find(m => m.data.cards === undefined); +t.eq('埋牌 不查牌 闲家无liangpai', mpXian && mpXian.data.liangpai, undefined); +// §9 deskinfo(重连) PushCards 门控:可查牌有 seatlist、不查牌无 +t.eq('重连 可查牌 PushCards有seatlist', E.get_deskinfo(e.o_room, 1).PushCards.seatlist !== undefined, true); +t.eq('重连 不查牌 PushCards无seatlist', E.get_deskinfo(en.o_room, 1).PushCards.seatlist, undefined); + process.exit(t.done('rpc') ? 0 : 1);