Files
erqiwang_youle/server/games/erqiwang/test/test_input.js
T
joywayerandClaude Opus 5 4508fe005f 二七王:出牌顺序收敛为唯一口径,重连包补发 baozhu
同一份数据「这一手打出的牌」此前有两个顺序口径:chupai*/playproc.cards
回显客户端请求原序,重连包 pushlist 则从 playround 反查后自己再排一次序。
一手多张(甩牌/对子/拖拉机)时两者必然分岔;同编码的一对牌更是排序都
区分不了,靠"两处各自排序"永远收敛不了。

改为单一写入处:
- 新增 order_playcards(),do_playcard 入口归一化一次(顺带隔离
  can_playcard 的原地排序,不再就地改写 pack.data.cards)
- 新增 paiju.playhistory,出牌当场归档;新增 get_pushlist() 只做深拷贝
- class.export.js 删掉整段反查+重排,改为直读归档
- do_playcard 三个分支都给 re.cards,mod.js 去掉回落到入参的兜底

顺带修复重连包漏发 baozhu:它是余主公示与明牌按钮的开关,此前只在
chupai 包里给,报无主后重连按钮会凭空消失。现与 chupai 同源同门控下发。

协议文档新增 §0.6「一手出牌」的顺序口径,并补 PushCards.baozhu 字段。
测试:pushlist 一节改为真实链路驱动(一手多张 + 乱序提交),断言升级为
「重连每一手与当时的 chupai.cards 逐元素相等」;三处修复均已退回验证转红。

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

