Files
erqiwang_youle/client/js/01_SubGame/codes/net/handlers/ResultHandler.js
T
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

116 lines
6.3 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.
///////////////////////////////////////////////////////////////
////////// EQW_ResultHandler: 结算(jiesuan)与解散(Free)///
///////////////////////////////////////////////////////////////
// 协议 §14:结算包由 chupai + bottom + aset 拼装,末局再加 account。
// 三种来源的组成不同:
// 正常出牌结算:chupai + bottom + aset(末局加 account)
// 投降结算: 只有 aset(末局加 account),无 chupai、无 bottom
// 解散结算: 只有 aset + account,且【走另一个入口】,见下
//
// 【解散的特殊性】(协议 §14.1,已对平台代码核实):
// 解散包的 route 是平台的 "room",平台 12_Logic.js 按 route 分流时把它交给
// Net[rpc] 而【不是】Game_Modify._ReceiveData——所以它根本进不了子游戏的分发表。
// 平台另有专门入口:07_Desk.js 的 Game_Modify.Free(Desk.deskfree)。
// 两个后果:
// 1. 取值路径比协议文档少一层——平台已把 deskfree 取出来传进来,
// 所以是 deskfree.data.aset,不是 data.deskfree.data.aset;
// 2. 参数可能是 null——解散若发生在开战后、首局发牌前,服务端 get_disbandRoom
// 返回 null,平台走「不带 deskfree」分支。这时只做房间收尾、不弹结算。
var EQW_ResultHandler = EQW_ResultHandler || {
handleJiesuan: function (data) {
if (!data.hasOwnProperty('aset')) {
console.error('[EQW_ResultHandler] jiesuan 缺 aset,已跳过该包');
return;
}
this._applyResult(data);
},
//平台解散入口。deskfree = { rpc:"jiesuan", data:{ success, aset, account } },可能为 null
handleFree: function (deskfree) {
if (!deskfree || !deskfree.data) {
//首局发牌前解散:没有结算数据,只做房间收尾,不弹结算面板
console.warn('[EQW_ResultHandler] 解散包无 deskfree 数据(首局发牌前解散)');
return;
}
var d = deskfree.data;
if (!d.hasOwnProperty('aset')) {
console.error('[EQW_ResultHandler] 解散包缺 aset,已跳过');
return;
}
this._applyResult(d);
},
//三种来源共用的落地逻辑:有哪组就写哪组,缺的保持 null
_applyResult: function (d) {
//—— 收尾轮的第三手:牌局在最后一手打完时【不发 chupai3】,整包换成 jiesuan(协议 §13/§14)——
//夹具实跑一局的包数就是证据:chupai1×28 / chupai2×28 / chupai3×27。
//只在 chupai3 归档会永远丢掉最后一轮,故在这里补齐——它必是本轮第三手,落进当前这一轮。
//jiesuan.chupai 的字段与 chupai3 不同:只有 seat / cards / maxseat,
//没有 order / playproc / cardsinhand / grade。
//归档随后会被下面的结算收敛清掉(deskinfo step6 不带 PushCards),但 emit 是同步的:
//落牌与收牌表现在清空之前就已取到完整的收尾轮。
if (d.chupai && d.chupai.hasOwnProperty('seat') && d.chupai.hasOwnProperty('cards')) {
EQW_GameState.pushPlay(d.chupai.seat, d.chupai.cards, false);
var lastPlay = { seat: d.chupai.seat, cards: d.chupai.cards };
if (d.chupai.hasOwnProperty('maxseat')) { lastPlay.maxseat = d.chupai.maxseat; }
EventBus.emit(EQW_Events.EQW_CARD_PLAYED, lastPlay);
}
//结算是"清空过程量、只留结算数据"的快照点:deskinfo step6 只带 Balance(=aset),
//其余分组一律回到 reset() 默认值——jiesuan 也要收敛到同一状态,否则重连前后画面不一致
//(清单以 ResyncHandler.handleDeskinfo 在 step6 时实际重建出的字段集为准,不多清不少清)。
EQW_GameState.aset.banker = -1;
EQW_GameState.aset.call = -1;
EQW_GameState.aset.multiple = 0;
EQW_GameState.aset.flower = 0;
EQW_GameState.aset.curmultiple = 0;
EQW_GameState.aset.grade = 0;
EQW_GameState.aset.baozhu = 0;
EQW_GameState.aset.touxiang = 0;
EQW_GameState.turn.seat = -1;
EQW_GameState.turn.countdown = 0;
EQW_GameState.call.currcall = 0;
EQW_GameState.call.calls = [null, null, null];
//末轮若由 jiesuan.chupai 带出(无 cardsinhand),出牌者自己的手牌不会被清空,这里显式清掉,
//不留幽灵牌——deskinfo 在结算阶段的 MyCards 恒为 []
EQW_GameState.my.cards = [];
EQW_GameState.my.mustCard = [];
EQW_GameState.my.bottomCards = [];
EQW_GameState.my.buryCards = [];
EQW_GameState.table.ancard3s = 0;
EQW_GameState.table.playproc = null;
EQW_GameState.table.pushlist = [];
EQW_GameState.table.seatlist = [];
EQW_GameState.table.liangpai = null;
EQW_GameState.table.mingpai = null;
EQW_GameState._apply(EQW_GameState.result, d, {
chupai: 'chupai',
bottom: 'bottom',
aset: 'aset',
account: 'account'
});
//阶段只读包字段:jiesuan 的 step 由服务端 get_paiju_account 统一给出(三种结算来源恒为 6),
//前端不按 rpc 名硬编码——解散结算走的是另一条投递路径,同样带着这个字段
EQW_GameState._apply(EQW_GameState.aset, d, { step: 'step' });
//三家总积分:jiesuan 的 aset.seatlist[i].score 就是"累积得分"(协议 §14),
//与 deskinfo.PlayerInfo 同源同值——deskinfo 恒有的 PlayerInfo 在增量路径上没有对应来源,
//只能从这里回填,否则一局中段(fapai 后至 jiesuan 前)room.playerScores 永远追不上服务端
if (d.aset && Object.prototype.toString.call(d.aset.seatlist) === '[object Array]') {
var scores = [];
for (var i = 0; i < d.aset.seatlist.length; i++) {
scores.push(d.aset.seatlist[i] && d.aset.seatlist[i].score);
}
EQW_GameState.room.playerScores = scores;
}
EventBus.emit(EQW_Events.EQW_ASET_RESULT);
//account 只在末局或解散时才有
if (d.hasOwnProperty('account')) {
EventBus.emit(EQW_Events.EQW_ACCOUNT_RESULT);
}
}
};