二七王:补 9 处 design 规则的正/反/边界用例(均通过变异检验)
对 design §1~§12 重做「每条规则是否三类用例齐全」的审计。此前的覆盖矩阵按 「被测函数」组织,容易漏掉跨函数才体现的规则,本轮补齐 9 处: - G1/G2 §4.2 叫分:下限5与非首家不叫接受,负数/缺call/步进外/同分拒; 「不叫即退出本局叫分」——反悔再叫被 SEAT 拒、callproc 不变、仍轮到下一家 - G3 §4.5 埋牌:非庄家 SEAT、step2/step5 STEP、7张/9张/空数组 PARAM - G4 §5.1 每轮由上一轮牌面最大的一方先出(闲1赢/闲2赢两种) - G5 §6.2 闲家赢计入台面全部分牌(含庄家自己打出的)、庄家赢作废、 两个闲家谁赢结果一致 - G6 §8 算奖快照:庄家埋后28(已埋不在内)、闲家28、投降36、打出后不缩水 - G7 §3 对子只认同花色同点数两副:跨花色副7/副2、正副7、大小王均不成对 - G8 §6.3 赢末轮的牌不全是主牌就不扣底(副牌拖拉机/混合出牌/顺序颠倒) - G9 §11 无超时托管守卫:对局阶段 min_ontimeout 调用次数必须为 0, 将来有人加超时代打就会转红 每条都在副本上注入违反该规则的改动确认转红(7 组变异全部被抓住)。 顺带修一个测试自身的问题:mkBury 桩缺 do_burycard,导致「埋牌张数校验被 改松」这类变异表现为整个文件崩溃而非某条断言转红,后续断言全不执行。桩已 补完整到能走完成功路径。 全套单测 462 → 511 项全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,7 +147,7 @@
|
|||||||
|
|
||||||
## 5. 当前状态小结 —— ✅ DoD 达成,P0/P1/P2 全部完成
|
## 5. 当前状态小结 —— ✅ DoD 达成,P0/P1/P2 全部完成
|
||||||
|
|
||||||
**共 361 项断言全绿**(`test_arith` 106 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_follow` 50 / `test_input` 34 / `test_paiju` 31 / `test_rpc` 68 / `test_success` 29)。
|
**共 511 项断言全绿**(`test_arith` 122 / `test_calc` 49 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_endgame` 25 / `test_follow` 58 / `test_followdiff` 12 / `test_input` 34 / `test_paiju` 50 / `test_rpc` 89 / `test_success` 29)。其中 `test_calc`/`test_followdiff` 见 §7、G1~G9 见 §8。
|
||||||
|
|
||||||
design 全部服务端可验证章节均有正/反/边界单测:
|
design 全部服务端可验证章节均有正/反/边界单测:
|
||||||
- **§2 构成**:`test_deal` 92张/去3-4/三家28+底8/分值。
|
- **§2 构成**:`test_deal` 92张/去3-4/三家28+底8/分值。
|
||||||
@@ -182,6 +182,55 @@ design 全部服务端可验证章节均有正/反/边界单测:
|
|||||||
| §9 出牌历史按查牌模式门控 | `get_deskinfo` | 反:不查牌房无 `pushlist`;正:可查牌房有 `pushlist`;边界:两种模式下当前轮桌面牌 `playproc.cards` 都必须恢复 | `test_rpc` | ✅ 4 |
|
| §9 出牌历史按查牌模式门控 | `get_deskinfo` | 反:不查牌房无 `pushlist`;正:可查牌房有 `pushlist`;边界:两种模式下当前轮桌面牌 `playproc.cards` 都必须恢复 | `test_rpc` | ✅ 4 |
|
||||||
| §3 相邻链花色 | `is_continuous` | 正:同花色 8-6 连;反:三组跨花色 8-6 不连;反:差 2 但非 8/6 不连 | `test_arith` | ✅ 5 |
|
| §3 相邻链花色 | `is_continuous` | 正:同花色 8-6 连;反:三组跨花色 8-6 不连;反:差 2 但非 8/6 不连 | `test_arith` | ✅ 5 |
|
||||||
|
|
||||||
**关于超时**:`countdown` 只作展示、服务端不做任何超时动作,这是 design §11 确认的规则(并非缺口),因此**没有也不需要「超时自动操作」的用例**;反过来,若将来有人给服务端加了超时代打,那才是违反 design §11,应由核对拦下。
|
**关于超时**:`countdown` 只作展示、服务端不做任何超时动作,这是 design §11 确认的规则(并非缺口),因此**没有也不需要「超时自动操作」的用例**;反过来,若将来有人给服务端加了超时代打,那才是违反 design §11——现已由 `test_rpc` 的**守卫用例**拦下(见 §8 G9)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 第五轮:按 design 原文另写参考实现的差分测试
|
||||||
|
|
||||||
|
> 前六节的用例都是**手写的场景枚举**——覆盖的是"我想到的情况"。第五轮补两组**差分测试**:把 design 的规则整条转写成参考实现,与代码大面积对拍,覆盖手写用例想不到的组合。
|
||||||
|
|
||||||
|
| 文件 | 覆盖 | 规模 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `test_calc.js` | §7 算子全量对拍:参考实现逐字转写自 §7.1/§7.2/§7.3 判定表,比 base / Q / 判定倍率 / 最终子数 | 2 模式 × 14 档 × grade 0~260 = **7308 格**,另按 design 的 17 张表逐格抽查 **141 个写死的最终子数** |
|
||||||
|
| `test_followdiff.js` | §5.2 跟牌强制层级穷举差分:参考裁定按原文写,对每手牌**枚举全部 C 张出牌组合**逐个对拍 | 约 **11 万组** |
|
||||||
|
|
||||||
|
**差分测试的强度取决于造牌器,不取决于对拍逻辑。** 第一版 `test_followdiff` 用纯随机抽牌跑 7.8 万组全绿,但把 `follow_tractor_cover_ok` 整个关掉**依然全绿**——因为唯一能区分"只出零散对子"与"先凑最长拖拉机"的形状是「同花色三组互不相邻的两连对」(12 张),随机抽根本抽不出来。故:
|
||||||
|
|
||||||
|
- 随机源必须是**固定种子 PRNG**(不用 `Math.random`),保证用例集可复现;
|
||||||
|
- 关键结构必须用**确定性手牌**钉死(`structHands()`),不能指望随机覆盖到;
|
||||||
|
- 注意别让掺入的其他花色把手牌撑过枚举上限而被**静默跳过**(踩过一次)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 第五轮:逐规则的正/反/边界补全
|
||||||
|
|
||||||
|
> 对 design §1~§12 重新做了一次"每条规则是否三类用例齐全"的审计。覆盖面本来就广,但仍有 9 处 design 明文规则缺用例(多是**规则本身**而非入参校验,前几轮的矩阵按"被测函数"组织,容易漏掉这类"跨函数才体现"的规则)。
|
||||||
|
|
||||||
|
| # | design | 规则 | 补的用例(正/反/边界) | 文件 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| G1 | §4.2 | 暂定庄家必须叫、其后才可"更低或不叫" | 边界:下限 5 接受;正:非首家 `call=0` 接受;反:负数 / 缺 `call`(NaN) / 步进外小值 1 / 与当前叫分相同 均拒 | `test_rpc` |
|
||||||
|
| G2 | §4.2 | **"不叫"即退出本局叫分,轮次跳过他** | 正:首家叫 65 后轮到 seat1;正:seat1 不叫后跳到 seat2;反:seat1 反悔再叫 → SEAT 拒、`callproc` 不变、仍轮到 seat2、未产生庄家 | `test_rpc` |
|
||||||
|
| G3 | §4.5 | 埋牌仅庄家、仅埋牌阶段、恰 8 张 | 反:非庄家 → SEAT;反:step2/step5 埋 → STEP;边界:7 张 / 9 张 / 空数组 → PARAM | `test_rpc` |
|
||||||
|
| G4 | §5.1 | **每轮由上一轮牌面最大的一方先出** | 正:闲 1 赢 → 下一轮 `start/currseat` 都是 1;边界:闲 2 赢 → 改由闲 2 先出 | `test_paiju` |
|
||||||
|
| G5 | §6.2 | 闲家赢 → 台面**全部**分牌计入;庄家赢 → 分牌作废 | 反:庄赢 → 下发面无 `grade`、闲家捡分 0;正:闲 1 赢 → 15 分(**含庄家自己打出的 ♥5**);边界:闲 2 赢结果与闲 1 赢一致("两个闲家谁大不影响") | `test_paiju` |
|
||||||
|
| G6 | §8 | 算奖快照的时间点与静态性 | 正:庄家 = 埋牌后 28 张、已埋 8 张不在内;正:闲家 = 发牌后 28 张;边界:投降局庄家 = 发牌+暗牌 36 张;边界:之后打出 10 张,快照仍是同样的 28 张 | `test_paiju` |
|
||||||
|
| G7 | §3 | **对子只认同花色同点数的两副** | 正:同花色两副 ♥7/♥2/大王成对;反:♥7+♣7、♥2+♣2、正7+副7、正2+副2、大王+小王 均不成对;边界:四张跨花色 7/2 组不出拖拉机,而正7对+副7对可以 | `test_arith` |
|
||||||
|
| G8 | §6.3 | 赢末轮的牌不全是主牌就不扣底 | 反:副牌两连对赢 → 0;反:混合出牌(主K+副K)→ 0;边界:主副提交顺序颠倒仍判 0 | `test_arith` |
|
||||||
|
| G9 | §11 | **无超时托管**(守卫) | 守卫:叫分→选主→埋牌→出牌全程 `min_ontimeout` 调用次数为 0。将来若有人加"超时自动出牌/代打",必然要注册定时器,这条即转红 | `test_rpc` |
|
||||||
|
|
||||||
|
**每条都做了变异检验**(在副本上注入违反该规则的改动,确认对应断言转红):
|
||||||
|
|
||||||
|
| 注入的缺陷 | 转红的断言 |
|
||||||
|
| --- | --- |
|
||||||
|
| 去掉 `mod.jiaofen` 的"非当前叫分位"校验 | G2 三条 |
|
||||||
|
| 埋牌张数 `!= 8` 放宽为 `< 1` | G3 两条边界 |
|
||||||
|
| §6.2 改成庄家赢也累计闲家捡分 | G5 一条 |
|
||||||
|
| 下一轮改为固定庄家先出 | G4 两条 |
|
||||||
|
| 算奖快照不排除已埋的牌 | G6 两条 + 亮牌快照一条 |
|
||||||
|
| `get_pairlist` 放宽为"只看点数" | G7 五条 |
|
||||||
|
| 在 `mod.chupai` 里注册一个定时器 | G9 一条 |
|
||||||
|
|
||||||
|
> 过程中的一个教训:M2(埋牌张数)第一次跑出来是**整个文件崩溃**而不是干净的 FAIL——因为 `mkBury` 桩缺 `do_burycard`,校验一被改松就走到成功路径抛异常,后面的断言全不执行。**桩要完整到能走完成功路径**,否则"校验被改松"这类变异只会表现为崩溃,定位与信号都变差。已修。
|
||||||
|
|
||||||
> 原列在此处的两项「待外部输入」均已由规则设计者拍板:「不查牌模式是否屏蔽出牌历史」确认屏蔽,已改 design §9 + 代码 + 用例(见上表);「超时托管默认动作」确认维持现状、不做超时动作,已写入 design §11。至此测试计划无待外部输入项。
|
> 原列在此处的两项「待外部输入」均已由规则设计者拍板:「不查牌模式是否屏蔽出牌历史」确认屏蔽,已改 design §9 + 代码 + 用例(见上表);「超时托管默认动作」确认维持现状、不做超时动作,已写入 design §11。至此测试计划无待外部输入项。
|
||||||
|
|||||||
@@ -171,6 +171,32 @@ t.eq('相邻 副♠8-副♦6 跨花色 不连', cont(id(1, 4, 8), id(1, 1, 6)),
|
|||||||
// 顺带确认非 8/6 的等差2组合仍不连(防守卫写宽)
|
// 顺带确认非 8/6 的等差2组合仍不连(防守卫写宽)
|
||||||
t.eq('相邻 主A-主Q 不连(差2但非8/6)', cont(zA, id(1, 1, 12)), false);
|
t.eq('相邻 主A-主Q 不连(差2但非8/6)', cont(zA, id(1, 1, 12)), false);
|
||||||
|
|
||||||
|
// ============ §3「关于对子的判定」:对子只认同一张具体牌的两副 ============
|
||||||
|
// design §3 注:正/副7、正/副2 虽各占一个大小等级,但**成对只认同花色同点数的两副**;
|
||||||
|
// 两张不同花色的副7(如♠7+♥7)不构成一对,副2 同理;大王与小王点数不同也不能混成一对。
|
||||||
|
// 拖拉机的连对判定以此为基础(每节连对里的每个对子都须是同花色同点数两副)。
|
||||||
|
const pl = cards => A.get_pairlist(MF, A.order_cards(MF, cards.slice())).length;
|
||||||
|
t.eq('对子 正 同花色两副♥7 成对', pl([id(1, 3, 7), id(2, 3, 7)]), 1);
|
||||||
|
t.eq('对子 反 ♥7+♣7 跨花色副7 不成对', pl([id(1, 3, 7), id(1, 2, 7)]), 0);
|
||||||
|
t.eq('对子 反 正7+副7 不成对', pl([id(1, 1, 7), id(1, 2, 7)]), 0);
|
||||||
|
t.eq('对子 正 同花色两副♥2 成对', pl([id(1, 3, 2), id(2, 3, 2)]), 1);
|
||||||
|
t.eq('对子 反 ♥2+♣2 跨花色副2 不成对', pl([id(1, 3, 2), id(1, 2, 2)]), 0);
|
||||||
|
t.eq('对子 反 正2+副2 不成对', pl([id(1, 1, 2), id(1, 2, 2)]), 0);
|
||||||
|
t.eq('对子 正 大王两副 成对', pl([big(1), big(2)]), 1);
|
||||||
|
t.eq('对子 反 大王+小王 不成对', pl([big(1), small(1)]), 0);
|
||||||
|
// 连对以「同花色同点数两副」为基础:四张跨花色的副7/副2 一对都凑不出,自然也没有拖拉机
|
||||||
|
t.eq('对子 边界 ♥7♣7♥2♣2 四张 → 0 对', pl([id(1, 3, 7), id(1, 2, 7), id(1, 3, 2), id(1, 2, 2)]), 0);
|
||||||
|
t.eq('连对 边界 上述四张组不出拖拉机',
|
||||||
|
A.get_tuolaji_list(MF, A.get_pairlist(MF, A.order_cards(MF, [id(1, 3, 7), id(1, 2, 7), id(1, 3, 2), id(1, 2, 2)])), 302).length, 0);
|
||||||
|
// 对照:正7对 + 副7对(各自同花色两副)才是合法两连对
|
||||||
|
t.eq('连对 正 正7对+副7对 构成两连对',
|
||||||
|
A.get_tuolaji_list(MF, A.get_pairlist(MF, A.order_cards(MF, [id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7)])), 302).length, 1);
|
||||||
|
|
||||||
|
// ============ §6.3 扣底触发的反面:赢末轮的牌不全是主牌就不扣底 ============
|
||||||
|
t.eq('扣底 反 副牌两连对赢末轮 → 0', mult([id(1, 2, 13), id(2, 2, 13), id(1, 2, 12), id(2, 2, 12)]), 0);
|
||||||
|
t.eq('扣底 反 混合出牌(主K+副K)赢末轮 → 0', mult([id(1, 1, 13), id(1, 2, 13)]), 0);
|
||||||
|
t.eq('扣底 反 主牌在前副牌在后也判 0(与提交顺序无关)', mult([id(1, 2, 13), id(1, 1, 13)]), 0);
|
||||||
|
|
||||||
// ============ §7 算子:每档小光/过庄分界 + 升3级(补强)============
|
// ============ §7 算子:每档小光/过庄分界 + 升3级(补强)============
|
||||||
t.eq('常规65 g39小光', judge(65, 39, false), '小光/4');
|
t.eq('常规65 g39小光', judge(65, 39, false), '小光/4');
|
||||||
t.eq('常规65 g40过庄(分界)', judge(65, 40, false), '过庄/2');
|
t.eq('常规65 g40过庄(分界)', judge(65, 40, false), '过庄/2');
|
||||||
|
|||||||
@@ -138,4 +138,79 @@ t.eq('甩牌集成 跟牌拆散主对→拒', badF.result, false);
|
|||||||
const goodF = P.do_playcard(fpf, [id(1, 1, 11), id(2, 1, 11), id(1, 1, 9)]); // 用主J对对位
|
const goodF = P.do_playcard(fpf, [id(1, 1, 11), id(2, 1, 11), id(1, 1, 9)]); // 用主J对对位
|
||||||
t.eq('甩牌集成 跟牌用主对→接受', goodF.result, true);
|
t.eq('甩牌集成 跟牌用主对→接受', goodF.result, true);
|
||||||
|
|
||||||
|
// ============ §5.1 控制权 + §6.2 捡分归属 ============
|
||||||
|
// 造一轮三家各出 1 张同花色副牌的完整墩:主花色=♦(1),本轮花色=♥(3)。
|
||||||
|
// deal: [[牌id, 座位], ...];scores: [[牌id, 分值], ...]
|
||||||
|
function mkTrick(deal, scores) {
|
||||||
|
const cards = [];
|
||||||
|
for (let i = 0; i < 108; i++) cards[i] = { id: i, dealowner: 99, playround: -1, playowner: -1, score: 0, flower: A.id_to_flower(i), number: A.id_to_number(i) };
|
||||||
|
for (const [cid, seat] of deal) cards[cid].dealowner = seat + 1;
|
||||||
|
for (const [cid, s] of scores) cards[cid].score = s;
|
||||||
|
const sl = () => [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]];
|
||||||
|
const o = { step: 5, banker: 0, flower: 1, cards, seatlist: [sl(), sl(), sl()], playproc: {} };
|
||||||
|
o.method = { have_baofu: () => o.seatlist.some(s => s[4][0] == 0) };
|
||||||
|
P.new_playround(o, 1, 0); // 第 1 轮由庄家(0)首出
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
const H5 = id(1, 3, 5), H8 = id(1, 3, 8), HK = id(1, 3, 13), HA = id(1, 3, 1); // ♥5 ♥8 ♥K ♥A(副牌)
|
||||||
|
const SC = [[H5, 5], [HK, 10]]; // §6.1:5→5分、K→10分;♥8/♥A 不计分
|
||||||
|
|
||||||
|
// —— 反:庄家赢下本轮 → 台面分牌全部作废,不计入闲家捡分 ——
|
||||||
|
const wb = mkTrick([[HA, 0], [HK, 1], [H5, 2]], SC);
|
||||||
|
P.do_playcard(wb, [HA]); P.do_playcard(wb, [HK]);
|
||||||
|
const wbRe = P.do_playcard(wb, [H5]);
|
||||||
|
// 注:第三家出完后 do_playcard 已开新一轮、playproc 被重置,胜者只能从返回值读
|
||||||
|
t.eq('§6.2 反 庄家赢本轮 maxseat=庄', wbRe.maxseat, 0);
|
||||||
|
t.eq('§6.2 反 庄家赢 → 下发面无本轮得分', wbRe.grade, undefined);
|
||||||
|
t.eq('§6.2 反 庄家赢 → 闲家捡分 0(分牌作废)', P.get_jian_grade(wb).grade, 0);
|
||||||
|
|
||||||
|
// —— 正:闲家1 赢下本轮 → 台面【全部】分牌(含庄家自己打出的♥5)计入闲家 ——
|
||||||
|
const w1 = mkTrick([[H5, 0], [HK, 1], [H8, 2]], SC);
|
||||||
|
P.do_playcard(w1, [H5]); P.do_playcard(w1, [HK]);
|
||||||
|
const w1Re = P.do_playcard(w1, [H8]);
|
||||||
|
t.eq('§6.2 正 闲1赢本轮 maxseat=1', w1Re.maxseat, 1);
|
||||||
|
t.eq('§6.2 正 本轮闲家得分=5+10(含庄家打出的♥5)', w1Re.grade, 15);
|
||||||
|
t.eq('§6.2 正 累计捡分=15', P.get_jian_grade(w1).grade, 15);
|
||||||
|
// §5.1:下一轮由本轮牌面最大的一方先出(不是固定庄家、也不是顺序下家)
|
||||||
|
t.eq('§5.1 正 下一轮由本轮最大者(闲1)先出', [w1.playproc.round, w1.playproc.start, w1.playproc.currseat], [2, 1, 1]);
|
||||||
|
|
||||||
|
// —— 边界:两个闲家谁赢都算「闲家捡到」,结果与闲1赢完全一致(design §6.2)——
|
||||||
|
const w2 = mkTrick([[H5, 0], [H8, 1], [HK, 2]], SC);
|
||||||
|
P.do_playcard(w2, [H5]); P.do_playcard(w2, [H8]);
|
||||||
|
const w2Re = P.do_playcard(w2, [HK]);
|
||||||
|
t.eq('§6.2 边界 闲2赢本轮 maxseat=2', w2Re.maxseat, 2);
|
||||||
|
t.eq('§6.2 边界 闲2赢与闲1赢同样计闲家捡分', [w2Re.grade, P.get_jian_grade(w2).grade], [15, 15]);
|
||||||
|
t.eq('§5.1 边界 下一轮改由闲2先出', [w2.playproc.round, w2.playproc.start], [2, 2]);
|
||||||
|
|
||||||
|
// ============ §8 算奖快照 get_seat_cards_award ============
|
||||||
|
// design §8:庄家按【埋牌完成后】的 28 张、闲家按【发牌后】的 28 张;快照全程固定,
|
||||||
|
// 不随之后的出牌改变。例外:70 分投降局庄家未埋牌,快照为「发牌 + 暗牌」共 36 张。
|
||||||
|
function mkAward() {
|
||||||
|
const cards = [];
|
||||||
|
for (let i = 0; i < 108; i++) cards[i] = { id: i, dealowner: -1, playround: -1, playowner: -1, score: 0 };
|
||||||
|
let n = 0;
|
||||||
|
const take = k => { const out = []; while (out.length < k) { out.push(n); n++; } return out; };
|
||||||
|
const bankerDeal = take(28), an = take(8), x1 = take(28), x2 = take(28);
|
||||||
|
bankerDeal.forEach(i => cards[i].dealowner = 1);
|
||||||
|
an.forEach(i => cards[i].dealowner = 0); // 暗牌
|
||||||
|
x1.forEach(i => cards[i].dealowner = 2);
|
||||||
|
x2.forEach(i => cards[i].dealowner = 3);
|
||||||
|
return { o: { banker: 0, flower: 1, cards }, bankerDeal, an, x1 };
|
||||||
|
}
|
||||||
|
// 投降局:未选主未埋牌 → 庄家快照 = 发牌 28 + 暗牌 8 = 36
|
||||||
|
const aw0 = mkAward();
|
||||||
|
t.eq('§8 边界 投降局 庄家快照=发牌+暗牌 36 张', P.get_seat_cards_award(aw0.o, 0).length, 36);
|
||||||
|
// 正常局:埋掉 8 张后 → 庄家快照 28 张,且已埋的不在内
|
||||||
|
const aw = mkAward();
|
||||||
|
const bury = aw.bankerDeal.slice(0, 5).concat(aw.an.slice(0, 3)); // 从发牌与暗牌里各埋几张
|
||||||
|
bury.forEach(i => aw.o.cards[i].playround = 0);
|
||||||
|
const snap = P.get_seat_cards_award(aw.o, 0);
|
||||||
|
t.eq('§8 正 庄家快照=埋牌后 28 张', snap.length, 28);
|
||||||
|
t.eq('§8 正 已埋的 8 张不在快照里', bury.some(i => snap.indexOf(i) >= 0), false);
|
||||||
|
t.eq('§8 正 闲家快照=发牌后 28 张', [P.get_seat_cards_award(aw.o, 1).length, P.get_seat_cards_award(aw.o, 2).length], [28, 28]);
|
||||||
|
// 快照是静态的:之后打出的牌仍算在内(否则闲家重连时算奖/亮牌会缩水)
|
||||||
|
snap.slice(0, 10).forEach(i => aw.o.cards[i].playround = 3);
|
||||||
|
t.eq('§8 边界 打出 10 张后 庄家快照仍是同样的 28 张', P.get_seat_cards_award(aw.o, 0), snap);
|
||||||
|
t.eq('§8 边界 打出后闲家快照也不缩水', P.get_seat_cards_award(aw.o, 1).length, 28);
|
||||||
|
|
||||||
process.exit(t.done('paiju') ? 0 : 1);
|
process.exit(t.done('paiju') ? 0 : 1);
|
||||||
|
|||||||
@@ -284,4 +284,70 @@ t.eq('提示 非法tip=9 拒(回失败包PARAM)', rej(tp, ERR.PARAM), true);
|
|||||||
tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(1, {}));
|
tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(1, {}));
|
||||||
t.eq('提示 缺tip 拒(回失败包PARAM)', rej(tp, ERR.PARAM), true);
|
t.eq('提示 缺tip 拒(回失败包PARAM)', rej(tp, ERR.PARAM), true);
|
||||||
|
|
||||||
|
// ===================== §4.2 叫分:边界与非法入参补全 =====================
|
||||||
|
// design §4.2:范围 5~70、步进 5;暂定庄家必须叫(不能"不叫"),其后才可以"叫更低"或"不叫"
|
||||||
|
t.eq('叫分 边界 下限5 接受', jf(0, 5), 5);
|
||||||
|
t.eq('叫分 正 非首家可以不叫(call=0) 接受', jf(65, 0), 0);
|
||||||
|
t.eq('叫分 反 负数 拒', jf(0, -5), null);
|
||||||
|
t.eq('叫分 反 缺 call 字段(NaN) 拒', jf(0, undefined), null);
|
||||||
|
t.eq('叫分 反 步进外的小值1 拒', jf(0, 1), null);
|
||||||
|
t.eq('叫分 反 与当前叫分相同(不更低) 拒', jf(55, 55), null);
|
||||||
|
|
||||||
|
// ===================== §4.2 已"不叫"的玩家不得再叫 =====================
|
||||||
|
// design §4.2:不叫即退出本局叫分,轮次直接跳过他;他再发 jiaofen 时已不是当前叫分位,应被拒。
|
||||||
|
const bd = P.new({ paiju_list: [] }, 0); // 真实牌局,暂定庄家 = 0 号座位
|
||||||
|
const bdc = setup("00000", bd);
|
||||||
|
mod.jiaofen(pack(0, { call: 65 }));
|
||||||
|
t.eq('叫分 正 首家叫65后轮到 seat1', bd.method.get_callgrade_seat(), 1);
|
||||||
|
mod.jiaofen(pack(1, { call: 0 })); // seat1 不叫
|
||||||
|
t.eq('叫分 正 seat1不叫后跳到 seat2', bd.method.get_callgrade_seat(), 2);
|
||||||
|
const bdLen = bd.callproc.length;
|
||||||
|
bdc.sent.length = 0;
|
||||||
|
mod.jiaofen(pack(1, { call: 60 })); // seat1 反悔想再叫
|
||||||
|
t.eq('叫分 反 已不叫者再叫 → 拒(SEAT)', rej(bdc, ERR.SEAT), true);
|
||||||
|
t.eq('叫分 反 已不叫者再叫 不改变叫分过程', bd.callproc.length, bdLen);
|
||||||
|
t.eq('叫分 反 已不叫者再叫 仍轮到 seat2', bd.method.get_callgrade_seat(), 2);
|
||||||
|
t.eq('叫分 反 已不叫者再叫 未产生庄家', bd.banker, -1);
|
||||||
|
|
||||||
|
// ===================== §4.5 埋牌 maipai:庄家 / 阶段 / 张数 =====================
|
||||||
|
// design §4.5:仅"选主/打牌"路径、由庄家从 36 张里选出 **8 张** 重新扣下
|
||||||
|
// 桩要"完整到能走完成功路径":否则一旦校验被改松,这里会**抛异常打断整个文件**,
|
||||||
|
// 后面的断言全不执行——看到的是崩溃而不是某条断言转红,定位与信号都变差。
|
||||||
|
const mkBury = (step, banker) => ({
|
||||||
|
step, banker, cards: make108(), playproc: { currseat: banker },
|
||||||
|
method: {
|
||||||
|
check_cards_valid: c => P.check_cards_valid({ cards: make108() }, c),
|
||||||
|
check_cards_inhand: () => true,
|
||||||
|
do_burycard: () => { },
|
||||||
|
get_seat_cards: () => [],
|
||||||
|
get_liangpai: () => null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let bc = setup("00000", mkBury(3, 1)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6, 7] }));
|
||||||
|
t.eq('埋牌 反 非庄家 → 拒(SEAT)', rej(bc, ERR.SEAT), true);
|
||||||
|
bc = setup("00000", mkBury(2, 0)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6, 7] }));
|
||||||
|
t.eq('埋牌 反 选主阶段(step2)就埋 → 拒(STEP)', rej(bc, ERR.STEP), true);
|
||||||
|
bc = setup("00000", mkBury(5, 0)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6, 7] }));
|
||||||
|
t.eq('埋牌 反 出牌阶段(step5)再埋 → 拒(STEP)', rej(bc, ERR.STEP), true);
|
||||||
|
bc = setup("00000", mkBury(3, 0)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6] }));
|
||||||
|
t.eq('埋牌 边界 只埋7张 → 拒(PARAM)', rej(bc, ERR.PARAM), true);
|
||||||
|
bc = setup("00000", mkBury(3, 0)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6, 7, 8] }));
|
||||||
|
t.eq('埋牌 边界 埋9张 → 拒(PARAM)', rej(bc, ERR.PARAM), true);
|
||||||
|
bc = setup("00000", mkBury(3, 0)); mod.maipai(pack(0, { cards: [] }));
|
||||||
|
t.eq('埋牌 反 空数组 → 拒(PARAM)', rej(bc, ERR.PARAM), true);
|
||||||
|
|
||||||
|
// ===================== §11 无超时托管:对局阶段不得注册任何定时器 =====================
|
||||||
|
// design §11 已确认「倒计时只作展示,服务端不做任何超时动作」。这不是缺口,而是规则——
|
||||||
|
// 所以这里放的是**守卫用例**:若将来有人给服务端加「超时自动出牌 / AI 代打」,必然要注册
|
||||||
|
// 定时器来推进对局,这条就会转红,从而把违反 §11 的改动拦在提交前。
|
||||||
|
// (min_ontimeout 在**结算**阶段另有正当用途:延迟扣房卡与存战绩,属平台事务、不推进对局,
|
||||||
|
// 故本守卫只覆盖到出牌阶段为止。)
|
||||||
|
const realTO = global.min_ontimeout;
|
||||||
|
let timerCount = 0;
|
||||||
|
global.min_ontimeout = function () { timerCount++; };
|
||||||
|
const gd = driveToPlay("00000");
|
||||||
|
mod.chupai(pack(0, { cards: [P.get_seat_cards(gd.pj, 0)[0]] }));
|
||||||
|
global.min_ontimeout = realTO;
|
||||||
|
t.eq('§11 叫分→选主→埋牌→出牌 全程未注册任何定时器', timerCount, 0);
|
||||||
|
|
||||||
process.exit(t.done('rpc') ? 0 : 1);
|
process.exit(t.done('rpc') ? 0 : 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user