S-1 当前抓分倍数(design §7.2.0): - class.paiju.js 新增 get_curmultiple(),复用 get_qvalue + get_upgrade, 与结算 aset.upgrade 同源同算法,差别仅在不含扣底(末轮才产生)。 - 随现有包下发、不另开推送:shangzhuang / chupai1-3 / deskinfo.PushCards。 - 三家同值,整表下发不涉及泄露(捡分本就公开);叫分未定时为 0。 S-2 跟牌必出牌(design §5.2): - class.paiju.js 新增 get_mustcard(seat),内部复用与出牌校验完全相同的 get_followcard,故建议与校验天然一致。 - 【只发给 nextseat 一家】:它是该玩家自己手牌的子集,整表下发会泄露 他家手牌结构(server 红线:发全 ≠ 发多)。 - 四种情形不下发:非出牌阶段、该座位是本轮首家、首家甩牌、必出牌为空。 甩牌一条尤其重要——甩牌跟牌走 flush_follow_ok 的逐分量匹配,与 get_followcard 不是同一条路径,给建议会给错。 测试(新增 test/test_hint.js,33 checks): - curmultiple:算法层逐档对齐 design §7.2.0 判定表(含爬坡 Q 分段)、 三家同值、重连与出牌包一致、叫分未定为 0、与 get_upgrade 绝对值一致。 - mustcard:只有 nextseat 收到、另两家没有、内容确属收包者自己的牌、 首家无、缺门有富余时无、手牌数恰等于首家张数时整手必出、甩牌不发、 非出牌阶段不发、重连按 currseat 门控。 过程中修正的两个测试自身问题(均为脚本缺陷,非业务缺陷,业务判定经证据核实正确): - 「缺门方无必出」原用例给闲2 只发 2 张牌,恰等于首家张数,get_followcard 正确判定为「整手必出」;改为发 4 张才留出选择余地,并补一条对照用例 锁住「恰等于张数则整手必出」这个正确行为。 - 「甩牌不发」「非出牌阶段不发」两条原本是假通过——桩的 startcount 仍是 -1, 前置守卫先返回了 null,根本没测到被测因素。改为先把桩推进到跟牌态、 每个用例只变动一个因素,并加一条「自证」用例确认推进后确实能算出必出牌。 另:mustcard 补进 test_leak.js 的 CARD_FIELDS 审计白名单(与 burycards 同理, 不加则误发不会被泄露审计发现)。反向验证:改成整表下发后,审计立刻报出 5 张越权牌;把 curmultiple 写死为 1 后,两条断言失败。均已还原、全绿。 协议文档同步 shangzhuang / chupai1-3 / PushCards 的字段说明。 至此 §7.5 服务端待补 5 条全部完成,服务端侧无遗留。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
259 lines
10 KiB
JavaScript
259 lines
10 KiB
JavaScript
///////////////////////////////////////////////////
|
||
/////// 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;
|
||
//当前抓分倍数(design §7.2.0):与 chupai 包同源同值,重连后角标立即正确
|
||
deskinfo.PushCards.curmultiple = paiju.method.get_curmultiple();
|
||
//当前的出牌情况
|
||
deskinfo.PushCards.playproc = paiju.playproc;
|
||
//本轮跟牌的必出牌(design §5.2):只在【轮到本座位】且本轮已有人出牌时才有,
|
||
//算的也只是本座位自己的手牌,不涉及他家(server 红线:按可见性下发)
|
||
if (paiju.playproc && paiju.playproc.currseat == seat){
|
||
var _must = paiju.method.get_mustcard(seat);
|
||
if (_must){
|
||
deskinfo.PushCards.mustcard = _must;
|
||
}
|
||
}
|
||
//座位列表(报副统计)与亮牌:仅可查牌模式下展示(§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;
|
||
} |