route 显式写 erqiwang——RpcHelper.sendGameRpc 预设 route="room", 用它包会被路由到平台房间模块、到不了子游戏 mod.js 且不报错。 只做形状校验(牌 id 范围/去重/张数),规则合法性由服务端裁定; 不合形状即抛错、不发残缺包。测试断言发包字段集合不含任何结论字段。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
115 lines
4.6 KiB
JavaScript
115 lines
4.6 KiB
JavaScript
///////////////////////////////////////////////////////////////
|
||
////////// 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', {});
|
||
}
|
||
};
|