Files
erqiwang_youle/server/games/erqiwang/shared/cards.js
T
joywayerandClaude Opus 5 b81be36f6e 二七王:拖拉机极大连续段扫描与牌编码区间常量收敛进 shared
前后端此前各写了一遍「分对子 → is_continuous 扫极大连续段 → ≥2 对算拖拉机」:
服务端 class.arith.js decompose_trump 与前端 core/CardMark.js。规则内容(什么算连续、
几对起算拖拉机)散在两处,正是 shared/ 要消除的重复。

- shared/cards.js 新增 group_tractor_runs(mainflower, pairlist):返回极大连续段数组,
  孤立对子作为长度 1 的段返回,拼接后恰好还原入参;阈值导出为 TRACTOR_MIN_PAIRS。
- shared/cards.js 导出牌编码区间边界具名常量(CODE_ZHU_MIN / CODE_ZHENG2_* /
  CODE_FU2_* / CODE_ZHENG7_* / CODE_FU7_* / CODE_XIAOWANG / CODE_DAWANG /
  CODE_A_IN_ZHU),并在 id_to_code / is_continuous / trump_rank 内部改用它们。
- decompose_trump 改调共享扫描,对外行为完全不变(分量类型、顺序、cards 归属不变)。
- CardMark 删掉本地扫描改调共享版本,区间判定与标记值改引用常量,去掉 1000/3000/
  4000/8000/9000 裸字面量。
- 前端副本经 sync_shared.cmd 同步;服务端单测 624→645 checks 全绿,前端 8 个测试全绿。

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

