二七王服务端:按 design.md 整改结算/算奖/甩牌,roomtype 改位串

结算与算奖(§6.3/§7/§8)
- 扣底倍数改线性:单张×1、主对×2、N连对×2N
- 算子实现常规算子 + 爬坡两套(大光×3、Q=40 / 爬坡分段),X=基础子数×判定倍率
- 常规算奖只计庄家;连对链修复(传实际主花色);移除 ≥10 老主误加奖与设计外的
  「冲关必叫/四王必踢」作废;算奖快照取庄家埋牌后28张(新增 get_seat_cards_award)
- 算奖并入结算改为 X×(2Ni−Nj−Nk);傍王按房间开关、庄闲每王1奖

出牌与甩牌(§5.4)
- 副牌绝对禁甩;甩牌最大性按对手全部手牌逐分量判定(新增 trump_rank/
  decompose_trump/opp_can_beat_flush);甩错惩罚(收回、只打最小一张、下发 shuaicuo)
- 修复 can_followcard 引用未定义 tuolaji_list 的崩溃、can_playcard 14张上限失效

开局/投降/暗牌(§4)
- 投降改为选主阶段(step2)与选主互斥、不埋牌、算奖用庄家36张;埋牌后直进出牌(删废弃step4)
- 70分暗牌摸牌前向所有玩家亮3秒(+ancard3s),非70分不再泄露暗牌给闲家;叫分上限≤70

查牌/亮牌(§8.2/§9)
- 新增亮牌 get_liangpai(庄家埋牌后28张按阈值统计);报无主统计与亮牌按查牌位门控
- 新增 mingpai 明牌 RPC(可查牌+已报无主时查看他家全部主牌)

