Files
erqiwang_youle/server/games/erqiwang/test/test_success.js
T
joywayerandClaude Opus 5 c2a375eef5 二七王:补齐 data.success 成败标志与失败回包
此前全仓(除协议文档开头一句说明外)没有任何下发包携带
data.success,且所有失败路径一律裸 return、不回任何包:前端点了
没反应,只能干等倒计时,同时违反 server dev-guide「成败标志唯一
是 data.success,主动推送的 data 必须自带 success」与 client
dev-guide「发包只请求、收包才表现」两条硬红线。

本次:
- 成功包一律补 data.success=true:fapai、zhunbei(class.desk.js)、
  jiaofen、shangzhuang、xuanzhu、maipai、chupai1/2/3、mingpai、
  tishi(mod.js),jiesuan 在 get_paiju_account 统一注入(覆盖
  正常/投降/解散三种结算来源)。
- 新增 youle_erqiwang.ERR 失败码表与 do_sendfail 统一回包封装:
  PLAYER/NODESK/STEP/SEAT/PARAM/RULE,失败只回发给请求者
  (conmode/fromid 取自请求包),data.success=false + errcode。
- 每个 handler 的每条失败分支都改为「回失败包 + return」,含
  check_player 不通过(平台侧只返回 null、不回包,前端会干等)。

tishi 成功时仍按 design §11 只转发给对家、不给发送者回执(该包不
含对局状态,属 fire-and-forget 提示),失败才回给发送者。

test_rpc 中原先以「不发包」表示被拒的断言,同步改为断言「恰好回
1 个 success=false 且 errcode 正确的包」——是随行为修正而加强,
不是软化。新增 test_success.js 29 项覆盖成功包 success 字段与各
handler 失败回包。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 04:15:31 +08:00

136 lines
7.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.
// 成败标志 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);