Files
erqiwang_youle/server/games/erqiwang/class.export.js
T
joywayerandClaude Opus 5 ce4c972b7b 二七王:修复跟牌判定依赖客户端入参顺序(可翻转本轮胜负与结算)
can_followcard 尾段计算 cardvalue/noflower/nopair 时用的是入参原始数组
followcards(客户端提交顺序),而 get_pairlist/get_tuolaji_list 都按降序
相邻取对。端到端实测:闲家用主拖拉机毙牌且为末轮,降序提交时闲家赢下本轮
(捡分 40、扣底 ×4),把同一手牌打乱成 [51,50,105,104] 后对子漏判、
cardvalue 归 0,变成庄家赢、闲家 0 分大光、不扣底——同一手合法牌仅靠数组
顺序就能翻转胜负、捡分归属与最终结算,同源问题还能抹掉 noflower 缺门标志
污染 §9 下发给全场的牌况表。

改为在 min_ary_deduct 削减 _followcards 之前另存完整排序快照 _sortfollow,
尾段 8 处全部改用它(_followcards 会被削减、不可复用)。

顺带给 get_disbandRoom 加空守卫:平台在 makewar 后立刻置 battlestate=1,
而首局是延迟 1 秒创建的,这段窗口内解散会在 curr_paiju() 的 undefined 上
解引用抛异常、打断平台解散链路;现返回 null 走平台既有的「不带 deskfree」分支。

补 8 条回归(入参顺序无关性 5 条 + 端到端 1 条 + 解散空守卫 2 条),
已用「回退修复 → 用例转红」反验有效性;全套单测 401 项全绿。

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

248 lines
9.3 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();
//底牌:暗牌只有庄家可见(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();
//底牌:暗牌只有庄家可见
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){
//埋的牌
deskinfo.PushCards.bottomcards = 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;
}