Files
erqiwang_youle/server/games/erqiwang/test/test_input.js
T
joywayerandClaude Opus 5 4276d38267 二七王:报无主后立即为全体三人刷新主牌统计
design §9:「一旦有玩家的主牌全部打空,系统就为全体三人显示另外
两家各自的主牌数量、以及这些主牌里有多少对子」。

原实现只在出牌者自己出牌时更新自己那一格 seatlist[seat][4],所以
A 打空主牌报无主的那一刻,B、C 的主牌数/对子数仍是初始 [-1,-1],
要等各自轮到出牌才补上,界面最多滞后两次出牌才完整。

改为:出牌后先判定出牌者主牌是否出空,一旦 have_baofu() 成立,就
按各家实际手牌为三个座位统一重算 [剩余主牌数, 剩余主对数] 及主花
色标志。重算幂等——主牌一旦出空不会再有,[4][0] 恒为 0。

test_rpc 的 mkChupai 原先只把 seatlist[1][4][0] 置 0 伪造报无主,
而不真的清空 seat1 的主牌;新逻辑按实际手牌重算会把这个假状态纠
正回去,导致 baozhu 断言失败。这是新实现更严格的正确表现,故改测
试造真状态(把 seat1 的主牌标为已出),不是放宽断言。

test_input 新增 6 项:报无主前三家均为 [-1,-1];打空瞬间三家统计
与主花色标志同时刷新到实际值。

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

116 lines
6.1 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: {}, 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
};
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);