/////////////////////////////////////////////////////////////// ////////// EQW_Rpc: 语义化发包(一操作一方法)///////////////// /////////////////////////////////////////////////////////////// // 前端 04 §1:业务不直接拼包,走两层封装—— // 业务 → EQW_Rpc.xxx(意图) → RpcHelper(注入平台字段)→ 引擎发送 // // 【route 必须是 erqiwang】RpcHelper.sendGameRpc 预设 route="room"(平台房间模块), // 用它包会被路由到平台、永远到不了子游戏 mod.js,而且不会有任何报错。 // // 【请求包只带意图,不带结论】(前端 04 §1 / 服务端 04 §8) // 可以带:操作类型、目标标识(牌 id / 花色 / 提示类型)、座位号(仅供一致性校验)。 // 禁止带:分数、番数、判定结果、阶段推进指令、nextSeat、handCards。 // 前端 shared/ 的计算只用于本地提示与预校验,【结果不回传】。 // 一句话:前端能自己推出来的东西,就是玩家能改的东西——这是防作弊的根本。 // // 本文件只做【形状】校验(牌 id 范围/去重/张数),不做【规则】校验 // (这手牌能不能出、叫分够不够低由服务端裁定)。形状不合法就 fail-fast 抛错, // 不发出残缺包——服务端会按 PARAM 拒绝,但让错误暴露在最近处更省事。 var EQW_Rpc = EQW_Rpc || { APP: 'youle', ROUTE: 'erqiwang', //—— 内部:取自己的座位(fail-fast)—— //room.mySeat 由 SubGameHooks._syncMySeat 从平台 C_Player.seat 同步,初始值是 -1。 //【为什么不能透传】seat:-1 发出去,服务端 check_player 必然返回 falsy → 每个操作都回 ERR.PLAYER, //叫分/选主/埋牌/出牌全部发不出去,而前端一声不吭。无声透传正是这类缺陷能长期潜伏的原因, //故与本文件其余形状校验一样在最近处显式失败。 _seat: function () { var seat = EQW_GameState.room.mySeat; if (seat !== 0 && seat !== 1 && seat !== 2) { throw new Error('[EQW_Rpc] 座位未同步: ' + seat + '(须为 0/1/2;检查 SubGameHooks._syncMySeat 是否已由平台入口调用)'); } return seat; }, //—— 内部:发送 —— _send: function (rpc, data) { var pack = { seat: this._seat() }; var key; for (key in data) { if (data.hasOwnProperty(key)) { pack[key] = data[key]; } } RpcHelper.sendRpc(this.APP, this.ROUTE, rpc, pack); }, //—— 内部:牌 id 列表形状校验(协议 §0.2)—— //必须是非空数组,元素是 0–107 的整数,互不重复 _checkCards: function (cards, expectCount) { if (Object.prototype.toString.call(cards) !== '[object Array]' || cards.length === 0) { throw new Error('[EQW_Rpc] cards 必须是非空数组'); } if (typeof expectCount === 'number' && cards.length !== expectCount) { throw new Error('[EQW_Rpc] cards 张数应为 ' + expectCount + ',实际 ' + cards.length); } var seen = {}; for (var i = 0; i < cards.length; i++) { var c = cards[i]; if (typeof c !== 'number' || c !== Math.floor(c) || c < 0 || c > 107) { throw new Error('[EQW_Rpc] 非法牌 id: ' + c + '(须为 0–107 的整数)'); } if (seen[c]) { throw new Error('[EQW_Rpc] 牌 id 重复: ' + c); } seen[c] = true; } }, //—— 叫分(call: 5–70 的 5 的倍数;0 = 不叫)—— jiaofen: function (call) { if (typeof call !== 'number' || call !== Math.floor(call)) { throw new Error('[EQW_Rpc] call 必须是整数'); } if (call !== 0 && (call < 5 || call > 70 || call % 5 !== 0)) { throw new Error('[EQW_Rpc] 非法叫分: ' + call + '(须为 0 或 5–70 的 5 的倍数)'); } this._send('jiaofen', { call: call }); }, //—— 投降(仅 70 分坐庄、选主阶段;合法性由服务端裁定)—— touxiang: function () { this._send('touxiang', {}); }, //—— 选主(flower: 1方块 2梅花 3红心 4黑桃)—— xuanzhu: function (flower) { if (typeof flower !== 'number' || flower < 1 || flower > 4) { throw new Error('[EQW_Rpc] 非法花色: ' + flower + '(须为 1–4)'); } this._send('xuanzhu', { flower: flower }); }, //—— 埋牌(恒 8 张)—— maipai: function (cards) { this._checkCards(cards, 8); this._send('maipai', { cards: cards }); }, //—— 出牌 —— chupai: function (cards) { this._checkCards(cards); this._send('chupai', { cards: cards }); }, //—— 明牌(查看他家未出主牌)—— mingpai: function () { this._send('mingpai', {}); }, //—— 出牌提示(tip: 1踩 2没分 3有分)—— tishi: function (tip) { if (tip !== 1 && tip !== 2 && tip !== 3) { throw new Error('[EQW_Rpc] 非法提示类型: ' + tip + '(须为 1踩 2没分 3有分)'); } this._send('tishi', { tip: tip }); }, //—— 准备 —— zhunbei: function () { this._send('zhunbei', {}); } };