118 lines
6.2 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.
// 客户端入参校验:重复牌id / 越界id / 非数组 / 非整数(check_cards_valid + check_cards_inhand)
// 回归两个真实缺陷:① 重复id 被 can_playcard 当成对子/拖拉机;② 埋牌传重复id 只埋下 1 张
require('./_shim');
const { mod, setup, make108, id } = require('./_rpc.js');
const P = require('../class.paiju.js');
const t = require('./_assert')();
// 造一个最小可用牌局:108 张牌全发给指定座位,主花色=1
function newPaiju(step, banker, owner) {
const paiju = {
cards: make108(), step, banker, call: 65, flower: 1,
seatlist: [[[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]],
playproc: {}, playhistory: [], callproc: [], idx: 1
};
for (let i = 0; i < 108; i++) paiju.cards[i].dealowner = owner + 1;
paiju.method = {
check_cards_valid: c => P.check_cards_valid(paiju, c),
check_cards_inhand: (c, s) => P.check_cards_inhand(paiju, c, s),
do_burycard: c => P.do_burycard(paiju, c),
do_playcard: c => P.do_playcard(paiju, c),
get_seat_cards: s => P.get_seat_cards(paiju, s),
get_seat_zhucards: s => P.get_seat_zhucards(paiju, s),
have_baofu: () => (paiju.seatlist[0][4][0] == 0 || paiju.seatlist[1][4][0] == 0 || paiju.seatlist[2][4][0] == 0),
get_liangpai: () => null,
get_curmultiple: () => P.get_curmultiple(paiju),
get_mustcard: s => P.get_mustcard(paiju, s)
};
return paiju;
}
// ============ L1 check_cards_valid:正 / 反 / 边界 ============
const pj = newPaiju(5, 0, 0);
const V = c => P.check_cards_valid(pj, c);
t.eq('valid 正 单张', V([0]), true);
t.eq('valid 正 多张不重复', V([0, 1, 2, 53]), true);
t.eq('valid 边界 最大牌id 107', V([107]), true);
t.eq('valid 边界 最小牌id 0', V([0]), true);
t.eq('valid 反 undefined', V(undefined), false);
t.eq('valid 反 null', V(null), false);
t.eq('valid 反 字符串', V('012'), false);
t.eq('valid 反 对象', V({ 0: 1, length: 1 }), false);
t.eq('valid 反 空数组', V([]), false);
t.eq('valid 反 重复id', V([5, 5]), false);
t.eq('valid 反 多张中含重复', V([1, 2, 3, 2]), false);
t.eq('valid 反 越界上界108', V([108]), false);
t.eq('valid 反 负数id', V([-1]), false);
t.eq('valid 反 小数id', V([1.5]), false);
t.eq('valid 反 数字字符串', V(['5']), false);
// ============ L1 check_cards_inhand ============
t.eq('inhand 正 在手上', P.check_cards_inhand(pj, [0, 1], 0), true);
t.eq('inhand 反 不在手上(别人的)', P.check_cards_inhand(pj, [0], 1), false);
t.eq('inhand 反 重复id(经 valid 拦截)', P.check_cards_inhand(pj, [0, 0], 0), false);
t.eq('inhand 反 越界id(经 valid 拦截)', P.check_cards_inhand(pj, [999], 0), false);
pj.cards[3].playround = 1; // 已打出
t.eq('inhand 反 已打出的牌', P.check_cards_inhand(pj, [3], 0), false);
pj.cards[4].playround = 0; // 已埋
t.eq('inhand 反 已埋下的牌', P.check_cards_inhand(pj, [4], 0), false);
// ============ L2 出牌:重复id 不得被当成一对 ============
const pj2 = newPaiju(5, 0, 0);
P.new_playround(pj2, 1, 0);
const cid = id(1, 1, 13); // 方块K
t.eq('出牌 反 重复id 被 mod 层拦截(valid)', P.check_cards_valid(pj2, [cid, cid]), false);
// ============ L3 mod.maipai:8 个重复id 必须被拒、不得进入出牌阶段 ============
const pj3 = newPaiju(3, 0, 0);
const s3 = setup('00000', pj3);
const dup = id(1, 1, 13);
mod.maipai({ data: { seat: 0, cards: [dup, dup, dup, dup, dup, dup, dup, dup] }, conmode: 0, fromid: 0 });
t.eq('埋牌 反 8个重复id 被拒(step仍为3)', pj3.step, 3);
t.eq('埋牌 反 8个重复id 未埋下任何牌', pj3.cards.filter(c => c.playround === 0).length, 0);
// 对照:8 张不同的牌可以正常埋下
const pj4 = newPaiju(3, 0, 0);
setup('00000', pj4);
mod.maipai({ data: { seat: 0, cards: [0, 1, 2, 3, 4, 5, 6, 7] }, conmode: 0, fromid: 0 });
t.eq('埋牌 正 8张不同牌 埋牌成功(step进5)', pj4.step, 5);
t.eq('埋牌 正 实际埋下8张', pj4.cards.filter(c => c.playround === 0).length, 8);
// ============ L3 mod.chupai:越界/非数组入参不得抛异常 ============
const pj5 = newPaiju(5, 0, 0);
P.new_playround(pj5, 1, 0);
const s5 = setup('00000', pj5);
let threw = null;
try {
mod.chupai({ data: { seat: 0, cards: [999] }, conmode: 0, fromid: 0 });
mod.chupai({ data: { seat: 0, cards: undefined }, conmode: 0, fromid: 0 });
mod.chupai({ data: { seat: 0, cards: 'x' }, conmode: 0, fromid: 0 });
mod.chupai({ data: { seat: 0, cards: [dup, dup] }, conmode: 0, fromid: 0 });
} catch (e) { threw = String(e); }
t.eq('出牌 反 非法入参不抛异常', threw, null);
t.eq('出牌 反 非法入参不产生任何出牌', pj5.cards.filter(c => c.playround > 0).length, 0);
// ============ design §9:报无主后立即为全体三人刷新主牌统计 ============
// 造 step5 牌局:seat0 手上只剩 1 张主牌(打掉即报无主),seat1/seat2 各持若干主牌
const pj6 = newPaiju(5, 0, 0);
for (let i = 0; i < 108; i++) pj6.cards[i].dealowner = -1; // 先全部清空
const give = (cid, seat) => { pj6.cards[cid].dealowner = seat + 1; pj6.cards[cid].playround = -1; };
give(id(1, 1, 5), 0); // seat0:主♦5(唯一主牌)
give(id(1, 3, 9), 0); // seat0:副♥9(保证还有牌)
give(id(1, 1, 13), 1); give(id(2, 1, 13), 1); give(id(1, 1, 12), 1); // seat1:主♦K对 + 主♦Q
give(id(1, 1, 7), 2); // seat2:正7(固定主牌)
P.new_playround(pj6, 1, 0);
t.eq('报无主前 三家统计均为初始 [-1,-1]', pj6.seatlist.map(s => s[4]), [[-1, -1], [-1, -1], [-1, -1]]);
P.do_playcard(pj6, [id(1, 1, 5)]); // seat0 打出唯一主牌 → 报无主
t.eq('报无主 seat0 主牌清空 [0,0]', pj6.seatlist[0][4], [0, 0]);
t.eq('报无主 立即刷新 seat1 [3张,1对]', pj6.seatlist[1][4], [3, 1]);
t.eq('报无主 立即刷新 seat2 [1张,0对]', pj6.seatlist[2][4], [1, 0]);
t.eq('报无主 seat0 主花色标志置1', pj6.seatlist[0][pj6.flower - 1], [1, 1]);
t.eq('报无主 seat2 无主对→无对标志置1', pj6.seatlist[2][pj6.flower - 1][1], 1);
process.exit(t.done('input') ? 0 : 1);