房间与配置(§10)
- 局数6/12、房主扣卡2/4;新增 class.config.js 以位串解析 roomtype(唯一入口 SSOT)
- 清理 do_choiceflower 死代码

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 22:32:53 +08:00
co-authored by Claude Opus 4.8
parent 96372d713c
commit 6120580505
5 changed files with 601 additions and 387 deletions
+206 -113
View File
@@ -3,19 +3,66 @@
///////////////////////////////////////////////////
var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || {
//根据叫分计算倍数
get_multiple_bycall: function(callgrade){
if (callgrade > 60){
return 1;
}
if (callgrade > 40){
//根据叫分计算基础子数(design §7.1 常规算子 / §7.3.1 爬坡)
//climb: 是否勾选爬坡。70 分打牌基础子数 2;投降(基础1)在结算处特殊处理
get_base_bycall: function(callgrade, climb){
if (callgrade == 70){
return 2;
}
if (callgrade > 0){
return 4;
if (!climb){
//常规算子:65→2 60→3 55→4 50及以下统一6
if (callgrade == 65) return 2;
if (callgrade == 60) return 3;
if (callgrade == 55) return 4;
if (callgrade >= 5 && callgrade <= 50) return 6;
return 0;
}
//爬坡:65→2 起每低5分+1子,直到5分→15
switch (callgrade){
case 65: return 2;
case 60: return 3;
case 55: return 4;
case 50: return 6;
case 45: return 7;
case 40: return 8;
case 35: return 9;
case 30: return 10;
case 25: return 11;
case 20: return 12;
case 15: return 13;
case 10: return 14;
case 5: return 15;
}
return 0;
},
},
//小光/过庄分界与升级级距 Q(design §7.2.0 常规固定40 / §7.3.2 爬坡分段)
get_qvalue: function(callgrade, climb){
if (!climb){
return 40;
}
if (callgrade >= 45){
return 40;
}
if (callgrade == 40 || callgrade == 35){
return 20;
}
if (callgrade == 30 || callgrade == 25){
return 15;
}
if (callgrade == 20 || callgrade == 15){
return 10;
}
if (callgrade == 10 || callgrade == 5){
return 5;
}
return 40;
},
//(兼容)叫分对应的基础子数,供叫分阶段展示,默认按常规算子取值
get_multiple_bycall: function(callgrade){
return cls_youle_erqiwang_arith.get_base_bycall(callgrade, false);
},
//牌id转花色
id_to_flower: function(cardid){
@@ -231,8 +278,104 @@ var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || {
return tuolaji;
},
//主牌归一化大小(副7 同级取 7000、副2 同级取 2000,其余用牌编码),仅用于甩牌分量比大小
trump_rank: function(mainflower, cardid){
var code = cls_youle_erqiwang_arith.id_to_code(mainflower, cardid);
if (code > 7000 && code < 8000){ //副7
return 7000;
}
if (code > 2000 && code < 3000){ //副2
return 2000;
}
return code;
},
//把一组主牌分解为分量列表(design §5.4.3/§5.4.4)
//返回 [{type:'single'|'pair'|'tractor', len, value, cards:[]}...],value 为该分量最大牌的归一化 rank
decompose_trump: function(mainflower, cards){
var ordered = cls_youle_erqiwang_arith.order_cards(mainflower, cards.concat());
//先分出对子与单张
var pairs = []; //{code, cards:[id,id]}
var singles = []; //{card}
var i = 0;
while (i < ordered.length){
var code_i = cls_youle_erqiwang_arith.id_to_code(mainflower, ordered[i]);
if (i + 1 < ordered.length){
var code_n = cls_youle_erqiwang_arith.id_to_code(mainflower, ordered[i + 1]);
if (code_i == code_n){
pairs.push({code: code_i, cards: [ordered[i], ordered[i + 1]]});
i = i + 2;
continue;
}
}
singles.push(ordered[i]);
i++;
}
//连续对子组成拖拉机,其余为单独对子
var comps = [];
var j = 0;
while (j < pairs.length){
var run = [pairs[j]];
var k = j;
while (k + 1 < pairs.length && cls_youle_erqiwang_arith.is_continuous(pairs[k].code, pairs[k + 1].code)){
run.push(pairs[k + 1]);
k++;
}
if (run.length >= 2){
var tcards = [];
for (var r = 0; r < run.length; r++){
tcards = tcards.concat(run[r].cards);
}
comps.push({type: "tractor", len: run.length, value: cls_youle_erqiwang_arith.trump_rank(mainflower, run[0].cards[0]), cards: tcards});
} else {
comps.push({type: "pair", len: 1, value: cls_youle_erqiwang_arith.trump_rank(mainflower, run[0].cards[0]), cards: run[0].cards});
}
j = k + 1;
}
for (var s = 0; s < singles.length; s++){
comps.push({type: "single", len: 1, value: cls_youle_erqiwang_arith.trump_rank(mainflower, singles[s]), cards: [singles[s]]});
}
return comps;
},
//甩牌最大性判定(design §5.4.2):两名对手是否持有能压过甩牌任一分量的主牌/主对/主拖拉机
//comps:甩牌分量;opp_zhulist:各对手未出的主牌id列表数组。返回 true 表示有人能压(甩错)
opp_can_beat_flush: function(mainflower, comps, opp_zhulist){
for (var o = 0; o < opp_zhulist.length; o++){
var zhu = cls_youle_erqiwang_arith.order_cards(mainflower, opp_zhulist[o].concat());
var pairs = cls_youle_erqiwang_arith.get_pairlist(mainflower, zhu);
for (var c = 0; c < comps.length; c++){
var comp = comps[c];
if (comp.type == "single"){
//对手持有任一 rank 更大的主牌单张即可压
for (var a = 0; a < zhu.length; a++){
if (cls_youle_erqiwang_arith.trump_rank(mainflower, zhu[a]) > comp.value){
return true;
}
}
} else if (comp.type == "pair"){
//对手持有 rank 更大的主对即可压
for (var p = 0; p < pairs.length; p++){
if (cls_youle_erqiwang_arith.trump_rank(mainflower, pairs[p][0]) > comp.value){
return true;
}
}
} else {
//拖拉机:对手持有同长度、顶牌 rank 更大的主拖拉机即可压
var tlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairs, 300 + comp.len);
for (var t = 0; t < tlist.length; t++){
if (cls_youle_erqiwang_arith.trump_rank(mainflower, tlist[t][0]) > comp.value){
return true;
}
}
}
}
}
return false;
},
//第一个位置是否可出牌
can_playcard: function(mainflower, cards, selfseat, seatlist){
can_playcard: function(mainflower, cards, selfseat, seatlist, opp_zhulist){
/*
参数:
mainflower: 主牌花色
@@ -273,7 +416,7 @@ var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || {
//规则外的约定,一次最多只能出14张牌
if (cards.length > 14){
do_returnfalse;
do_returnfalse();
return can;
}
@@ -380,64 +523,33 @@ var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || {
}
}
/////////// 甩牌处理 ///////////
//其他玩家没有主牌
var other_noflower = function(_flower){
for (var i = 0; i < seatlist.length; i++){
if (i != selfseat){
if (seatlist[i][_flower - 1][0] == 0){
return false;
}
/////////// 甩牌处理(design §5.4)///////////
//把要出的牌分解为分量;>1 个分量即为甩牌
var comps = cls_youle_erqiwang_arith.decompose_trump(mainflower, cards);
if (comps.length > 1){
//副牌绝对禁甩(§5.4.1):甩牌必须全是主牌
var allTrump = true;
for (var ci = 0; ci < cards.length; ci++){
if (cls_youle_erqiwang_arith.id_to_code(mainflower, cards[ci]) < 1000){
allTrump = false;
break;
}
}
return true;
}
//其他玩家没有主对
var other_noflowerpair = function(_flower){
for (var i = 0; i < seatlist.length; i++){
if (i != selfseat){
if (seatlist[i][_flower - 1][1] == 0){
return false;
}
}
}
return true;
}
//不可甩牌判断
if (can.cardtype > 101 && can.cardtype < 200){ //单张甩牌
//其他玩家有主牌则不能甩牌
if (!other_noflower(mainflower)){
if (!allTrump){
//副牌不能甩,属非法出牌(无甩错惩罚,直接拒绝)
do_returnfalse();
return can;
}
//副牌甩牌需要满足其他玩家没有主牌且其他玩家都没有该花色的牌
if (!other_noflower(can.flower)){
//最大性判定(§5.4.2):按全部手牌,两对手都不持有能压过任一分量的主牌
if (cls_youle_erqiwang_arith.opp_can_beat_flush(mainflower, comps, opp_zhulist)){
//甩错(§5.4.5):do_playcard 据此收回甩牌、只强制打出最小一张
do_returnfalse();
can.shuaicuo = true;
can.smallest = cards[cards.length - 1]; //cards 已从大到小排序,末位最小
return can;
}
} else if (can.cardtype > 201 && can.cardtype < 300){ //对子甩牌
if (can.flower == mainflower){ //甩主对
//其他玩家有主牌对子则不能甩牌
if (!other_noflowerpair(mainflower)){
do_returnfalse();
return can;
}
} else { //甩副牌对子
//其他玩家有主牌则不能甩牌
if (!other_noflower(mainflower)){
do_returnfalse();
return can;
}
//副牌甩牌需要其他玩家没有主牌且其他玩家没有该花色的对子
if (!other_noflowerpair(can.flower)){
do_returnfalse();
return can;
}
}
}
return can;
},
@@ -798,7 +910,9 @@ var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || {
return can;
}
//要是正确的拖拉机牌型
if (tuolaji_list.length == 0){
var _fpairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _followcards);
var _ftljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _fpairlist, get.cantype);
if (_ftljlist.length == 0){
do_returnfalse();
return can;
}
@@ -922,51 +1036,41 @@ var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || {
}
},
//闲家抠底倍数
//闲家抠底倍数(design §6.3:单张主 ×1、主对子 ×2、N 连对拖拉机 ×2N)
get_bottom_multiple: function(mainflower, cards){
//拷贝数组
var _cards = cards.concat();
var _cards = cards.concat();
//将牌从大到小排序
_cards = cls_youle_erqiwang_arith.order_cards(mainflower, _cards);
//扣底要求赢下最后一轮的牌全是主牌(首末张都 >=1000 即全为主牌)
var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[0]);
if (_code < 1000){
return 0;
}
var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[_cards.length - 1]);
_code = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[_cards.length - 1]);
if (_code < 1000){
return 0;
}
//单张主牌不翻倍
var _pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _cards);
if (_pairlist.length == 0){
return 2;
return 1;
}
var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 302);
if (_tljlist.length == 0){
return 4;
}
var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 303);
if (_tljlist.length == 0){
return 8;
//取牌中最长的连对数 N,倍数为 2N(主对子按 N=1,即 2 倍)
var _n = 1;
var _len = 2;
while (true){
var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 300 + _len);
if (_tljlist.length == 0){
break;
}
_n = _len;
_len = _len + 1;
}
var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 304);
if (_tljlist.length == 0){
return 16;
}
var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 305);
if (_tljlist.length == 0){
return 32;
}
var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 306);
if (_tljlist.length == 0){
return 64;
}
return 2 * _n;
},
//冲关计算关数
@@ -1042,48 +1146,37 @@ var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || {
}
}
//六2六7判断
//六2六7判断(design §8.1:6/7/8 个 7 → 1/2/3 奖,6/7/8 个 2 同理)
if (_7list.length >= 6){
re.count = re.count + _7list.length - 5;
do_addlisttocards(_7list);
}
if (_2list.length >= 6){
re.count = re.count + _2list.length - 5;
re.count = re.count + _2list.length - 5;
do_addlisttocards(_2list);
}
//10个老主判断
var _total = _wlist.length + _7list.length + _2list.length;
if (_total >= 10){
re.count = re.count + _total - 9;
do_addlisttocards(_7list);
do_addlisttocards(_2list);
}
//注:固定主牌 ≥10 张只触发亮牌(§8.2)、不算奖,故此处不再加奖
re.cards = cls_youle_erqiwang_arith.order_cards(mainflower, re.cards);
return re;
},
//计算升级级数
get_upgrade: function(call, grade){
//叫分的一半
var _halfcall;
if (call % 2 == 0){
_halfcall = call / 2;
} else {
_halfcall = (call + 5) / 2;
//计算判定的子数倍率(带符号):大光 +3 / 小光 +2 / 过庄 +1 / 升N级 -N(design §7.2.0)
//q: 小光/过庄分界与升级级距(见 get_qvalue:常规40,爬坡分段)
//先判是否达标(升级),再判大光/小光/过庄,顺序不可颠倒
get_upgrade: function(call, grade, q){
if (grade >= call){
//闲家捡分达到叫分:闲赢升级,级数 = 1 + floor((grade-call)/q)
return -1 * (parseInt((grade - call) / q) + 1);
}
if (grade == 0){
return 4;
return 3; //大光
}
if (grade < _halfcall){
return 2;
if (grade < q){
return 2; //小光
}
if (grade < call){
return 1;
}
return -1 * (parseInt((grade - call) / _halfcall) + 1);
return 1; //过庄
}
}