二七王修复:拖拉机跟牌降级递归 else 分支欠约束(放行非法跟牌)

get_followcard 降规格递归中,当某级"等长短拖拉机候选总对数 > 剩余需求"
时(class.arith.js else 分支),因 cantype 单一结构模型无法表达"任选其一
成拖拉机、其余自由",直接返回宽松的"任意 N 对",导致跟牌方明明有 2 连对
却被允许只出三个孤立对子(如首家 3 连对、跟牌方 AA KK JJ TT 88 66 时打
AA JJ 88),违反 design §5.2"能凑多长拖拉机就必须先凑多长"。

方案 2(根治,不引入过约束):在 can_followcard 结构校验后新增"最大拖拉机
覆盖度"闸门 follow_tractor_cover_ok——
- tractor_run_lengths:把同花色对子按相邻拆成连对 run 长度表;
- max_tractor_profile:贪心最长优先取 K 对,得字典序最大规格档案 best;
- profile_less:跟牌自身档案 got 不得字典序劣于 best,否则拒。
仅在"跟牌整手=首家花色对子且该花色牌≥首家张数"时生效,垫/混合/对子不够
等一律放行。can_followcard 是唯一出牌闸门(paiju.js:605),覆盖人工+托管。
get_followcard else 分支加注释指明由该闸门兜底。

回归测试:test_follow.js +5 条(303/304 三组 2 连对场景,含 else 欠约束
回归:出孤立对→拒、出尽量长拖→合法)。全套 259 项断言通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 07:18:42 +08:00
co-authored by Claude Opus 4.8
parent cab66bea01
commit 21e76b976a
2 changed files with 103 additions and 1 deletions
+12
View File
@@ -70,6 +70,18 @@ t.eq('拖 3连对·有3连拖必出', res(f([K(1), K(2), Q(1), Q(2), J(1), J(2),
t.eq('拖 3连对·富余对子中出3连拖', res(f([K(1), K(2), Q(1), Q(2), J(1), J(2), N9(1), N9(2)], [K(1), K(2), Q(1), Q(2), J(1), J(2)], 6, 2, 303)), true);
t.eq('拖 3连对·拆散3连拖(改出99)→拒', res(f([K(1), K(2), Q(1), Q(2), J(1), J(2), N9(1), N9(2)], [K(1), K(2), Q(1), Q(2), N9(1), N9(2)], 6, 2, 303)), false);
// —— 最大覆盖度校验:修复 get_followcard 降级递归 else 分支欠约束 ——
// 跟牌方该副花色 = AA KK JJ TT 88 66(三组互不相邻的2连对:{A,K} {J,T} {8,6})
const sA = d => id(d, 2, 1); // 副A(flower2, number1;非固定主)
const H3 = [sA(1), sA(2), K(1), K(2), J(1), J(2), T(1), T(2), N8(1), N8(2), N6(1), N6(2)];
// 303:无3连拖、有多组2连拖 → 必出1个2连拖+1散对(档案[2,1]);出三孤立对(档案[1,1,1])应拒
t.eq('拖 303·三组2连·出2连+散(合法)', res(f(H3, [sA(1), sA(2), K(1), K(2), N8(1), N8(2)], 6, 2, 303)), true);
t.eq('拖 303·三组2连·出三孤立对AA/JJ/88→拒(else回归)', res(f(H3, [sA(1), sA(2), J(1), J(2), N8(1), N8(2)], 6, 2, 303)), false);
t.eq('拖 303·三组2连·另组KK/TT/66孤立→拒', res(f(H3, [K(1), K(2), T(1), T(2), N6(1), N6(2)], 6, 2, 303)), false);
// 304:无4连拖、有三组2连拖 → 必出两个2连拖(档案[2,2]);出1个2连+两散对(档案[2,1,1])应拒
t.eq('拖 304·三组2连·出两2连拖(合法)', res(f(H3, [sA(1), sA(2), K(1), K(2), J(1), J(2), T(1), T(2)], 8, 2, 304)), true);
t.eq('拖 304·三组2连·出1个2连+两散对→拒(else回归)', res(f(H3, [sA(1), sA(2), K(1), K(2), J(1), J(2), N8(1), N8(2)], 8, 2, 304)), false);
// ===================== 手牌数 == 首出张数:全部必出 =====================
t.eq('全出 手牌恰1张(单张首出)', res(f([oth(9)], [oth(9)], 1, 2, 101)), true);
t.eq('全出 手牌恰2张(对子首出)', res(f([oth(9), oth(8)], [oth(9), oth(8)], 2, 2, 201)), true);