/////////////////////////////////////////////////////////////// ////////// EQW_GameState: 服务端快照的镜像(前端 SSOT)//////// /////////////////////////////////////////////////////////////// // 前端红线「视图 = f(服务端快照)」的落点:本对象只存【服务端下发过的字段】, // 不存任何前端算出来的结论。手牌排序、牌面标记、花色统计等由 core/ 的纯函数 // 在渲染时现算,不落地成状态。 // // 三条硬约束: // 1. 只镜像不派生——存进来的必须是服务端给过的 // 2. 字段名对齐协议——协议叫 currcall 就叫 currcall,便于逐条核对 // 3. 缺字段不兜底——包里没有的保持原值,不填默认值掩盖;必需字段缺失 // 则 console.error 并跳过该包(服务端漏发,修在服务端) var EQW_GameState = EQW_GameState || { //数据分组名(snapshot / reset 按此遍历;新增分组要同步加进来) _GROUPS: ['room', 'aset', 'turn', 'call', 'my', 'table', 'result'], //—— 房间级(跨小局)—— room: { asetCount: 0, //总局数 ← fapai.asetcount / deskinfo.count / setRoomDes asetIdx: 0, //当前第几局 ← fapai.asetidx / deskinfo.idx mySeat: -1, //自己的座位 ← 平台 C_Player.seat playerScores: [], //三家总积分 ← deskinfo.PlayerInfo / jiesuan.aset.seatlist[].score,跨局不清 options: null //roomtype 解析结果 }, //—— 小局级 —— aset: { step: 0, //1叫分 2选主/投降 3埋牌 5出牌 6结算 banker: -1, call: -1, //庄家叫分 multiple: 0, //基础子数 flower: 0, //主牌花色(0 = 未选主) curmultiple: 0, //当前抓分倍数(带符号,服务端权威,前端不自算) grade: 0, //闲家已捡分 baozhu: 0, //是否已有人报无主 touxiang: 0 //是否允许投降 }, //—— 当前控制权与倒计时(任何阶段都读这里)—— turn: { seat: -1, //该谁操作 countdown: 0 //展示用秒数;服务端无对应定时器,归零【不做任何界面推进】 }, //—— 叫分过程 —— call: { currcall: 0, calls: [null, null, null] //null 未叫 / 0 不叫 / >0 叫了多少 }, //—— 自己 —— my: { cards: [], mustCard: [], //本轮必出牌(服务端建议,只发给轮到的那家) bottomCards: [], //底牌(庄家恒有;闲家仅 70 分坐庄时有) buryCards: [] //埋牌底牌(仅庄家) }, //—— 桌面 —— table: { ancard3s: 0, //开底标志(70 分坐庄) playproc: null, pushlist: [], seatlist: [], liangpai: null, mingpai: null }, //—— 结算 —— //字段集与 deskinfo step6 的 Balance 严格对齐(aset / bottom / account)。 //【不存 chupai】收尾墩的「谁最大、得几分」是一次性表现,随 EQW_TRICK_END 事件载荷走, //任何 deskinfo 都不下发它——存进来就是一处服务端不知道的私有对局态(见 ResultHandler / PlayHandler) result: { bottom: null, aset: null, account: null }, //新一局开局时清空对局态;room.mySeat / room.options / room.playerScores 跨局不变,不清 //(playerScores 由 deskinfo.PlayerInfo 或 jiesuan.aset.seatlist[].score 增量更新,见 ResyncHandler / ResultHandler) reset: function () { this.room.asetIdx = 0; this.aset = { step: 0, banker: -1, call: -1, multiple: 0, flower: 0, curmultiple: 0, grade: 0, baozhu: 0, touxiang: 0 }; this.turn = { seat: -1, countdown: 0 }; this.call = { currcall: 0, calls: [null, null, null] }; this.my = { cards: [], mustCard: [], bottomCards: [], buryCards: [] }; this.table = { ancard3s: 0, playproc: null, pushlist: [], seatlist: [], liangpai: null, mingpai: null }; this.result = { bottom: null, aset: null, account: null }; }, //深拷贝快照(只含数据分组,不含方法),供测试做「增量 vs 全量」比对 snapshot: function () { var out = {}; for (var i = 0; i < this._GROUPS.length; i++) { var g = this._GROUPS[i]; out[g] = JSON.parse(JSON.stringify(this[g])); } return out; }, //把「一手牌」记进出牌历史 table.pushlist。 //结构与 deskinfo.PushCards.pushlist 完全一致:外层下标 = 轮次-1,内层恒 3 个数组、 //下标 = 座位序号(协议 §断线重连 pushlist)。 // //【只记录服务端发来的事实】:谁、出了哪几张。轮次的推进不在这里推导—— //startRound 由调用方按【包类型】给出:chupai1 就是「一轮的第一手」(服务端定义的包, //不是前端算出来的),其余各手落进当前这一轮。 // //【为什么不能读 chupai3.playproc.cards 直接归档】:服务端 do_playcard 在第三家出完后 //就地调用 new_playround,chupai3 带的是【下一轮】的进行态(round+1、cards 全空, //协议 §13 明标),本轮那三手只在 chupai1/2/3 各自的 seat + cards 里。 // //【仅可查牌模式累积】:pushlist 是「查牌」历史,不查牌模式下服务端的 deskinfo 压根 //不下发它(design §9),前端也不能自己攒一份——否则增量路径会凭空多出重连路径没有的历史。 pushPlay: function (seat, cards, startRound) { var opts = this.room.options; if (!opts || opts.nocheck) { return; } var list = this.table.pushlist; if (startRound || list.length === 0) { list.push([[], [], []]); } list[list.length - 1][seat] = cards; }, //把包里的字段写进目标分组。map = { 目标键: 源键 }。 //【缺字段不兜底】源键在包里不存在就不写,保持原值——不填默认值掩盖漏发。 _apply: function (target, src, map) { for (var dst in map) { if (!map.hasOwnProperty(dst)) { continue; } var srcKey = map[dst]; if (src.hasOwnProperty(srcKey)) { target[dst] = src[srcKey]; } } } };