// §7 算子全量对拍:把 design §7.1/§7.2/§7.3 的判定表**按原文另写一份参考实现**, // 再与 class.arith 的 get_base_bycall / get_qvalue / get_upgrade 逐格比对。 // 参考实现只依据 design 的文字与表格,不看 class.arith 的实现,属差分测试而非同义反复。 // 覆盖:2 种算子模式 × 14 个叫分档 × grade 0~260,比 base / Q / 判定倍率 / 最终子数 X 四项。 require('./_shim'); const A = require('../class.arith.js'); const t = require('./_assert')(); // ============ 参考实现(逐字转写自 design)============ // §7.1 常规算子:65→2 60→3 55→4 50及以下统一6;70 打牌→2 // §7.3.1 爬坡:65 起每低 5 分加 1 个子,50→6 后继续延伸到 5 分→15;70 仍为 2 const CLIMB_BASE = { 65: 2, 60: 3, 55: 4, 50: 6, 45: 7, 40: 8, 35: 9, 30: 10, 25: 11, 20: 12, 15: 13, 10: 14, 5: 15 }; function refBase(climb, call) { if (call === 70) return 2; // §7.1 末尾:70 分打牌基础子数 2 if (!climb) return (call === 65) ? 2 : (call === 60) ? 3 : (call === 55) ? 4 : 6; return CLIMB_BASE[call]; } // §7.2.0 常规算子:小光/过庄分界固定 40,升级级距同为 40 // §7.3.2 爬坡:≥45→40、40/35→20、30/25→15、20/15→10、10→5、5→5;70 分不适用分段,仍 40 function refQ(climb, call) { if (!climb) return 40; if (call === 70 || call >= 45) return 40; if (call === 40 || call === 35) return 20; if (call === 30 || call === 25) return 15; if (call === 20 || call === 15) return 10; return 5; } // §7.2.0 判定顺序:**必须先判是否达标(升级),再判大光/小光/过庄** // 返回带符号倍率:大光 +3 / 小光 +2 / 过庄 +1 / 升 N 级 −N function refUpgrade(call, grade, Q) { if (grade >= call) return -(Math.floor((grade - call) / Q) + 1); if (grade === 0) return 3; if (grade < Q) return 2; return 1; } // ============ 全量对拍 ============ const CALLS = [70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5]; let totalCells = 0; for (const climb of [false, true]) { const tag = climb ? '爬坡' : '常规'; for (const call of CALLS) { const eb = refBase(climb, call), eq = refQ(climb, call); const gb = A.get_base_bycall(call, climb), gq = A.get_qvalue(call, climb); const bad = []; if (gb !== eb) bad.push(['base', gb, eb]); if (gq !== eq) bad.push(['Q', gq, eq]); for (let g = 0; g <= 260; g++) { const exp = refUpgrade(call, g, eq); const got = A.get_upgrade(call, g, gq); totalCells++; if (got !== exp) { bad.push(['判定 grade=' + g, got, exp]); continue; } // §7.0 第3层:X = 基础子数 × |判定倍率| if (gb * Math.abs(got) !== eb * Math.abs(exp)) bad.push(['X grade=' + g, gb * Math.abs(got), eb * Math.abs(exp)]); } t.eq('§7 全量对拍 ' + tag + ' 叫' + call + ' 分(base/Q/grade0~260)失配', bad.slice(0, 3), []); } } t.eq('§7 全量对拍覆盖格数', totalCells, 2 * CALLS.length * 261); // ============ design 判定表里「写死的最终子数」逐格抽查 ============ // [climb, call, grade, 期望最终子数 X, 期望判定(G大光/X小光/P过庄/Un升n级)] const TABLES = { '§7.2.1 常规 65 分坐庄': [[0, 65, 0, 6, 'G'], [0, 65, 1, 4, 'X'], [0, 65, 39, 4, 'X'], [0, 65, 40, 2, 'P'], [0, 65, 64, 2, 'P'], [0, 65, 65, 2, 'U1'], [0, 65, 104, 2, 'U1'], [0, 65, 105, 4, 'U2'], [0, 65, 144, 4, 'U2'], [0, 65, 145, 6, 'U3']], '§7.2.1 常规 60 分坐庄': [[0, 60, 0, 9, 'G'], [0, 60, 39, 6, 'X'], [0, 60, 40, 3, 'P'], [0, 60, 59, 3, 'P'], [0, 60, 60, 3, 'U1'], [0, 60, 99, 3, 'U1'], [0, 60, 100, 6, 'U2'], [0, 60, 139, 6, 'U2'], [0, 60, 140, 9, 'U3']], '§7.2.1 常规 55 分坐庄': [[0, 55, 0, 12, 'G'], [0, 55, 39, 8, 'X'], [0, 55, 40, 4, 'P'], [0, 55, 54, 4, 'P'], [0, 55, 55, 4, 'U1'], [0, 55, 94, 4, 'U1'], [0, 55, 95, 8, 'U2'], [0, 55, 135, 12, 'U3']], '§7.2.1 常规 50 分坐庄': [[0, 50, 0, 18, 'G'], [0, 50, 39, 12, 'X'], [0, 50, 40, 6, 'P'], [0, 50, 49, 6, 'P'], [0, 50, 50, 6, 'U1'], [0, 50, 89, 6, 'U1'], [0, 50, 90, 12, 'U2'], [0, 50, 130, 18, 'U3']], '§7.2.1 常规 45 分坐庄': [[0, 45, 0, 18, 'G'], [0, 45, 39, 12, 'X'], [0, 45, 40, 6, 'P'], [0, 45, 44, 6, 'P'], [0, 45, 45, 6, 'U1'], [0, 45, 84, 6, 'U1'], [0, 45, 85, 12, 'U2'], [0, 45, 125, 18, 'U3']], '§7.2.1 常规 40 分及以下(无过庄档)': [[0, 40, 0, 18, 'G'], [0, 40, 39, 12, 'X'], [0, 40, 40, 6, 'U1'], [0, 40, 79, 6, 'U1'], [0, 40, 80, 12, 'U2'], [0, 10, 0, 18, 'G'], [0, 10, 5, 12, 'X'], [0, 10, 9, 12, 'X'], [0, 10, 10, 6, 'U1'], [0, 10, 49, 6, 'U1'], [0, 10, 50, 12, 'U2']], '§7.2.1 常规 5 分坐庄(无小光)': [[0, 5, 0, 18, 'G'], [0, 5, 5, 6, 'U1'], [0, 5, 44, 6, 'U1'], [0, 5, 45, 12, 'U2'], [0, 5, 85, 18, 'U3']], '§7.2.1 70 分坐庄·打牌': [[0, 70, 0, 6, 'G'], [0, 70, 39, 4, 'X'], [0, 70, 40, 2, 'P'], [0, 70, 69, 2, 'P'], [0, 70, 70, 2, 'U1'], [0, 70, 109, 2, 'U1'], [0, 70, 110, 4, 'U2'], [0, 70, 150, 6, 'U3'], [1, 70, 0, 6, 'G'], [1, 70, 70, 2, 'U1']], '§7.3.3 爬坡 40 分坐庄(Q=20)': [[1, 40, 0, 24, 'G'], [1, 40, 1, 16, 'X'], [1, 40, 19, 16, 'X'], [1, 40, 20, 8, 'P'], [1, 40, 39, 8, 'P'], [1, 40, 40, 8, 'U1'], [1, 40, 59, 8, 'U1'], [1, 40, 60, 16, 'U2'], [1, 40, 79, 16, 'U2'], [1, 40, 80, 24, 'U3']], '§7.3.3 爬坡 35 分坐庄(Q=20)': [[1, 35, 0, 27, 'G'], [1, 35, 19, 18, 'X'], [1, 35, 20, 9, 'P'], [1, 35, 34, 9, 'P'], [1, 35, 35, 9, 'U1'], [1, 35, 54, 9, 'U1'], [1, 35, 55, 18, 'U2'], [1, 35, 75, 27, 'U3']], '§7.3.3 爬坡 30 分坐庄(Q=15)': [[1, 30, 0, 30, 'G'], [1, 30, 14, 20, 'X'], [1, 30, 15, 10, 'P'], [1, 30, 29, 10, 'P'], [1, 30, 30, 10, 'U1'], [1, 30, 44, 10, 'U1'], [1, 30, 45, 20, 'U2'], [1, 30, 60, 30, 'U3']], '§7.3.3 爬坡 25 分坐庄(Q=15)': [[1, 25, 0, 33, 'G'], [1, 25, 14, 22, 'X'], [1, 25, 15, 11, 'P'], [1, 25, 24, 11, 'P'], [1, 25, 25, 11, 'U1'], [1, 25, 39, 11, 'U1'], [1, 25, 40, 22, 'U2'], [1, 25, 55, 33, 'U3']], '§7.3.3 爬坡 20 分坐庄(Q=10)': [[1, 20, 0, 36, 'G'], [1, 20, 9, 24, 'X'], [1, 20, 10, 12, 'P'], [1, 20, 19, 12, 'P'], [1, 20, 20, 12, 'U1'], [1, 20, 29, 12, 'U1'], [1, 20, 30, 24, 'U2'], [1, 20, 40, 36, 'U3']], '§7.3.3 爬坡 15 分坐庄(Q=10)': [[1, 15, 0, 39, 'G'], [1, 15, 9, 26, 'X'], [1, 15, 10, 13, 'P'], [1, 15, 14, 13, 'P'], [1, 15, 15, 13, 'U1'], [1, 15, 24, 13, 'U1'], [1, 15, 25, 26, 'U2'], [1, 15, 35, 39, 'U3']], '§7.3.3 爬坡 10 分坐庄(Q=5,无小光)': [[1, 10, 0, 42, 'G'], [1, 10, 5, 14, 'P'], [1, 10, 9, 14, 'P'], [1, 10, 10, 14, 'U1'], [1, 10, 14, 14, 'U1'], [1, 10, 15, 28, 'U2'], [1, 10, 20, 42, 'U3']], '§7.3.3 爬坡 5 分坐庄(无小光、无过庄)': [[1, 5, 0, 45, 'G'], [1, 5, 5, 15, 'U1'], [1, 5, 9, 15, 'U1'], [1, 5, 10, 30, 'U2'], [1, 5, 15, 45, 'U3']], '§7.3.1/§7.3.3 爬坡 ≥45 档(45 为爬坡独有)': [[1, 45, 0, 21, 'G'], [1, 45, 39, 14, 'X'], [1, 45, 40, 7, 'P'], [1, 45, 44, 7, 'P'], [1, 45, 45, 7, 'U1'], [1, 45, 85, 14, 'U2'], [1, 65, 0, 6, 'G'], [1, 60, 0, 9, 'G'], [1, 55, 0, 12, 'G'], [1, 50, 0, 18, 'G']] }; let cellCount = 0; for (const name in TABLES) { const bad = []; for (const [c, call, grade, expX, expKind] of TABLES[name]) { cellCount++; const base = A.get_base_bycall(call, !!c), q = A.get_qvalue(call, !!c); const up = A.get_upgrade(call, grade, q); const kind = up === 3 ? 'G' : up === 2 ? 'X' : up === 1 ? 'P' : ('U' + (-up)); const X = base * Math.abs(up); if (X !== expX || kind !== expKind) bad.push(['grade=' + grade, X + '/' + kind, expX + '/' + expKind]); } t.eq(name + ' 逐格失配', bad, []); } t.eq('design 判定表抽查格数', cellCount, 141); // ============ 投降 / 解散:不走 7.1~7.3 模型(§7.0 例外、§12.2)============ // 数值由 get_paiju_account 直接固定(投降 base=1、解散 0),此处只钉住 get_base_bycall 对 70 分的取值 t.eq('§7.1 70 分打牌基础子数 2(两种模式一致)', [A.get_base_bycall(70, false), A.get_base_bycall(70, true)], [2, 2]); t.eq('§7.1 无叫分 → 0', [A.get_base_bycall(0, false), A.get_base_bycall(0, true)], [0, 0]); process.exit(t.done('calc') ? 0 : 1);