一致性测试(同一局真包:逐包增量回放 vs 一次性 deskinfo 重连)暴露的六处前端缺陷:
① PlayHandler:table.playproc 由「字段映射错误」改为原样拷贝
原实现把这一手的 seat/cards/count/flower/cardtype 灌进 playproc,而服务端 playproc
是本轮进行态(round/start/currseat/.../cards),两者几乎无交集、撞名的 cards 语义还相反。
服务端已在 chupai1/2/3 补发 playproc,前端零推导直接镜像,不自行累积 round/maxseat。
② 本手信息改走 EQW_CARD_PLAYED 事件载荷,不进 GameState
{order, seat, cards, count?, flower?, cardtype?, shuai?, shuaicuo?},带 ? 的缺字段不兜底。
落牌是一次性表现而非对局状态;新立 GameState 字段只会再制造一处增量/全量不一致。
③ PlayHandler:table.pushlist 逐手落盘、按轮归档(GameState.pushPlay)
chupai1 开新的一轮、2/3 落进当前轮,任意时刻都与 deskinfo 的全量重建相等(服务端
pushlist 含进行中的当前轮)。不读 chupai3.playproc.cards——那是下一轮的进行态(协议 §13)。
仅可查牌模式累积,与服务端同一道门控,否则不查牌房会凭空攒出重连没有的历史。
④ ResultHandler:归档收尾轮(最后一手走 jiesuan.chupai,不发 chupai3)
夹具实证 chupai1×28 / chupai2×28 / chupai3×27。jiesuan.chupai 只有 seat/cards/maxseat。
归档随即被结算收敛清空(deskinfo step6 不带 PushCards),但 emit 同步,落牌表现在清空前取值。
⑤ BuryHandler:接住 maipai 新增的 seatlist(此刻为全初值,界面无差别但重连侧有)
⑥ ResyncHandler 的 ChooseMain/BuryCards 补映射 curmultiple(原重连重建为 0,抓分角标掉档);
BuryHandler 埋牌完成后清空 my.bottomCards——底牌上庄时已并入手牌,埋牌后只剩 burycards
有意义,deskinfo 的 PushCards 不下发 bottomcards,重连重建恒为 [](语义已逐条核实)。
一致性测试 8 红 → 3 红。剩余 3 条同源:服务端 maipai 漏发 playproc(与本次服务端补发
seatlist 是同一个窗口、同一个理由),前端无法自补且不得伪造,详见报告 §7 待裁决。
另有 test_handlers_play.js:84 断言的正是 ① 修掉的错误映射,按纪律未改测试,见报告 §8。
未触碰 server/、client/tests/(含夹具与 EXCLUDE);服务端 29 checks 全绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
142 lines
5.7 KiB
JavaScript
142 lines
5.7 KiB
JavaScript
///////////////////////////////////////////////////////////////
|
||
////////// 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'
|
||
});
|
||
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' });
|
||
}
|
||
};
|