Files
erqiwang_youle/client/js/01_SubGame/codes/net/Rpc.js
T
joywayerandClaude Opus 5 ff46d874c0 二七王:前端语义化发包
route 显式写 erqiwang——RpcHelper.sendGameRpc 预设 route="room",
用它包会被路由到平台房间模块、到不了子游戏 mod.js 且不报错。

只做形状校验(牌 id 范围/去重/张数),规则合法性由服务端裁定;
不合形状即抛错、不发残缺包。测试断言发包字段集合不含任何结论字段。

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

115 lines
4.6 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_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',
//—— 内部:取自己的座位 ——
_seat: function () {
return EQW_GameState.room.mySeat;
},
//—— 内部:发送 ——
_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', {});
}
};