- 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>
107 lines
5.6 KiB
JavaScript
107 lines
5.6 KiB
JavaScript
// 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);
|