Files
joywayerandClaude Opus 5 8b2cfdaf27 二七王:前端删掉 9 处阶段/控制权推导,一律读服务端包字段
I-3 前端不再持有对局状态机(配合服务端上一提交的下发面补齐):
- 删掉 5 处按 rpc 名硬编码的 aset.step(Deal=1 / Call=2 / Main=3 / Bury=5 / Result=6),
  一律改 _apply 读包里的 step
- 删掉 4 处 turn.seat = aset.banker(CallHandler / MainHandler / ResyncHandler×2),
  增量路径读 nextseat、重连路径读分组里的 seat;同一条规则不再有 4 个写入处

I-2 ResyncHandler._applyBalance 由只接 aset 扩为接 aset/bottom/account,
结算面板开着时重连/硬刷新不再丢抠底明细与末局大结算。
一致性测试 EXCLUDE 随之从 4 条收缩为 2 条(去掉 result.bottom / result.account,
只留一次性事件 table.ancard3s 与归下一轮的 result.chupai),未新增任何豁免条目。

夹具按新包结构重跑(种子不变,牌局不变):diff 纯为 67 行新增字段、0 行删除;
test_fixture 补上 step/nextseat/Balance 同源断言(68→82 checks)。
export_packets.js 里关于 playproc/seatlist 活引用的注释已过时,更正为仍存在的
Balance.readystate 与三份结算快照。

各 handler 单测补齐新字段并新增守卫用例:刻意构造 nextseat != banker、
step 为非预期值的包,使「退回硬编码/banker 反推」的写法必然转红(11/11 已验证)。

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

