Files
erqiwang_youle/server/games/erqiwang/test/test_rpc.js
T
joywayerandClaude Opus 4.8 9456ab9319 二七王:实现 §11 闲家三提示(踩/没分/有分) + 明确副7/副2 成对语义
全面符合性再审查(4 维度独立对抗核对)结论:与计分/发牌/出牌/跟牌/甩牌
相关规则均一致,无缺陷;仅 §11 闲家三提示服务端缺失、副7/副2 异花色
成对语义存疑两项待办。本次按用户裁定落地:

1. §11 三提示(新增 mod.tishi):闲家在出牌阶段(step5)发 踩/没分/有分,
   服务端不校验真实性、只 SendPack 转发给对家(另一闲家 3-banker-seat);
   庄家无对家不参与;非法 tip/非出牌阶段/庄家发均拒。
   协议补 packet_protocol §13.7/13.8;test_rpc +11 例(正反+边界)。
2. 副7/副2 成对语义:裁定"只同花色同点数两副成对"(代码本已如此,无需改),
   在 design §3 补说明闭合该反复出现的存疑(异花色副7 不成对)。

compliance 文档同步:§11 状态转✅、首轮结论摘要加"非当前状态"说明、
计数 259→270。全套 270 项断言全绿(rpc 34→45)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:39:04 +08:00

157 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
// ===================== §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);
// ===================== §11 闲家出牌提示 tishi(只转发给对家,不校验)=====================
const mkTip = (step, banker) => ({ step, banker });
// 闲家1 发"踩"(tip=1),banker=0 → 对家=2;默认 seatlist fromid==座位号
let tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(1, { tip: 1 }));
t.eq('提示 合法 下发1包', tp.sent.length, 1);
t.eq('提示 只发给对家(fromid=2)', tp.sent[0] && tp.sent[0].fromid, 2);
t.eq('提示 rpc=tishi', tp.sent[0] && tp.sent[0].rpc, 'tishi');
t.eq('提示 data.seat=发出者1', tp.sent[0] && tp.sent[0].data.seat, 1);
t.eq('提示 data.tip=1(踩)', tp.sent[0] && tp.sent[0].data.tip, 1);
// 闲家2 发"有分"(tip=3) → 对家=1
tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(2, { tip: 3 }));
t.eq('提示 闲2发→对家1(fromid=1)', tp.sent[0] && tp.sent[0].fromid, 1);
// banker=2 时,闲家0 发"没分"(tip=2) → 对家=1
tp = setup("00000", mkTip(5, 2)); mod.tishi(pack(0, { tip: 2 }));
t.eq('提示 banker=2·闲0发→对家1', tp.sent[0] && tp.sent[0].fromid, 1);
// 庄家发 → 拒(庄家无对家)
tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(0, { tip: 1 }));
t.eq('提示 庄家发 拒', tp.sent.length, 0);
// 非出牌阶段 → 拒
tp = setup("00000", mkTip(3, 0)); mod.tishi(pack(1, { tip: 1 }));
t.eq('提示 非出牌阶段(step3) 拒', tp.sent.length, 0);
// 非法 tip / 缺 tip → 拒
tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(1, { tip: 9 }));
t.eq('提示 非法tip=9 拒', tp.sent.length, 0);
tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(1, {}));
t.eq('提示 缺tip 拒', tp.sent.length, 0);
process.exit(t.done('rpc') ? 0 : 1);