二七王:拖拉机极大连续段扫描与牌编码区间常量收敛进 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>
This commit is contained in:
@@ -85,11 +85,52 @@ t.eq('算奖 10固定主无对无链', cg([big(1), small(1), small(2), id(1, 1,
|
||||
t.eq('算奖 连对链到主A对', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 1, 1), id(2, 1, 1)], 'count'), 6);
|
||||
t.eq('算奖 副花色A对不续链', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 2, 1), id(2, 2, 1)], 'count'), 5);
|
||||
|
||||
// ============ shared/cards.js:牌编码区间常量 + 极大连续段扫描 ============
|
||||
const S = require('../shared/cards.js');
|
||||
|
||||
// 常量必须与 id_to_code 的实际产物一致(常量是区间判定的 SSOT,写歪了这里就红)
|
||||
const inOpen = (code, lo, hi) => code > lo && code < hi;
|
||||
t.eq('常量 正7 区间', inOpen(S.id_to_code(MF, id(1, 1, 7)), S.CODE_ZHENG7_MIN, S.CODE_ZHENG7_MAX), true);
|
||||
t.eq('常量 副7 区间', inOpen(S.id_to_code(MF, id(1, 2, 7)), S.CODE_FU7_MIN, S.CODE_FU7_MAX), true);
|
||||
t.eq('常量 正2 区间', inOpen(S.id_to_code(MF, id(1, 1, 2)), S.CODE_ZHENG2_MIN, S.CODE_ZHENG2_MAX), true);
|
||||
t.eq('常量 副2 区间', inOpen(S.id_to_code(MF, id(1, 2, 2)), S.CODE_FU2_MIN, S.CODE_FU2_MAX), true);
|
||||
t.eq('常量 主花色普通牌区间', inOpen(S.id_to_code(MF, id(1, 1, 5)), S.CODE_ZHU_MIN, S.CODE_ZHU_MAX), true);
|
||||
t.eq('常量 主牌判据 code>CODE_ZHU_MIN 对主花色牌成立', S.id_to_code(MF, id(1, 1, 5)) > S.CODE_ZHU_MIN, true);
|
||||
t.eq('常量 主牌判据 对副牌不成立', S.id_to_code(MF, id(1, 2, 5)) > S.CODE_ZHU_MIN, false);
|
||||
t.eq('常量 小王编码', S.id_to_code(MF, small(1)), S.CODE_XIAOWANG);
|
||||
t.eq('常量 大王编码', S.id_to_code(MF, big(1)), S.CODE_DAWANG);
|
||||
t.eq('常量 主A 的 code%100', S.id_to_code(MF, id(1, 1, 1)) % 100, S.CODE_A_IN_ZHU);
|
||||
t.eq('常量 拖拉机起算对数', S.TRACTOR_MIN_PAIRS, 2);
|
||||
|
||||
// group_tractor_runs:极大连续段(服务端 decompose_trump 与前端手牌「拖」标记共用)
|
||||
const pr = (n) => [id(1, 1, n), id(2, 1, n)]; // 主花色 n 的对子
|
||||
const runLens = (pairs) => S.group_tractor_runs(MF, pairs).map(r => r.length);
|
||||
t.eq('连续段 空表', S.group_tractor_runs(MF, []), []);
|
||||
t.eq('连续段 单个对子是长度1的段', runLens([pr(13)]), [1]);
|
||||
t.eq('连续段 KQ 连续合成一段', runLens([pr(13), pr(12)]), [2]);
|
||||
t.eq('连续段 KQJ 三连对是一段(极大)', runLens([pr(13), pr(12), pr(11)]), [3]);
|
||||
t.eq('连续段 K 与 9 不连 → 两段', runLens([pr(13), pr(9)]), [1, 1]);
|
||||
t.eq('连续段 KQ + 9 → 2+1', runLens([pr(13), pr(12), pr(9)]), [2, 1]);
|
||||
t.eq('连续段 KQ + 98 → 2+2', runLens([pr(13), pr(12), pr(9), pr(8)]), [2, 2]);
|
||||
// 8-6 相连(design §5.3:7 不与 6/8 相连),且必须同花色
|
||||
t.eq('连续段 主8-主6 相连', runLens([pr(8), pr(6)]), [2]);
|
||||
t.eq('连续段 跨花色 主8+副6 不相连', runLens([pr(8), [id(1, 2, 6), id(2, 2, 6)]]), [1, 1]);
|
||||
// 段的拼接必须恰好还原入参(不丢对子、不改顺序)
|
||||
const runsIn = [pr(13), pr(12), pr(9)];
|
||||
t.eq('连续段 拼接还原入参', [].concat.apply([], S.group_tractor_runs(MF, runsIn)), runsIn);
|
||||
// arith 上挂的引用与 shared 同一实现
|
||||
t.eq('arith 挂了 group_tractor_runs 引用', A.group_tractor_runs === S.group_tractor_runs, true);
|
||||
|
||||
// ============ §5.4 甩牌 decompose / 最大性 / can_playcard ============
|
||||
const types = cards => A.decompose_trump(MF, cards).map(c => c.type + (c.len > 1 ? c.len : '')).sort().join(',');
|
||||
t.eq('分解 大王对+主K单', types([big(1), big(2), id(1, 1, 13)]), 'pair,single');
|
||||
t.eq('分解 主K主Q两连对', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)]), 'tractor2');
|
||||
t.eq('分解 主K对+主9对(不连)', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 9), id(2, 1, 9)]), 'pair,pair');
|
||||
// 拖拉机段与孤立对子、单张混排:分量顺序与 cards 归属都不变(改走 shared 扫描后的行为一致性)
|
||||
const mixComp = A.decompose_trump(MF, [id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 9), id(2, 1, 9), id(1, 1, 5)]);
|
||||
t.eq('分解 KQ拖+9对+5单 的分量序', mixComp.map(c => c.type + '/' + c.len), ['tractor/2', 'pair/1', 'single/1']);
|
||||
t.eq('分解 拖拉机分量含4张', mixComp[0].cards.length, 4);
|
||||
t.eq('分解 对子分量含2张', mixComp[1].cards.length, 2);
|
||||
const play = (cards, opps) => A.can_playcard(MF, cards, 0, null, opps);
|
||||
const K1 = id(1, 1, 13), K2 = id(2, 1, 13), A1 = id(1, 1, 1), P5 = id(1, 1, 5), P6 = id(1, 1, 6), fK1 = id(1, 2, 13), f9 = id(1, 2, 9), fK2 = id(2, 2, 13), Q1 = id(1, 1, 12), Q2 = id(2, 1, 12);
|
||||
t.eq('甩牌 合法(大王对+主K单)', play([big(1), big(2), K1], [[P5, P6], []]).result, true);
|
||||
|
||||
Reference in New Issue
Block a user