Files
erqiwang_youle/server/games/erqiwang/class.export.js
T
joywayerandClaude Opus 5 ecec44e93a 二七王:S-4 拆分 bottomcards 字段名,S-5 术语同步收口
S-4(服务端改动):底牌与埋牌底牌不再共用一个字段名。
- bottomcards = 底牌(发牌留桌 8 张)—— shangzhuang / ChooseMain / BuryCards,保持不变
- burycards   = 埋牌底牌(庄家埋下 8 张)—— maipai(mod.js)、PushCards(class.export.js)
- 结算 bottom.cards 未改名(分组名已表明是抠底相关),文档注明其语义

测试:
- test_leak.js 的 CARD_FIELDS 白名单加入 burycards。这一步是必须的——不加的话
  新字段会静默脱离下发面审计,闲家误收也发现不了。
- test_rpc.js 新增 11 条正反用例:庄家有 burycards 且为 8 张、庄家不再有
  bottomcards(抓改名漏改)、闲家两个字段皆无、重连 PushCards 同上。

验证(不止「跑过了」):
- 改动前基线 16 个文件全绿;改动后仍全绿,新增后 rpc 从 89 → 100 checks。
- 反向验证一:把 mod.js 的 burycards 改回 bottomcards → test_rpc 新用例 FAIL,
  且 test_leak 报出真实泄露(赋值改了而 delete 没改时,闲家会收到庄家埋的牌)。
- 反向验证二:删掉闲家侧的 delete msg.data.burycards → test_leak 立刻抓出
  5 张越权牌,确认 burycards 确已纳入审计白名单。
两次验证后均已还原并复跑全绿。

S-5:术语同步收口。design.md / packet_protocol.md / 代码注释均已改用
「底牌 / 埋牌底牌」;compliance 三份历史核对记录有意不改(反映当时的 design
表述,下轮核对按新 design 重做)。

另:T-9 亮牌条文案暂定「x对 x主」(取 liangpai.zhupair 与 zhu,其余档位
先不渲染、协议不变)。至此 29 项待确认规格全部有结论。

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

