Files
erqiwang_youle/client/js/01_SubGame/codes/net/handlers/ResyncHandler.js
T
joywayerandClaude Opus 5 dac4a94b38 二七王:夹具补甩牌样本,重连恢复 baozhu
导出脚本原来全出单张,单张一手无所谓顺序,服务端两个顺序口径的分岔
在夹具里永远测不出来。改出牌策略(种子不变):首家优先合法甩牌 → 领
最大的一对 → 最小单张;跟家凑合法跟牌;提交时刻意反转成升序,模拟真实
客户端的点击顺序,把「下发顺序是否被请求顺序污染」暴露出来。
中段 step5 快照锚点从 1 个加到 5 个——开局那三张快照的 pushlist 还是空的,
只有打到一半的快照才真的比对到出牌历史。

重导结果:51 手出牌,一手多张 30 手、合法甩牌 4 手,deskinfo 快照 18 张,
仍覆盖 step 1/2/3/5/6;连跑两次逐字节相同。
该样本在修复前使 test_consistency 5 个比对点转红,修复后全绿。

前端 ResyncHandler 补上 PushCards.baozhu 的映射(服务端已补发),
test_resync 加正反两条断言。

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

145 lines
6.0 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_ResyncHandler: 重连(deskinfo)与开局(StartWar)
///////////////////////////////////////////////////////////////
// 前端 04 §3:重连 = 重画。断线重连与硬刷新本质相同,复用同一条路径:
// 填 GameState → emit EQW_RESYNC_ALL → 各组件据数据重建界面
// 【绝不为重连单写一套渲染】。
//
// deskinfo 按 step 只带对应阶段的分组(协议文末):
// 恒有 count / idx / PlayerInfo / step / MyCards
// CallRun(step1) ChooseMain(step2) BuryCards(step3) PushCards(step5) Balance(step6)
//
// 【重连不重放一次性事件】:70 分坐庄的 3 秒开底是上庄时的一次性事件,
// deskinfo 里没有 ancard3s,前端也不得补播。
var EQW_ResyncHandler = EQW_ResyncHandler || {
//平台房间信息(roomcode / 总局数 / roomtype 位串)
handleSetRoomDes: function (roomcode, asetcount, roomtype) {
if (typeof asetcount === 'number') { EQW_GameState.room.asetCount = asetcount; }
EQW_GameState.room.options = EQW_RoomOptions_Parse.parse(roomtype);
},
//开局:makewar 可能差异化下发(sendtype:1 + seatlist[]),先取自己那份
handleStartWar: function (msg) {
if (!msg || !msg.data) {
console.warn('[EQW_ResyncHandler] StartWar 无数据,已忽略');
return;
}
var raw = msg.data.deskwar || msg.data;
var mine = raw;
if (raw && raw.sendtype === 1 && Object.prototype.toString.call(raw.seatlist) === '[object Array]') {
mine = null;
for (var i = 0; i < raw.seatlist.length; i++) {
if (raw.seatlist[i] && raw.seatlist[i].seat === EQW_GameState.room.mySeat) {
mine = raw.seatlist[i].data;
break;
}
}
if (!mine) {
console.error('[EQW_ResyncHandler] StartWar 差异化下发里没有本座位的数据');
return;
}
}
this.handleDeskinfo(mine);
},
//重连:deskinfo 全量快照
handleDeskinfo: function (info) {
if (!info) {
console.warn('[EQW_ResyncHandler] deskinfo 为空,已忽略');
return;
}
EQW_GameState.reset();
//—— 恒有的部分 ——
EQW_GameState._apply(EQW_GameState.room, info, {
asetCount: 'count',
asetIdx: 'idx',
playerScores: 'PlayerInfo'
});
EQW_GameState._apply(EQW_GameState.aset, info, { step: 'step' });
EQW_GameState._apply(EQW_GameState.my, info, { cards: 'MyCards' });
//—— 按阶段分组 ——
if (info.CallRun) { this._applyCallRun(info.CallRun); }
if (info.ChooseMain) { this._applyChooseMain(info.ChooseMain); }
if (info.BuryCards) { this._applyBuryCards(info.BuryCards); }
if (info.PushCards) { this._applyPushCards(info.PushCards); }
if (info.Balance) { this._applyBalance(info.Balance); }
EventBus.emit(EQW_Events.EQW_RESYNC_ALL);
},
_applyCallRun: function (g) {
EQW_GameState._apply(EQW_GameState.call, g, {
currcall: 'nowcall',
calls: 'call'
});
EQW_GameState._apply(EQW_GameState.aset, g, { multiple: 'multiple' });
EQW_GameState._apply(EQW_GameState.turn, g, {
seat: 'seat',
countdown: 'countdown'
});
},
_applyChooseMain: function (g) {
EQW_GameState._apply(EQW_GameState.aset, g, {
banker: 'banker',
call: 'call',
multiple: 'multiple',
touxiang: 'touxiang',
curmultiple: 'curmultiple' //与 shangzhuang 推送同源同值,漏读会让「抓分」角标掉档
});
EQW_GameState._apply(EQW_GameState.my, g, { bottomCards: 'bottomcards' });
EQW_GameState._apply(EQW_GameState.turn, g, { countdown: 'countdown' });
EQW_GameState.turn.seat = EQW_GameState.aset.banker; //选主由庄家做
},
_applyBuryCards: function (g) {
EQW_GameState._apply(EQW_GameState.aset, g, {
banker: 'banker',
call: 'call',
multiple: 'multiple',
flower: 'flower',
curmultiple: 'curmultiple' //同上:一张牌未出时恒为 +3(大光),也必须重建
});
EQW_GameState._apply(EQW_GameState.my, g, { bottomCards: 'bottomcards' });
EQW_GameState._apply(EQW_GameState.turn, g, { countdown: 'countdown' });
EQW_GameState.turn.seat = EQW_GameState.aset.banker; //埋牌由庄家做
},
_applyPushCards: function (g) {
EQW_GameState._apply(EQW_GameState.aset, g, {
banker: 'banker',
call: 'call',
multiple: 'multiple',
flower: 'flower',
grade: 'grade',
curmultiple: 'curmultiple',
//报无主:明牌按钮与余主公示的开关,增量路径由 chupai1/2/3 的 baozhu 维护,
//重连路径必须从 PushCards.baozhu 一起恢复,否则重连后按钮凭空消失
baozhu: 'baozhu'
});
EQW_GameState._apply(EQW_GameState.my, g, {
buryCards: 'burycards',
mustCard: 'mustcard'
});
EQW_GameState._apply(EQW_GameState.table, g, {
playproc: 'playproc',
pushlist: 'pushlist',
seatlist: 'seatlist',
liangpai: 'liangpai'
});
EQW_GameState._apply(EQW_GameState.turn, g, { countdown: 'countdown' });
//当前该谁出牌由 playproc.currseat 给出(服务端权威,前端不推导)
if (g.playproc && typeof g.playproc.currseat === 'number') {
EQW_GameState.turn.seat = g.playproc.currseat;
}
},
_applyBalance: function (g) {
EQW_GameState._apply(EQW_GameState.result, g, { aset: 'aset' });
}
};