From 6939e70ed7a947fca63ea45b222ce841c6e7e67f Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 5 Jul 2026 01:14:33 +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=20P1=20=E7=94=A8=E4=BE=8B=EF=BC=88L?= =?UTF-8?q?3=20RPC=20=E5=B1=82=20+=20=E5=9D=90=E5=BA=84=E8=BD=AE=E6=8D=A2?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mod.js:加 module.exports 守卫,使其可被 Node require(友乐无 module 跳过) - test/_rpc.js(新):L3 脚手架——挂类全局、mock cls_mod/min_loadJsFile 装配 mod.js、 捕获 SendPack/sendpack_toother 下发包 - test/test_rpc.js(新):§9 明牌 mingpai(合法/不查牌·未报无主·非出牌阶段拒 + others结构)、 §9 出牌 chupai 的 info/baozhu 查牌门控(可查有/不查无)、§4 投降 touxiang 条件(70·step2·庄家)、 §4.2 mod.jiaofen 入参校验(>70/非5倍/首家必叫/后叫更低)、 §4 70分上庄暗牌亮牌(真实 jiaofen 驱动到上庄,验证 bottomcards/ancard3s 门控) - test/test_desk.js(新):§4.6 坐庄轮换 do_prepare(庄赢连庄/闲赢·投降下家) - 02-测试计划.md/README:P1 标记完成,剩余归入 P2 全部单测 156 项断言全绿(node server/games/erqiwang/test/run.js) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../erqiwang/docs/compliance/02-测试计划.md | 32 +++--- server/games/erqiwang/mod.js | 5 + server/games/erqiwang/test/README.md | 2 + server/games/erqiwang/test/_rpc.js | 54 +++++++++ server/games/erqiwang/test/test_desk.js | 21 ++++ server/games/erqiwang/test/test_rpc.js | 106 ++++++++++++++++++ 6 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 server/games/erqiwang/test/_rpc.js create mode 100644 server/games/erqiwang/test/test_desk.js create mode 100644 server/games/erqiwang/test/test_rpc.js diff --git a/server/games/erqiwang/docs/compliance/02-测试计划.md b/server/games/erqiwang/docs/compliance/02-测试计划.md index a5b16f0..d9bfca7 100644 --- a/server/games/erqiwang/docs/compliance/02-测试计划.md +++ b/server/games/erqiwang/docs/compliance/02-测试计划.md @@ -110,16 +110,18 @@ > 注:§4.2 中 `mod.jiaofen` 的**入参校验**(>70/非5倍/首家必叫/后叫更低)属 L3,随投降/暗牌/查牌门控一起在 P1 补。 -### P1 · 规则关键、当前间接覆盖或需 mock -5. **§9 查牌门控 + mingpai**(L3,需 mod RPC 脚手架)——info/baozhu/liangpai 按 `nocheck` 门控、明牌 RPC 条件。 -6. **§4 投降 + 暗牌亮牌**(L3)——touxiang step2/70分互斥、shangzhuang 暗牌 70 分发全员+ancard3s、非70只发庄。 -7. **§4.6 坐庄轮换**(`do_prepare`,L2/L3)——庄赢连庄/庄输下家/首局0号。 -8. **§2/§3 构成与编码专测**(L1/L2)——92张/去3-4/分值;id_to_code 排序、is_continuous 全链。 +### P1 · 规则关键、需 RPC 脚手架 —— ✅ 已完成(`_rpc.js` + `test_rpc.js` / `test_desk.js`) +5. ✅ **§9 查牌门控 + mingpai**——chupai 的 info/baozhu 按 `nocheck` 门控(可查有/不查无);mingpai 合法下发他家主牌,不查牌/未报无主/非出牌阶段均拒。 +6. ✅ **§4 投降 + 暗牌亮牌**——touxiang 仅 70分/step2/庄家;真实 jiaofen 驱动到 70分上庄,验证 shangzhuang 闲家有 bottomcards+ancard3s、叫5上庄闲家无。 +7. ✅ **§4.2 `mod.jiaofen` 入参校验**——>70/非5倍/首家必叫/后叫更低 全部拒,合法接受。 +8. ✅ **§4.6 坐庄轮换 `do_prepare`**——庄赢连庄、闲赢/投降下家。 -### P2 · 补强既有 -9. §8.2 亮牌 更多阈值组合与 9/10 边界。 -10. §7 算子 每档过庄/小光分界与升3级补齐。 -11. §10 改为直接调 `export.get_asetcount`/`get_needroomcard`(替代 config 复刻)。 +### 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 复刻)。 --- @@ -144,10 +146,12 @@ ## 5. 当前状态小结 -**共 123 项断言全绿**(`test_arith` 69 / `test_callgrade` 6 / `test_config` 12 / `test_follow` 17 / `test_paiju` 19)。 +**共 156 项断言全绿**(`test_arith` 69 / `test_callgrade` 6 / `test_config` 12 / `test_desk` 6 / `test_follow` 17 / `test_paiju` 19 / `test_rpc` 27)。 -已覆盖(✅):§5.1/5.2 正常跟牌毙牌垫牌、§5.4 甩牌全套、§5.4.4 跟甩牌、§6.3 扣底倍数+触发、§7 算子(常规+爬坡)、§7/§8 结算集成(大光/过庄/升级/爬坡/傍王/算奖参与/投降)、§8.1 算奖 get_chongguan、§8.2 亮牌、§4.2 叫分坐庄 do_callgrade、§10.1 位串/局数扣卡。**P0 全部完成。** +已覆盖(✅)——**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 门控)。 -剩余(P1/P2): -- **L3 RPC 层**(需先搭 RPC 捕获脚手架,见 §3):§9 查牌门控/明牌、§4 投降 handler/暗牌亮牌下发、§4.2 `mod.jiaofen` 入参校验、§4.6 坐庄轮换。 -- **L1/L2 补强**:§2 牌局构成专测、§3 编码/排序专测、§8.2 亮牌更多阈值、§7 算子每档分界与升3级、§12 端到端一局。 +剩余(P2,价值递减): +- §9 `mod.maipai` 亮牌下发 / `get_deskinfo` PushCards 门控——**与 chupai 同一 `cfg.nocheck` 门控模式、且 get_liangpai 已单测**,属同构补强。 +- §2 牌局构成专测、§3 编码/排序专测、§8.2 亮牌更多阈值、§7 算子每档分界与升3级、§12 完整一局端到端。 diff --git a/server/games/erqiwang/mod.js b/server/games/erqiwang/mod.js index c0992c8..14c1b81 100644 --- a/server/games/erqiwang/mod.js +++ b/server/games/erqiwang/mod.js @@ -549,4 +549,9 @@ youle_erqiwang.zhunbei = function(pack){ //准备成功 o_desk.method.do_prepare(seat); +} + +//Node(本地/单元测试)导出模块对象;友乐/浏览器无 module 时跳过(dev-guide 01 §1) +if (typeof module !== "undefined"){ + module.exports = youle_erqiwang; } \ No newline at end of file diff --git a/server/games/erqiwang/test/README.md b/server/games/erqiwang/test/README.md index 0bab62e..fb9ce53 100644 --- a/server/games/erqiwang/test/README.md +++ b/server/games/erqiwang/test/README.md @@ -33,6 +33,8 @@ node server/games/erqiwang/test/test_arith.js # 单独跑某一组 - `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_desk.js`:坐庄轮换 do_prepare(庄赢连庄/闲赢·投降下家)(§4.6)。 +- `test_rpc.js`(L3,用 `_rpc.js` 装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 info/baozhu 查牌门控(§9)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。 ## 约定(dev-guide §10 测试纪律) diff --git a/server/games/erqiwang/test/_rpc.js b/server/games/erqiwang/test/_rpc.js new file mode 100644 index 0000000..e0af0cc --- /dev/null +++ b/server/games/erqiwang/test/_rpc.js @@ -0,0 +1,54 @@ +// L3 RPC 测试脚手架:装配 mod.js(含 RPC 方法),捕获下发包 +require('./_shim'); +// mod.js 的 RPC handler 按全局名引用这些类,先挂到 global +global.cls_youle_erqiwang_config = require('../class.config.js'); +global.cls_youle_erqiwang_pai = require('../class.pai.js'); +global.cls_youle_erqiwang_arith = require('../class.arith.js'); +global.cls_youle_erqiwang_paiju = require('../class.paiju.js'); +global.cls_youle_erqiwang_desk = require('../class.desk.js'); +// mod.js 顶部依赖:cls_mod.new 造模块对象、min_loadJsFile 置为同步 no-op(类已单独 require) +global.cls_mod = { new: (name, route, app) => ({ modname: name, routename: route, app }) }; +global.youle_app = {}; +global.min_loadJsFile = (p, cb) => { if (cb) cb(); }; + +const mod = require('../mod.js'); // = youle_erqiwang,含 jiaofen/xuanzhu/.../mingpai/zhunbei + +const clone = m => JSON.parse(JSON.stringify(m)); + +// 装配一局:给定 roomtype 与 o_paiju,返回 { sent, o_room },并把 check_player/SendPack/sendpack_toother +// 全部指向同一个 sent 数组(快照每个下发包,因 mod 会跨座位改删字段) +function setup(roomtype, o_paiju, seatlist) { + const sent = []; + const o_desk = { + paiju_list: [o_paiju], + o_room: null, + method: { + curr_paiju: () => o_paiju, + get_countdown_jiaofen: () => 15, get_countdown_xuanzhu: () => 20, + get_countdown_maipai: () => 25, get_countdown_chupai: () => 30, + do_prepare: () => { }, get_desk_account: m => m + } + }; + const o_room = { + roomtype, asetcount: 6, + seatlist: seatlist || [{ conmode: 0, fromid: 0 }, { conmode: 0, fromid: 1 }, { conmode: 0, fromid: 2 }], + o_desk, + method: { sendpack_toother: m => sent.push(clone(m)) } + }; + o_desk.o_room = o_room; + if (o_paiju) o_paiju.o_desk = o_desk; + mod.import = { check_player: () => o_room, deduct_roomcard: () => { }, save_grade: () => { } }; + mod.app = { SendPack: m => sent.push(clone(m)) }; + return { sent, o_room, o_desk }; +} + +// 构造 108 张牌(带 flower/number/dealowner/playround),供 get_seat_zhucards / get_seat_cards 用 +function make108(deal) { + const A = global.cls_youle_erqiwang_arith; + const cards = []; + for (let i = 0; i < 108; i++) cards[i] = { id: i, dealowner: 1, playround: -1, playowner: -1, score: 0, flower: A.id_to_flower(i), number: A.id_to_number(i) }; + if (deal) for (const [cid, owner] of deal) cards[cid].dealowner = owner; + return cards; +} + +module.exports = { mod, setup, make108, id: (d, f, n) => (d - 1) * 54 + (f - 1) * 13 + (n - 1) }; diff --git a/server/games/erqiwang/test/test_desk.js b/server/games/erqiwang/test/test_desk.js new file mode 100644 index 0000000..2583365 --- /dev/null +++ b/server/games/erqiwang/test/test_desk.js @@ -0,0 +1,21 @@ +// §4.6 坐庄轮换 do_prepare(庄赢连庄 / 闲赢·投降下家)。期望取自 design §4.6 +require('./_shim'); +const D = require('../class.desk.js'); +const t = require('./_assert')(); + +// 造一个牌桌,压入一局"已结束"的牌局(banker/result),三家都准备后开新局,取新局 firstseat +function nextFirstseat(banker, result) { + const o_room = { seatlist: [{ conmode: 0, fromid: 0 }, { conmode: 0, fromid: 1 }, { conmode: 0, fromid: 2 }], asetcount: 6, method: { sendpack_toother: () => { } } }; + const desk = D.new(o_room); + desk.paiju_list.push({ banker, result, tmp_jiesuan_aset: {} }); + D.do_prepare(desk, 0); D.do_prepare(desk, 1); D.do_prepare(desk, 2); + return desk.paiju_list[desk.paiju_list.length - 1].firstseat; +} +t.eq('轮庄 庄1赢(result0)连庄', nextFirstseat(1, 0), 1); +t.eq('轮庄 庄1闲赢(result1)下家2', nextFirstseat(1, 1), 2); +t.eq('轮庄 庄1投降(result2)下家2', nextFirstseat(1, 2), 2); +t.eq('轮庄 庄2赢连庄', nextFirstseat(2, 0), 2); +t.eq('轮庄 庄2闲赢下家(回0)', nextFirstseat(2, 1), 0); +t.eq('轮庄 庄0闲赢下家1', nextFirstseat(0, 1), 1); + +process.exit(t.done('desk') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_rpc.js b/server/games/erqiwang/test/test_rpc.js new file mode 100644 index 0000000..df030e9 --- /dev/null +++ b/server/games/erqiwang/test/test_rpc.js @@ -0,0 +1,106 @@ +// L3 RPC 层:§9 明牌/查牌门控、§4 投降条件。期望取自 design §4/§9 +const R = require('./_rpc'); +const P = require('../class.paiju.js'); +const { mod, setup, make108, id } = R; +const t = require('./_assert')(); + +const pack = (seat, extra) => Object.assign({ data: Object.assign({ agentid: 'a', playerid: 1, gameid: 'g', roomcode: 1, seat }, extra), conmode: 0, fromid: seat }); + +// ===================== §9 明牌 mingpai ===================== +const dealMP = [[id(1, 1, 13), 2], [id(1, 1, 12), 3]]; // seat1 主K, seat2 主Q +const mkMP = (step, baofu) => ({ step, banker: 0, flower: 1, cards: make108(dealMP), method: { have_baofu: () => baofu } }); +let ctx = setup("00000", mkMP(5, true)); // 可查牌 + 报无主 + 出牌阶段 +mod.mingpai(pack(0)); +t.eq('明牌 合法 下发1包', ctx.sent.length, 1); +t.eq('明牌 others=他家主牌', ctx.sent[0] && ctx.sent[0].data.others, [{ seat: 1, zhucards: [id(1, 1, 13)] }, { seat: 2, zhucards: [id(1, 1, 12)] }]); +ctx = setup("00001", mkMP(5, true)); mod.mingpai(pack(0)); // 不查牌(位4=1) +t.eq('明牌 不查牌 拒', ctx.sent.length, 0); +ctx = setup("00000", mkMP(5, false)); mod.mingpai(pack(0)); // 未报无主 +t.eq('明牌 未报无主 拒', ctx.sent.length, 0); +ctx = setup("00000", mkMP(3, true)); mod.mingpai(pack(0)); // 非出牌阶段 +t.eq('明牌 非出牌阶段 拒', ctx.sent.length, 0); + +// ===================== §4 投降 touxiang 条件 ===================== +const mkTX = (step, banker, call) => ({ step, banker, call, method: { get_paiju_account: (type, msg) => { msg.rpc = 'jiesuan'; msg.data = { type }; return msg; } } }); +let p = mkTX(2, 0, 70); ctx = setup("00000", p); mod.touxiang(pack(0)); // 70分 step2 庄家 +t.eq('投降 70分step2庄家 step→6', p.step, 6); +t.eq('投降 下发jiesuan', ctx.sent.length === 1 && ctx.sent[0].rpc, 'jiesuan'); +p = mkTX(2, 0, 65); ctx = setup("00000", p); mod.touxiang(pack(0)); +t.eq('投降 65分 拒', ctx.sent.length, 0); +p = mkTX(3, 0, 70); ctx = setup("00000", p); mod.touxiang(pack(0)); +t.eq('投降 埋牌后step3 拒', ctx.sent.length, 0); +p = mkTX(2, 1, 70); ctx = setup("00000", p); mod.touxiang(pack(0)); +t.eq('投降 非庄家 拒', ctx.sent.length, 0); + +// ===================== §9 出牌 chupai 的查牌门控(info/baozhu)===================== +// 造真实牌局 → 强制到首出(step5),庄家(0)出一张副牌单张;seat1 已报无主 +function mkChupai(roomtype) { + const o_desk_stub = { paiju_list: [] }; + const pj = P.new(o_desk_stub, 0); + pj.banker = 0; pj.call = 65; pj.flower = 1; pj.step = 5; + P.new_playround(pj, 1, 0); // 本轮庄家(0)首出 + const lead = id(1, 3, 9); // 副9(flower3) 作单张首出 + pj.cards[lead].dealowner = 1; // 归庄家 + pj.cards[lead].playround = -1; + pj.seatlist[1][4][0] = 0; // seat1 报无主 → have_baofu()=true + const c = setup(roomtype, pj); + mod.chupai(pack(0, { cards: [lead] })); + return c; +} +let cv = mkChupai("00000"); // 可查牌 +t.eq('出牌 可查牌 有info', cv.sent[0] && cv.sent[0].data.info !== undefined, true); +t.eq('出牌 可查牌 baozhu=1(有人报无主)', cv.sent[0] && cv.sent[0].data.baozhu, 1); +let cn = mkChupai("00001"); // 不查牌(位4=1) +t.eq('出牌 不查牌 无info', cn.sent[0] && cn.sent[0].data.info, undefined); +t.eq('出牌 不查牌 baozhu=0', cn.sent[0] && cn.sent[0].data.baozhu, 0); + +// ===================== §4.2 mod.jiaofen 入参校验 ===================== +// do_callgrade 被调用=接受;被 return 挡掉=拒绝。step 设 0 跳过后续广播 +function mkJF(currcall) { + const st = { called: null }; + const pj = { + step: 1, banker: -1, call: -1, + method: { + get_callgrade_seat: () => 0, + get_callgrade_value: () => currcall, + do_callgrade: c => { st.called = c; pj.step = 0; } + } + }; + return { pj, st }; +} +function jf(currcall, call) { + const { pj, st } = mkJF(currcall); + setup("00000", pj); + mod.jiaofen(pack(0, { call })); + return st.called; +} +t.eq('叫分 >70 拒', jf(0, 75), null); +t.eq('叫分 非5倍数 拒', jf(0, 63), null); +t.eq('叫分 首家不叫(currcall0,call0) 拒', jf(0, 0), null); +t.eq('叫分 后叫不更低(call>=currcall) 拒', jf(55, 60), null); +t.eq('叫分 首家合法65 接受', jf(0, 65), 65); +t.eq('叫分 后叫更低50 接受', jf(55, 50), 50); +t.eq('叫分 边界70 接受', jf(0, 70), 70); + +// ===================== §4 70分上庄的暗牌亮牌 gating(真实 jiaofen 驱动到上庄)===================== +function driveUp(calls) { + const pj = P.new({ paiju_list: [] }, 0); // 真实牌局 step1 firstseat0 + const c = setup("00000", pj); + for (const [seat, call] of calls) mod.jiaofen(pack(seat, { call })); + return c.sent.filter(m => m.rpc === 'shangzhuang'); +} +// 0叫70、1不叫、2不叫 → 0上庄70 +const sz70 = driveUp([[0, 70], [1, 0], [2, 0]]); +t.eq('上庄70 有shangzhuang下发', sz70.length > 0, true); +const bankerPk = sz70.find(m => m.data.cards !== undefined); +const xianPk = sz70.find(m => m.data.cards === undefined); +t.eq('上庄70 庄家包有手牌', !!bankerPk, true); +t.eq('上庄70 闲家包有暗牌', xianPk && xianPk.data.bottomcards !== undefined, true); +t.eq('上庄70 ancard3s=1', xianPk && xianPk.data.ancard3s, 1); +// 对照:叫5立即上庄(非70) → 闲家无暗牌、无ancard3s +const sz5 = driveUp([[0, 5]]); +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); + +process.exit(t.done('rpc') ? 0 : 1);