249 lines
9.5 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.
///////////////////////////////////////////////////
/////// cls_youle_erqiwang_export: 输出接口 ///////
///////////////////////////////////////////////////
//跨模块依赖:Node 走 require 守卫;友乐/浏览器由 min_loadJsFile 加载为同名全局(dev-guide 01 §1)
if (typeof require !== "undefined"){
var cls_youle_erqiwang_config = require("./class.config.js");
var cls_youle_erqiwang_arith = require("./class.arith.js");
var cls_youle_erqiwang_desk = require("./class.desk.js");
}
var cls_youle_erqiwang_export = cls_youle_erqiwang_export || {
new: function() {
var exp = {};
//解析roomtype获取开房所需房卡(design §10.1)
exp.get_needroomcard = function(roomtype){
var cfg = cls_youle_erqiwang_config.parse(roomtype);
if (cfg.deductAA){
//AA每人扣卡:6局每人1张 / 12局每人2张
return cfg.aset12 ? 2 : 1;
}
//房主扣卡:6局2张 / 12局4张
return cfg.aset12 ? 4 : 2;
}
//解析roomtype获取总局数(design §10.1:6局 / 12局)
exp.get_asetcount = function(roomtype){
var cfg = cls_youle_erqiwang_config.parse(roomtype);
return cfg.aset12 ? 12 : 6;
}
//加入房间时根据roomtype获取需要的房卡数量
exp.get_needroomcard_joinroom = function(roomtype){
var cfg = cls_youle_erqiwang_config.parse(roomtype);
if (cfg.deductAA){
//AA每人扣卡:加入者按自己那份扣(6局1张 / 12局2张)
return cfg.aset12 ? 2 : 1;
}
//房主扣卡:加入者不扣
return 0;
}
//开战
exp.makewar = function(o_room){
//牌桌对象
o_room.o_desk = cls_youle_erqiwang_desk.new(o_room);
for (var i = 0; i < o_room.seatlist.length; i++) {
o_room.seatlist[i].gameinfo.isbet = 1;
}
//开始第一局
var do_newpaiju = function(){
o_room.o_desk.method.do_new_paiju(0);
}
min_ontimeout(do_newpaiju, 1000);
}
//获取某个玩家的牌桌整桌当前信息
exp.get_deskinfo = function(o_room, seat){
if (!o_room.o_desk){
return null;
}
var paiju = o_room.o_desk.method.curr_paiju();
//本局房间可选规则:爬坡改变「叫分 → 基础子数」的取值(design §7.3.1),
//各阶段下发的 multiple 必须与结算包 aset.multiple 用同一套规则
var cfg = cls_youle_erqiwang_config.parse(o_room.roomtype);
var deskinfo = {};
deskinfo.count = o_room.asetcount; //总局数
deskinfo.idx = paiju.idx; //当前局数
deskinfo.PlayerInfo = []; //玩家当前累积得分
for (var i = 0; i < o_room.o_desk.seatlist.length; i++) {
deskinfo.PlayerInfo.push(o_room.o_desk.seatlist[i][0]);
};
deskinfo.step = paiju.step; //牌局状态
deskinfo.MyCards = paiju.method.get_seat_cards(seat); //我手上的牌
switch (paiju.step){
case 1: //发完牌叫分
deskinfo.CallRun = {};
//当前叫分位置
deskinfo.CallRun.seat = paiju.method.get_callgrade_seat();
//叫分倒计时
deskinfo.CallRun.countdown = o_room.o_desk.method.get_countdown_jiaofen();
//当前叫分
deskinfo.CallRun.nowcall = paiju.method.get_callgrade_value();
//当前叫分倍数
deskinfo.CallRun.multiple = cls_youle_erqiwang_arith.get_base_bycall(deskinfo.CallRun.nowcall, cfg.climb);
//所有玩家的叫分情况
deskinfo.CallRun.call = [null, null, null];
deskinfo.CallRun.call[0] = paiju.method.get_seat_callgrade(0);
deskinfo.CallRun.call[1] = paiju.method.get_seat_callgrade(1);
deskinfo.CallRun.call[2] = paiju.method.get_seat_callgrade(2);
break;
case 2: //选主
deskinfo.ChooseMain = {};
//庄家
deskinfo.ChooseMain.banker = paiju.banker;
//庄家叫分
deskinfo.ChooseMain.call = paiju.call;
//庄家叫分倍数
deskinfo.ChooseMain.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//选主倒计时
deskinfo.ChooseMain.countdown = o_room.o_desk.method.get_countdown_xuanzhu();
//底牌(发牌时没发给玩家、扣在桌面的 8 张):只有庄家可见(70分的3秒亮牌是上庄时的一次性事件,重连不重放)
if (seat == paiju.banker){
deskinfo.ChooseMain.bottomcards = paiju.method.get_bottomcards();
}
//是否可投降
if (paiju.call != 70){
deskinfo.ChooseMain.touxiang = 0;
} else {
deskinfo.ChooseMain.touxiang = 1;
}
break;
case 3: //埋牌
deskinfo.BuryCards = {};
//庄家
deskinfo.BuryCards.banker = paiju.banker;
//庄家叫分
deskinfo.BuryCards.call = paiju.call;
//庄家叫分倍数
deskinfo.BuryCards.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//埋牌倒计时
deskinfo.BuryCards.countdown = o_room.o_desk.method.get_countdown_maipai();
//底牌(发牌留桌的 8 张):只有庄家可见
if (seat == paiju.banker){
deskinfo.BuryCards.bottomcards = paiju.method.get_bottomcards();
}
//主牌花色
deskinfo.BuryCards.flower = paiju.flower;
//埋牌阶段无投降(投降是选主阶段与选主互斥的选择)
break;
case 5: //出牌
deskinfo.PushCards = {};
//庄家
deskinfo.PushCards.banker = paiju.banker;
//庄家叫分
deskinfo.PushCards.call = paiju.call;
//庄家叫分倍数
deskinfo.PushCards.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//出牌倒计时
deskinfo.PushCards.countdown = o_room.o_desk.method.get_countdown_chupai();
//主牌花色
deskinfo.PushCards.flower = paiju.flower;
//捡分
var jian_grade = paiju.method.get_jian_grade();
if (seat == paiju.banker){
//埋牌底牌(庄家埋下的 8 张)。与「底牌」(发牌留桌 8 张,见 ChooseMain/BuryCards.bottomcards)
//是两批不同的牌,字段名刻意区分,勿混用
deskinfo.PushCards.burycards = paiju.method.get_burycard();
} else {
//闲家当前的捡分分牌
deskinfo.PushCards.gradecards = jian_grade.cards;
}
//闲家当前的捡分分数
deskinfo.PushCards.grade = jian_grade.grade;
//当前的出牌情况
deskinfo.PushCards.playproc = paiju.playproc;
//座位列表(报副统计)与亮牌:仅可查牌模式下展示(§9/§8.2)
if (!cfg.nocheck){
deskinfo.PushCards.seatlist = paiju.seatlist;
if (seat != paiju.banker){
var _lp = paiju.method.get_liangpai();
if (_lp){
deskinfo.PushCards.liangpai = _lp;
}
}
}
//出牌历史:往轮打出、已被收走的牌属于「查牌」范畴,仅可查牌模式下发(design §9)。
//不查牌模式下不带此属性——当前这一轮桌面上的牌由上面的 playproc 恢复,两种模式都有,
//否则后出的人无从跟牌(design §9 末尾的边界说明)
if (!cfg.nocheck){
deskinfo.PushCards.pushlist = [];
for (var i = 0; i < paiju.cards.length; i++) {
var pai = paiju.cards[i];
if (pai.playround > 0){
if (deskinfo.PushCards.pushlist.length < pai.playround){
deskinfo.PushCards.pushlist.length = pai.playround;
}
if (!deskinfo.PushCards.pushlist[pai.playround - 1]){
deskinfo.PushCards.pushlist[pai.playround - 1] = [[], [], []];
}
var owner = pai.dealowner;
if (owner == 0){
owner = paiju.banker + 1;
}
deskinfo.PushCards.pushlist[pai.playround - 1][owner - 1].push(i);
}
}
//按大小排序:外层下标=轮次、内层恒为 3 个座位;排序须按本局主牌花色
//(内层曾误用轮次数作上界,会把内层数组撑成轮次长度并填入 undefined;
// 且曾误用上一个循环泄漏出来的 pai.flower——那是最后一张牌大王的花色 5,
// 等于按「无主牌」排序)
for (var i = 0; i < deskinfo.PushCards.pushlist.length; i++) {
if (!deskinfo.PushCards.pushlist[i]){
continue;
}
for (var j = 0; j < 3; j++) {
deskinfo.PushCards.pushlist[i][j] = cls_youle_erqiwang_arith.order_cards(paiju.flower, deskinfo.PushCards.pushlist[i][j]);
}
}
}
break;
case 6: //结算
deskinfo.Balance = {};
//玩家的准备状态
deskinfo.Balance.readystate = o_room.o_desk.prepare;
if (o_room.o_desk.prepare[seat] == 0){
//结算情况
deskinfo.Balance.aset = paiju.tmp_jiesuan_aset;
}
break;
}
return deskinfo;
}
//解散房间
//平台在 battlestate==1 时即会调用(rpc.js 开战处 makewar 后立刻置 1),而本游戏的首个牌局
//是 makewar 里 min_ontimeout(...,1000) 延迟创建的——这段窗口内 o_desk 已存在但 paiju_list 为空。
//此时返回 null(平台对 null 的 _deskfree 有分支:照常发解散包、不带 deskfree),
//而不是让 curr_paiju() 的 undefined 解引用抛异常打断整条解散链路。
exp.get_disbandRoom = function(o_room){
if (!o_room.o_desk){
return null;
}
var paiju = o_room.o_desk.method.curr_paiju();
if (!paiju){
return null;
}
var msg = {};
return paiju.method.get_paiju_account(2, msg);
}
return exp;
}
}
//对内输出接口(友乐/浏览器:youle_erqiwang 由 mod.js 先建;Node 单测无此全局时跳过)
if (typeof youle_erqiwang !== "undefined"){
youle_erqiwang.export = cls_youle_erqiwang_export.new();
}
//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)
if (typeof module !== "undefined"){
module.exports = cls_youle_erqiwang_export;
}