// 成败标志 data.success(dev-guide server 03 §4/§5 硬红线): // ① 每个下发包(主动推送与应答)都必须自带 data.success; // ② 每条失败路径都必须回一个 success=false + errcode 的包给请求者,不得静默丢弃。 require('./_shim'); const { mod, setup, make108, id } = require('./_rpc.js'); const P = require('../class.paiju.js'); const D = require('../class.desk.js'); const t = require('./_assert')(); const ERR = mod.ERR; const clone = m => JSON.parse(JSON.stringify(m)); const pack = (seat, extra) => ({ data: Object.assign({ agentid: 'a', playerid: 1, gameid: 'g', roomcode: 1, seat }, extra), conmode: 0, fromid: seat }); // ============ ① 成功包一律带 success=true ============ // -- 牌桌层主动推送:fapai / zhunbei 走全局 youle_erqiwang.app.SendPack -- const gsent = []; global.youle_erqiwang.app.SendPack = m => gsent.push(clone(m)); const o_room = { roomtype: '00000', asetcount: 6, seatlist: [{ conmode: 0, fromid: 0 }, { conmode: 0, fromid: 1 }, { conmode: 0, fromid: 2 }], method: { sendpack_toother: m => gsent.push(clone(m)) } }; const desk = D.new(o_room); o_room.o_desk = desk; D.do_new_paiju(desk, 0); const fapai = gsent.filter(m => m.rpc === 'fapai'); t.eq('fapai 下发3包(每座位1)', fapai.length, 3); t.eq('fapai 带 success=true', fapai.every(m => m.data.success === true), true); gsent.length = 0; desk.paiju_list.push({ banker: 0, result: 0, tmp_jiesuan_aset: {} }); D.do_prepare(desk, 0); // 只有1家准备 → 广播 zhunbei const zb = gsent.filter(m => m.rpc === 'zhunbei'); t.eq('zhunbei 带 success=true', zb.length === 1 && zb[0].data.success === true, true); // -- RPC 层:真实驱动 叫分→上庄→选主→埋牌→出牌 -- function driveToPlay() { const pj = P.new({ paiju_list: [] }, 0); const c = setup('00000', 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) })); return { pj, c }; } const e = driveToPlay(); const rpcs = e.c.sent.map(m => m.rpc); t.eq('端到端 覆盖 jiaofen/shangzhuang/xuanzhu/maipai', ['jiaofen', 'shangzhuang', 'xuanzhu', 'maipai'].every(r => rpcs.indexOf(r) >= 0), true); t.eq('端到端 所有下发包均 success=true', e.c.sent.every(m => m.data.success === true), true); // 首出一张牌 → chupai1 也带 success const lead = P.get_seat_cards(e.pj, 0)[0]; e.c.sent.length = 0; mod.chupai(pack(0, { cards: [lead] })); const cp1 = e.c.sent.filter(m => m.rpc === 'chupai1'); t.eq('chupai1 下发3包', cp1.length, 3); t.eq('chupai1 带 success=true', cp1.every(m => m.data.success === true), true); // -- 结算包 jiesuan:正常/投降/解散 三种 type 都带 success -- const mkDesk = () => ({ seatlist: [[0, []], [0, []], [0, []]], o_room: { roomtype: '00000', asetcount: 6 }, method: { get_desk_account: m => m } }); const mkPaiju = () => ({ call: 65, banker: 0, flower: 1, cards: [], idx: 2, o_desk: mkDesk(), endtime: null, result: null }); t.eq('jiesuan 正常结算 success=true', P.get_paiju_account(mkPaiju(), 0, {}).data.success, true); t.eq('jiesuan 投降结算 success=true', P.get_paiju_account(mkPaiju(), 1, {}).data.success, true); t.eq('jiesuan 解散结算 success=true', P.get_paiju_account(mkPaiju(), 2, {}).data.success, true); // mingpai / tishi 成功包 const mp = setup('00000', { step: 5, banker: 0, flower: 1, cards: make108(), method: { have_baofu: () => true } }); mod.mingpai(pack(0)); t.eq('mingpai 成功包 success=true', mp.sent.length === 1 && mp.sent[0].data.success === true, true); const tp = setup('00000', { step: 5, banker: 0 }); mod.tishi(pack(1, { tip: 1 })); t.eq('tishi 成功包 success=true', tp.sent.length === 1 && tp.sent[0].data.success === true, true); // ============ ② 失败路径回包:success=false + errcode,且只回给请求者 ============ // 每个 handler 取一条代表性失败路径;断言"恰好1包 + success=false + errcode + 回到请求者" function failCase(name, o_paiju, call, seat, errcode) { const c = setup('00000', o_paiju); call(seat); const ok = c.sent.length === 1 && c.sent[0].data.success === false && c.sent[0].data.errcode === errcode && c.sent[0].fromid === seat; t.eq(name, ok, true); } const stubJF = { step: 9, method: { get_callgrade_seat: () => 0, get_callgrade_value: () => 0, do_callgrade: () => { } } }; failCase('jiaofen 阶段不对 → STEP', stubJF, s => mod.jiaofen(pack(s, { call: 65 })), 0, ERR.STEP); failCase('jiaofen 非当前叫分位 → SEAT', { step: 1, method: { get_callgrade_seat: () => 1, get_callgrade_value: () => 0, do_callgrade: () => { } } }, s => mod.jiaofen(pack(s, { call: 65 })), 0, ERR.SEAT); failCase('jiaofen 叫分>70 → PARAM', { step: 1, method: { get_callgrade_seat: () => 0, get_callgrade_value: () => 0, do_callgrade: () => { } } }, s => mod.jiaofen(pack(s, { call: 75 })), 0, ERR.PARAM); failCase('jiaofen 后叫不更低 → RULE', { step: 1, method: { get_callgrade_seat: () => 0, get_callgrade_value: () => 55, do_callgrade: () => { } } }, s => mod.jiaofen(pack(s, { call: 60 })), 0, ERR.RULE); failCase('xuanzhu 非庄家 → SEAT', { step: 2, banker: 1 }, s => mod.xuanzhu(pack(s, { flower: 1 })), 0, ERR.SEAT); failCase('xuanzhu 花色非法 → PARAM', { step: 2, banker: 0 }, s => mod.xuanzhu(pack(s, { flower: 9 })), 0, ERR.PARAM); const mkMP8 = () => ({ step: 3, banker: 0, cards: make108(), method: { check_cards_valid: c => P.check_cards_valid({ cards: make108() }, c), check_cards_inhand: () => true } }); failCase('maipai 张数不是8 → PARAM', mkMP8(), s => mod.maipai(pack(s, { cards: [0, 1, 2] })), 0, ERR.PARAM); failCase('maipai 入参非数组 → PARAM', mkMP8(), s => mod.maipai(pack(s, { cards: 'x' })), 0, ERR.PARAM); failCase('touxiang 非70分 → RULE', { step: 2, banker: 0, call: 65 }, s => mod.touxiang(pack(s)), 0, ERR.RULE); const mkCP = () => { const pj = P.new({ paiju_list: [] }, 0); pj.banker = 0; pj.call = 65; pj.flower = 1; pj.step = 5; P.new_playround(pj, 1, 0); return pj; }; failCase('chupai 重复牌id → PARAM', mkCP(), s => mod.chupai(pack(s, { cards: [0, 0] })), 0, ERR.PARAM); failCase('chupai 牌不在手上 → PARAM', (function () { const pj = mkCP(); for (let i = 0; i < 108; i++) pj.cards[i].dealowner = 2; return pj; })(), s => mod.chupai(pack(s, { cards: [0] })), 0, ERR.PARAM); failCase('chupai 非当前出牌位 → SEAT', (function () { const pj = mkCP(); pj.playproc.currseat = 1; return pj; })(), s => mod.chupai(pack(s, { cards: [0] })), 0, ERR.SEAT); failCase('mingpai 未报无主 → RULE', { step: 5, banker: 0, method: { have_baofu: () => false } }, s => mod.mingpai(pack(s)), 0, ERR.RULE); failCase('tishi 庄家发 → RULE', { step: 5, banker: 0 }, s => mod.tishi(pack(s, { tip: 1 })), 0, ERR.RULE); failCase('zhunbei 非结算阶段 → STEP', { step: 5 }, s => mod.zhunbei(pack(s)), 0, ERR.STEP); // 不查牌房的 mingpai 需要 roomtype '00001',单列 const mpn = setup('00001', { step: 5, banker: 0, method: { have_baofu: () => true } }); mod.mingpai(pack(0)); t.eq('mingpai 不查牌 → 回 RULE 失败包', mpn.sent.length === 1 && mpn.sent[0].data.errcode === ERR.RULE, true); // check_player 不通过(房间/座位对不上)也要回包,不能让前端干等 const cp = setup('00000', { step: 1 }); mod.import.check_player = () => null; mod.jiaofen(pack(0, { call: 65 })); t.eq('check_player 失败 → 回 PLAYER 失败包', cp.sent.length === 1 && cp.sent[0].data.errcode === ERR.PLAYER, true); process.exit(t.done('success') ? 0 : 1);