二七王测试:完成 P1 用例(L3 RPC 层 + 坐庄轮换)
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -110,16 +110,18 @@
|
|||||||
|
|
||||||
> 注:§4.2 中 `mod.jiaofen` 的**入参校验**(>70/非5倍/首家必叫/后叫更低)属 L3,随投降/暗牌/查牌门控一起在 P1 补。
|
> 注:§4.2 中 `mod.jiaofen` 的**入参校验**(>70/非5倍/首家必叫/后叫更低)属 L3,随投降/暗牌/查牌门控一起在 P1 补。
|
||||||
|
|
||||||
### P1 · 规则关键、当前间接覆盖或需 mock
|
### P1 · 规则关键、需 RPC 脚手架 —— ✅ 已完成(`_rpc.js` + `test_rpc.js` / `test_desk.js`)
|
||||||
5. **§9 查牌门控 + mingpai**(L3,需 mod RPC 脚手架)——info/baozhu/liangpai 按 `nocheck` 门控、明牌 RPC 条件。
|
5. ✅ **§9 查牌门控 + mingpai**——chupai 的 info/baozhu 按 `nocheck` 门控(可查有/不查无);mingpai 合法下发他家主牌,不查牌/未报无主/非出牌阶段均拒。
|
||||||
6. **§4 投降 + 暗牌亮牌**(L3)——touxiang step2/70分互斥、shangzhuang 暗牌 70 分发全员+ancard3s、非70只发庄。
|
6. ✅ **§4 投降 + 暗牌亮牌**——touxiang 仅 70分/step2/庄家;真实 jiaofen 驱动到 70分上庄,验证 shangzhuang 闲家有 bottomcards+ancard3s、叫5上庄闲家无。
|
||||||
7. **§4.6 坐庄轮换**(`do_prepare`,L2/L3)——庄赢连庄/庄输下家/首局0号。
|
7. ✅ **§4.2 `mod.jiaofen` 入参校验**——>70/非5倍/首家必叫/后叫更低 全部拒,合法接受。
|
||||||
8. **§2/§3 构成与编码专测**(L1/L2)——92张/去3-4/分值;id_to_code 排序、is_continuous 全链。
|
8. ✅ **§4.6 坐庄轮换 `do_prepare`**——庄赢连庄、闲赢/投降下家。
|
||||||
|
|
||||||
### P2 · 补强既有
|
### P2 · 补强既有(价值递减)
|
||||||
9. §8.2 亮牌 更多阈值组合与 9/10 边界。
|
9. §2 牌局构成专测(92张/去3-4/分值)、§3 编码/排序(id_to_code 顺序、is_continuous 全链)专测。
|
||||||
10. §7 算子 每档过庄/小光分界与升3级补齐。
|
10. §9 `mod.maipai` 亮牌 / `get_deskinfo` PushCards 门控(同 chupai 的 nocheck 模式)。
|
||||||
11. §10 改为直接调 `export.get_asetcount`/`get_needroomcard`(替代 config 复刻)。
|
11. §8.2 亮牌更多阈值组合与 9/10 边界;§7 算子每档分界与升3级补齐。
|
||||||
|
12. §12 完整一局端到端(发牌→叫分→选主→埋牌→出牌→结算→轮庄)。
|
||||||
|
13. §10 改为直接调 `export.get_asetcount`/`get_needroomcard`(替代 config 复刻)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -144,10 +146,12 @@
|
|||||||
|
|
||||||
## 5. 当前状态小结
|
## 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):
|
剩余(P2,价值递减):
|
||||||
- **L3 RPC 层**(需先搭 RPC 捕获脚手架,见 §3):§9 查牌门控/明牌、§4 投降 handler/暗牌亮牌下发、§4.2 `mod.jiaofen` 入参校验、§4.6 坐庄轮换。
|
- §9 `mod.maipai` 亮牌下发 / `get_deskinfo` PushCards 门控——**与 chupai 同一 `cfg.nocheck` 门控模式、且 get_liangpai 已单测**,属同构补强。
|
||||||
- **L1/L2 补强**:§2 牌局构成专测、§3 编码/排序专测、§8.2 亮牌更多阈值、§7 算子每档分界与升3级、§12 端到端一局。
|
- §2 牌局构成专测、§3 编码/排序专测、§8.2 亮牌更多阈值、§7 算子每档分界与升3级、§12 完整一局端到端。
|
||||||
|
|||||||
@@ -550,3 +550,8 @@ youle_erqiwang.zhunbei = function(pack){
|
|||||||
//准备成功
|
//准备成功
|
||||||
o_desk.method.do_prepare(seat);
|
o_desk.method.do_prepare(seat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Node(本地/单元测试)导出模块对象;友乐/浏览器无 module 时跳过(dev-guide 01 §1)
|
||||||
|
if (typeof module !== "undefined"){
|
||||||
|
module.exports = youle_erqiwang;
|
||||||
|
}
|
||||||
@@ -33,6 +33,8 @@ node server/games/erqiwang/test/test_arith.js # 单独跑某一组
|
|||||||
- `test_callgrade.js`:叫分坐庄 do_callgrade(叫5立即/两家不叫/后叫更低)(§4.2)。
|
- `test_callgrade.js`:叫分坐庄 do_callgrade(叫5立即/两家不叫/后叫更低)(§4.2)。
|
||||||
- `test_config.js`:roomtype 位串解析、局数/扣卡映射(§10.1)。
|
- `test_config.js`:roomtype 位串解析、局数/扣卡映射(§10.1)。
|
||||||
- `test_paiju.js`:亮牌 get_liangpai(§8.2)、结算 get_paiju_account 集成(§7/§8,大光/过庄/升级/爬坡/傍王/投降)、扣底触发 get_bottom_account(§6.3)。
|
- `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 测试纪律)
|
## 约定(dev-guide §10 测试纪律)
|
||||||
|
|
||||||
|
|||||||
@@ -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) };
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user