diff --git a/client/js/01_SubGame/codes/net/Dispatcher.js b/client/js/01_SubGame/codes/net/Dispatcher.js index 2d4a3a7..2f8c38c 100644 --- a/client/js/01_SubGame/codes/net/Dispatcher.js +++ b/client/js/01_SubGame/codes/net/Dispatcher.js @@ -49,3 +49,18 @@ var EQW_Dispatcher = EQW_Dispatcher || { } } }; + +//—— 路由表:一 rpc 一处理器(协议里每个服务端推送都要在这里注册)—— +//新增一种推送 = 加一条注册 + 写对应 handler;本文件不写业务。 +EQW_Dispatcher.register('fapai', function (d) { EQW_DealHandler.handle(d); }); +EQW_Dispatcher.register('jiaofen', function (d) { EQW_CallHandler.handleJiaofen(d); }); +EQW_Dispatcher.register('shangzhuang', function (d) { EQW_CallHandler.handleShangzhuang(d); }); +EQW_Dispatcher.register('xuanzhu', function (d) { EQW_MainHandler.handleXuanzhu(d); }); +EQW_Dispatcher.register('maipai', function (d) { EQW_BuryHandler.handle(d); }); +EQW_Dispatcher.register('chupai1', function (d) { EQW_PlayHandler.handle(d, 1); }); +EQW_Dispatcher.register('chupai2', function (d) { EQW_PlayHandler.handle(d, 2); }); +EQW_Dispatcher.register('chupai3', function (d) { EQW_PlayHandler.handle(d, 3); }); +EQW_Dispatcher.register('mingpai', function (d) { EQW_QueryHandler.handleMingpai(d); }); +EQW_Dispatcher.register('tishi', function (d) { EQW_QueryHandler.handleTishi(d); }); +EQW_Dispatcher.register('jiesuan', function (d) { EQW_ResultHandler.handleJiesuan(d); }); +EQW_Dispatcher.register('zhunbei', function (d) { EQW_ReadyHandler.handle(d); }); diff --git a/client/js/01_SubGame/codes/net/handlers/ResyncHandler.js b/client/js/01_SubGame/codes/net/handlers/ResyncHandler.js new file mode 100644 index 0000000..f1a0e57 --- /dev/null +++ b/client/js/01_SubGame/codes/net/handlers/ResyncHandler.js @@ -0,0 +1,139 @@ +/////////////////////////////////////////////////////////////// +////////// 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' + }); + 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' + }); + 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' }); + } +}; diff --git a/client/tests/test_dispatcher.js b/client/tests/test_dispatcher.js index 6908dd4..42a42c2 100644 --- a/client/tests/test_dispatcher.js +++ b/client/tests/test_dispatcher.js @@ -4,7 +4,18 @@ 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/FailHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/DealHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/CallHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/MainHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/BuryHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/PlayHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/QueryHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/ReadyHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/ResultHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/ResyncHandler.js'); load('client/js/01_SubGame/codes/net/Dispatcher.js'); // ---- 注册与分发 ---- @@ -40,16 +51,9 @@ t.eq('未知 rpc 不抛错', threw, false); }); // ---- 路由表完整性:协议里每个服务端推送 rpc 都要有 handler ---- -// (本条在 Task 10 全部 handler 就位后才会真正通过;此刻先写下期望, -// Task 10 完成时它会自动转绿——这正是它存在的意义:漏接一个包就会红) -// Task 10 完成后取消注释以下代码块 -/* -load('client/js/01_SubGame/codes/state/GameState.js'); -load('client/js/01_SubGame/codes/state/RoomOptions.js'); ['fapai','jiaofen','shangzhuang','xuanzhu','maipai', 'chupai1','chupai2','chupai3','mingpai','tishi','jiesuan','zhunbei'].forEach(rpc => { t.eq('已注册 handler: ' + rpc, EQW_Dispatcher.hasHandler(rpc), true); }); -*/ process.exit(t.done('dispatcher') ? 0 : 1); diff --git a/client/tests/test_resync.js b/client/tests/test_resync.js new file mode 100644 index 0000000..be82efe --- /dev/null +++ b/client/tests/test_resync.js @@ -0,0 +1,105 @@ +// 重连(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] + } +}); +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); + +// ================= 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: { 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('控制权归庄家', S.turn.seat, 0); + +// 重连【不重放】70 分开底:deskinfo 里没有 ancard3s +t.eq('重连不重放开底', S.table.ancard3s, 0); + +// ================= 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: [] } } +}); +t.eq('结算阶段', S.aset.step, 6); +t.eq('小局结算数据', S.result.aset.upgrade, 1); + +// ================= 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);