159 lines
7.3 KiB
JavaScript
Raw Permalink 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.
// 重连(deskinfo)与开局(StartWar)
const { load } = require('./_load');
const t = require('./_assert')();
load('client/js/gameabc-framework/system/EventBus.js');
load('client/js/01_SubGame/codes/state/Events.js');
load('client/js/01_SubGame/codes/state/GameState.js');
load('client/js/01_SubGame/codes/state/RoomOptions.js');
load('client/js/01_SubGame/codes/net/handlers/ResyncHandler.js');
const S = EQW_GameState;
const evts = [];
Object.keys(EQW_Events).forEach(k => EventBus.on(EQW_Events[k], () => evts.push(k)));
// ================= setRoomDes:房间选项 =================
S.reset(); S.room.options = null;
EQW_ResyncHandler.handleSetRoomDes(123456, 12, '10100');
t.eq('总局数', S.room.asetCount, 12);
t.eq('傍王开', S.room.options.bangwang, 1);
t.eq('可查牌', S.room.options.nocheck, 0);
// ================= deskinfo:step 5 出牌阶段 =================
S.reset(); S.room.mySeat = 1; evts.length = 0;
EQW_ResyncHandler.handleDeskinfo({
count: 6, idx: 2, PlayerInfo: [10, -5, -5], step: 5, MyCards: [1,2,3],
PushCards: {
banker: 0, call: 60, multiple: 3, flower: 3, countdown: 15,
grade: 25, playproc: { round: 4, currseat: 1, cards: [[9],[],[]] },
seatlist: [[0,0,0,0,[5,2]],[0,0,0,0,[6,1]],[0,0,0,0,[7,3]]],
liangpai: { cards: [52,53] }, pushlist: [[[1],[2],[3]]],
curmultiple: 2, mustcard: [4,5], baozhu: 1
}
});
t.eq('总局数', S.room.asetCount, 6);
t.eq('当前局', S.room.asetIdx, 2);
t.eq('三家总分', S.room.playerScores, [10, -5, -5]);
t.eq('阶段', S.aset.step, 5);
t.eq('手牌', S.my.cards, [1,2,3]);
t.eq('庄家', S.aset.banker, 0);
t.eq('主牌花色', S.aset.flower, 3);
t.eq('捡分', S.aset.grade, 25);
t.eq('抓分倍数', S.aset.curmultiple, 2);
t.eq('当前轮出牌情况', S.table.playproc.round, 4);
t.eq('出牌历史', S.table.pushlist.length, 1);
t.eq('亮牌', S.table.liangpai, { cards: [52,53] });
t.eq('必出牌', S.my.mustCard, [4,5]);
t.eq('控制权(由 playproc.currseat 给出)', S.turn.seat, 1);
t.eq('重连发全量重画事件', evts.indexOf('EQW_RESYNC_ALL') >= 0, true);
// 报无主:明牌按钮与余主公示的开关(协议 PushCards.baozhu)。
// 增量路径由 chupai1/2/3 的 baozhu 维护,重连路径必须一并恢复,
// 否则重连后按钮凭空消失(两条路径对同一状态给出不同结果)。
t.eq('报无主(PushCards.baozhu)', S.aset.baozhu, 1);
// 反面:不查牌房服务端恒发 0,前端照写不反推
S.reset(); S.room.mySeat = 1;
EQW_ResyncHandler.handleDeskinfo({
count: 6, idx: 2, PlayerInfo: [0,0,0], step: 5, MyCards: [1],
PushCards: { banker: 0, flower: 3, playproc: { round: 1, currseat: 1, cards: [[],[],[]] }, baozhu: 0 }
});
t.eq('不查牌 baozhu 恒 0', S.aset.baozhu, 0);
// ================= deskinfo:step 1 叫分阶段 =================
S.reset(); evts.length = 0;
EQW_ResyncHandler.handleDeskinfo({
count: 6, idx: 1, PlayerInfo: [0,0,0], step: 1, MyCards: [7,8],
CallRun: { seat: 2, countdown: 15, nowcall: 65, multiple: 2, call: [null, 0, 65] }
});
t.eq('叫分阶段', S.aset.step, 1);
t.eq('当前叫分者', S.turn.seat, 2);
t.eq('当前叫到的分', S.call.currcall, 65);
t.eq('三家叫分', S.call.calls, [null, 0, 65]);
// ================= deskinfo:step 2 选主阶段(庄家有 bottomcards)=================
S.reset(); evts.length = 0;
EQW_ResyncHandler.handleDeskinfo({
count: 6, idx: 1, PlayerInfo: [0,0,0], step: 2, MyCards: [1,2],
ChooseMain: { seat: 0, banker: 0, call: 70, multiple: 2, countdown: 20,
bottomcards: [1,2,3,4,5,6,7,8], touxiang: 1 }
});
t.eq('选主阶段', S.aset.step, 2);
t.eq('底牌', S.my.bottomCards, [1,2,3,4,5,6,7,8]);
t.eq('允许投降', S.aset.touxiang, 1);
t.eq('控制权读 ChooseMain.seat', S.turn.seat, 0);
// 重连【不重放】70 分开底:deskinfo 里没有 ancard3s
t.eq('重连不重放开底', S.table.ancard3s, 0);
// 【守卫】控制权取分组里的 seat(服务端权威),不用本地 banker 反推。
// 真实链路里 ChooseMain.seat 恒 == banker,相等时"读哪个"分辨不出来;这里刻意让它们不同,
// 退回 `turn.seat = aset.banker` 的写法必转红。
S.reset();
EQW_ResyncHandler.handleDeskinfo({
count: 6, idx: 1, PlayerInfo: [0,0,0], step: 2, MyCards: [1,2],
ChooseMain: { seat: 2, banker: 0, call: 70, multiple: 2, countdown: 20, touxiang: 1 }
});
t.eq('ChooseMain 控制权取 seat,不取 banker', S.turn.seat, 2);
// ================= deskinfo:step 3 埋牌阶段 =================
S.reset();
EQW_ResyncHandler.handleDeskinfo({
count: 6, idx: 1, PlayerInfo: [0,0,0], step: 3, MyCards: [1,2],
BuryCards: { seat: 0, banker: 0, call: 65, multiple: 2, flower: 1, countdown: 25,
bottomcards: [1,2,3,4,5,6,7,8], curmultiple: 3 }
});
t.eq('埋牌阶段', S.aset.step, 3);
t.eq('控制权读 BuryCards.seat', S.turn.seat, 0);
// 同样的守卫:seat 与 banker 刻意不同
S.reset();
EQW_ResyncHandler.handleDeskinfo({
count: 6, idx: 1, PlayerInfo: [0,0,0], step: 3, MyCards: [1,2],
BuryCards: { seat: 1, banker: 0, call: 65, multiple: 2, flower: 1, countdown: 25 }
});
t.eq('BuryCards 控制权取 seat,不取 banker', S.turn.seat, 1);
// ================= deskinfo:step 6 结算阶段 =================
S.reset(); evts.length = 0;
EQW_ResyncHandler.handleDeskinfo({
count: 6, idx: 6, PlayerInfo: [12,-6,-6], step: 6, MyCards: [],
Balance: { readystate: [1,0,0], aset: { banker: 0, upgrade: 1, seatlist: [] },
bottom: { cards: [1,2,3,4,5,6,7,8], multiple: 2, grade1: 20, grade2: 40 },
account: [{ score: 12 }, { score: -6 }, { score: -6 }] }
});
t.eq('结算阶段', S.aset.step, 6);
t.eq('小局结算数据', S.result.aset.upgrade, 1);
// I-2:结算面板还开着时重连,抠底明细与大局结算必须一并恢复(与 jiesuan 推送同一份数据)。
// 漏接会让重连后抠底明细、末局大结算全部空白
t.eq('重连恢复抠底明细', S.result.bottom.grade2, 40);
t.eq('重连恢复大局结算', S.result.account.length, 3);
// 反面:非末局 / 投降局的 Balance 没有 bottom / account,缺字段保持 null,不兜底造分组
S.reset();
EQW_ResyncHandler.handleDeskinfo({
count: 6, idx: 2, PlayerInfo: [0,0,0], step: 6, MyCards: [],
Balance: { readystate: [0,0,0], aset: { banker: 0, upgrade: -99, seatlist: [] } }
});
t.eq('无抠底分组时保持 null', S.result.bottom, null);
t.eq('无大局结算时保持 null', S.result.account, null);
// ================= StartWar:差异化下发 =================
S.reset(); S.room.mySeat = 2; evts.length = 0;
EQW_ResyncHandler.handleStartWar({ data: { deskwar: { sendtype: 1, seatlist: [
{ seat: 0, data: { count: 6, idx: 1, PlayerInfo: [0,0,0], step: 1, MyCards: [90] } },
{ seat: 2, data: { count: 6, idx: 1, PlayerInfo: [0,0,0], step: 1, MyCards: [11,12] } }
] } } });
t.eq('按本座位取到自己那份', S.my.cards, [11,12]);
// 非差异化下发:直接用 data
S.reset(); S.room.mySeat = 0;
EQW_ResyncHandler.handleStartWar({ data: { count: 6, idx: 1, PlayerInfo: [0,0,0],
step: 1, MyCards: [3,4] } });
t.eq('非差异化下发', S.my.cards, [3,4]);
// 畸形入参不崩
let boom = false;
try { EQW_ResyncHandler.handleStartWar(null); EQW_ResyncHandler.handleDeskinfo(null); }
catch (e) { boom = true; }
t.eq('畸形入参不崩', boom, false);
process.exit(t.done('resync') ? 0 : 1);