305 lines
10 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.
///////////////////////////////////////////////////////////////
////// youle_erqiwang_shared_cards: 前后端共享牌值算法 //////////
///////////////////////////////////////////////////////////////
// 【权威源】本文件是前后端共享算法的唯一可修改处。
// 前端副本 client/js/01_SubGame/codes/shared/cards.js 由仓库根目录的
// sync_shared.cmd 单向同步生成,为只读;任何改动都必须落在本文件,
// 改完跑一次 sync_shared.cmd,否则 client/tests/test_shared_sync.js 会红。
//
// 内容:牌值编码与牌型分组的纯函数,不依赖对局状态、不依赖平台 API。
// 牌编码区间约定(id_to_code 的产物,各处判定据此):
// 9554 大王 / 9553 小王 / 8000–9000 正7 / 7000–8000 副7
// 3000–4000 正2 / 2000–3000 副2 / 1000–2000 主花色普通牌 / 100–500 副牌普通牌
// ⇒ 主牌 ⟺ code > 1000
var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || {
//——— 牌编码区间边界:上面那张区间表的机器可读形式,是【全部区间判定的唯一权威来源】———
//一律是【开区间】:CODE_ZHENG7_MIN < code < CODE_ZHENG7_MAX ⟺ 正7,其余同理。
//本文件内部(is_continuous / trump_rank / id_to_code)与前端 core/CardMark.js 都引用这些常量,
//不再各处重抄 3000/4000/8000/9000 之类裸字面量(工程总则 §1 SSOT / §6 配置优先于硬编码)。
CODE_XIAOWANG: 9553, //小王(单值,非区间)
CODE_DAWANG: 9554, //大王(单值,非区间)
CODE_ZHENG7_MIN: 8000, //正7:8000 < code < 9000
CODE_ZHENG7_MAX: 9000,
CODE_FU7_MIN: 7000, //副7:7000 < code < 8000
CODE_FU7_MAX: 8000,
CODE_ZHENG2_MIN: 3000, //正2:3000 < code < 4000
CODE_ZHENG2_MAX: 4000,
CODE_FU2_MIN: 2000, //副2:2000 < code < 3000
CODE_FU2_MAX: 3000,
CODE_ZHU_MIN: 1000, //主牌 ⟺ code > CODE_ZHU_MIN;主花色普通牌:1000 < code < 2000
CODE_ZHU_MAX: 2000,
CODE_A_IN_ZHU: 14, //主A 的 code % 100(A 在 id_to_code 里 +13 提到 14)
//几对连续对子起算拖拉机(design §5.3)。group_tractor_runs 返回极大连续段,
//段长 >= 本值即拖拉机——服务端分解甩牌与前端手牌标记共用这一个阈值
TRACTOR_MIN_PAIRS: 2,
//牌id转花色
id_to_flower: function(cardid){
if (cardid == 52 || cardid == 106){
//小王
return 5;
}
if (cardid == 53 || cardid == 107){
//大王
return 5;
}
return parseInt((cardid % 54) / 13) + 1;
},
//牌id转牌面数值
id_to_number: function(cardid){
if (cardid == 52 || cardid == 106){
//小王
return 53;
}
if (cardid == 53 || cardid == 107){
//大王
return 54;
}
return (cardid % 54) % 13 + 1;
},
//牌id转牌编码
id_to_code: function(mainflower, cardid){
if (cardid == 52 || cardid == 106){
//小王
return youle_erqiwang_shared_cards.CODE_XIAOWANG;
}
if (cardid == 53 || cardid == 107){
//大王
return youle_erqiwang_shared_cards.CODE_DAWANG;
}
var flower = youle_erqiwang_shared_cards.id_to_flower(cardid);
var number = youle_erqiwang_shared_cards.id_to_number(cardid);
var code = flower * 100 + number;
//特殊处理
switch (number)
{
case 1:
//A 提到 K 之上(14)
code = code + 13;
break;
case 2:
//2 提到副2 段
code = code + youle_erqiwang_shared_cards.CODE_FU2_MIN;
break;
case 7:
//7 提到副7 段
code = code + youle_erqiwang_shared_cards.CODE_FU7_MIN;
break;
}
//主牌花色处理:整体再上移一个主牌段(副2→正2、副7→正7、普通牌→主花色普通牌)
if (flower == mainflower){
code = code + youle_erqiwang_shared_cards.CODE_ZHU_MIN;
}
return code;
},
//从大到小排序(冒泡排序)
order_cards: function(mainflower, aryCardIDs){
if (aryCardIDs){
for (var i = 0; i < aryCardIDs.length; i++) {
for (var j = i + 1; j < aryCardIDs.length; j++) {
var i_code = youle_erqiwang_shared_cards.id_to_code(mainflower, aryCardIDs[i]);
var j_code = youle_erqiwang_shared_cards.id_to_code(mainflower, aryCardIDs[j]);
if (i_code < j_code) {
var tmp = aryCardIDs[j];
aryCardIDs[j] = aryCardIDs[i];
aryCardIDs[i] = tmp;
}
}
}
}
return aryCardIDs;
},
//判断两个牌是否是连续的
is_continuous: function(bigcode, smallcode){
var S = youle_erqiwang_shared_cards;
//是小王
var is_xiaowang = function(_code){
if (_code == S.CODE_XIAOWANG){
return true;
}
return false;
}
//是正7
var is_zheng7 = function(_code){
if (_code > S.CODE_ZHENG7_MIN && _code < S.CODE_ZHENG7_MAX){
return true;
}
return false;
}
//是负7
var is_fu7 = function(_code){
if (_code > S.CODE_FU7_MIN && _code < S.CODE_FU7_MAX){
return true;
}
return false;
}
//是正2
var is_zheng2 = function(_code){
if (_code > S.CODE_ZHENG2_MIN && _code < S.CODE_ZHENG2_MAX){
return true;
}
return false;
}
//是负2
var is_fu2 = function(_code){
if (_code > S.CODE_FU2_MIN && _code < S.CODE_FU2_MAX){
return true;
}
return false;
}
//是主A
var is_zhuA = function(_code){
if (_code > S.CODE_ZHU_MIN && _code < S.CODE_ZHU_MAX && _code % 100 == S.CODE_A_IN_ZHU){
return true;
}
return false;
}
//是8
var is_8 = function(_code){
if (_code % 100 == 8){
return true;
}
return false;
}
//是6
var is_6 = function(_code){
if (_code % 100 == 6){
return true;
}
return false;
}
//普通的连牌
if (bigcode - smallcode == 1){
return true;
}
//小王、正7
if (is_xiaowang(bigcode) && is_zheng7(smallcode)){
return true;
}
//正7、负7
if (is_zheng7(bigcode) && is_fu7(smallcode)){
return true;
}
//负7、正2
if (is_fu7(bigcode) && is_zheng2(smallcode)){
return true;
}
//正2、负2
if (is_zheng2(bigcode) && is_fu2(smallcode)){
return true;
}
//负2、主A
if (is_fu2(bigcode) && is_zhuA(smallcode)){
return true;
}
//8、6(design §5.3:7 不与 6/8 相连,6 与 8 相连)
//必须是同一花色的 8 与 6:牌编码同花色相差 2(主花色 1408-1406=2、副牌 308-306=2),
//跨花色则相差 100 的倍数。只判 %100 会把「♥8 + ♣6」误判为连对,
//进而在 get_chongguan 扫描整手牌时让副牌的 6 对错误接续主 8、多算奖(design §8.1 连对链)
if (is_8(bigcode) && is_6(smallcode) && (bigcode - smallcode == 2)){
return true;
}
return false;
},
//获取对子列表,cards为同一花色的排过序的牌列表
get_pairlist: function(mainflower, flowercards){
var pairlist = [];
var i = 0;
while (i < flowercards.length - 1){
//牌编码,两张牌两张牌一取
var code_i = youle_erqiwang_shared_cards.id_to_code(mainflower, flowercards[i]);
var code_j = youle_erqiwang_shared_cards.id_to_code(mainflower, flowercards[i + 1]);
if (code_i == code_j){
pairlist.push([flowercards[i], flowercards[i + 1]]);
i = i + 2;
} else {
i++;
}
}
return pairlist;
},
//把对子列表扫成【极大连续段】。pairlist 为同一牌组内、已按牌编码从大到小排好的对子列表
//(形如 get_pairlist 的返回值:[[cardid, cardid], ...])。
//返回 [[pair, ...], ...]:每一段是若干【相邻且 is_continuous】的对子,段与段之间必不连续;
//孤立对子也作为长度 1 的段返回,故 concat 所有段 == 原 pairlist(顺序不变)。
//段长 >= TRACTOR_MIN_PAIRS 即拖拉机。
//【唯一实现】服务端 decompose_trump(甩牌分解)与前端 core/CardMark.js(手牌「拖」标记)
//此前各写过一遍同样的扫描,规则内容(什么算连续、几对起算拖拉机)散在两处——现收敛到这里。
group_tractor_runs: function(mainflower, pairlist){
var runs = [];
var i = 0;
while (i < pairlist.length){
var run = [pairlist[i]];
var k = i;
while (k + 1 < pairlist.length){
var code_k = youle_erqiwang_shared_cards.id_to_code(mainflower, pairlist[k][0]);
var code_k1 = youle_erqiwang_shared_cards.id_to_code(mainflower, pairlist[k + 1][0]);
if (!youle_erqiwang_shared_cards.is_continuous(code_k, code_k1)){
break;
}
run.push(pairlist[k + 1]);
k++;
}
runs.push(run);
i = k + 1;
}
return runs;
},
//获取拖拉机列表,pairlist为同一花色的排过序的对子列表,cardtype为拖拉机牌型
get_tuolaji_list: function(mainflower, pairlist, cardtype){
var tuolaji = [];
var count = cardtype - 300; //拖拉机的对子数量
var i = 0;
for (var i = 0; i < pairlist.length - count + 1; i++){
//每次按拖拉机的对子数量取N对
var tmplist = pairlist.slice(i, i + count);
//判断是否是cardtype类型的拖拉机
var is_tlj = true;
for (var j = 0; j < tmplist.length - 1; j++) {
var code_j1 = youle_erqiwang_shared_cards.id_to_code(mainflower, tmplist[j][0]);
var code_j2 = youle_erqiwang_shared_cards.id_to_code(mainflower, tmplist[j+1][0]);
if (!youle_erqiwang_shared_cards.is_continuous(code_j1, code_j2)){
is_tlj = false;
break;
}
}
if (is_tlj){
var tlj = [];
for (var k = 0; k < tmplist.length; k++){
tlj = tlj.concat(tmplist[k]);
}
tuolaji.push(tlj);
}
}
return tuolaji;
},
//主牌归一化大小(副7 同级取 7000、副2 同级取 2000,其余用牌编码),仅用于甩牌分量比大小
trump_rank: function(mainflower, cardid){
var code = youle_erqiwang_shared_cards.id_to_code(mainflower, cardid);
if (code > youle_erqiwang_shared_cards.CODE_FU7_MIN && code < youle_erqiwang_shared_cards.CODE_FU7_MAX){ //副7
return youle_erqiwang_shared_cards.CODE_FU7_MIN;
}
if (code > youle_erqiwang_shared_cards.CODE_FU2_MIN && code < youle_erqiwang_shared_cards.CODE_FU2_MAX){ //副2
return youle_erqiwang_shared_cards.CODE_FU2_MIN;
}
return code;
},
}
//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)
if (typeof module !== "undefined"){
module.exports = youle_erqiwang_shared_cards;
}