二七王:design 全量复验(无新增不一致)+ 补端到端整局单测
对 E1~E3 修复后的实现做取证式复核:把 design 的判定语句做成可执行探针直接 打在代码上(§5.1/§5.2 跟牌层级、§8.1 算奖连对链、§7 算子逐档、§5.4 甩牌最大性、 §6.3 扣底、§8.4 三条支付线、§4/§9/§10/§11 流程与门控),未发现新的不一致。 新增 test/test_endgame.js:用真实发牌跑完整一局(叫分→上庄→选主→埋牌→28 轮 出牌→小局结算)并校验与牌面无关的不变量(牌张守恒 84/8/16、捡分=闲家赢得分牌 +扣底分、X 分配庄±2X/闲∓X、算奖 X×(2Ni−Nj−Nk)、零和、result 与判定一致), 另覆盖投降局与中途解散两条支线。连跑 10 次无抖动,全套单测 389 项通过。 合规文档补记「第四轮·复验」取证表。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,7 +112,24 @@
|
||||
|
||||
### 尚未处置
|
||||
|
||||
**无。** E1/E2/E3 已全部改代码 + 补单测(全套 368 项全绿),协议文档同步。
|
||||
**无。** E1/E2/E3 已全部改代码 + 补单测,协议文档同步。
|
||||
|
||||
### 第四轮·复验(E1~E3 修复后的独立取证复核)
|
||||
|
||||
对修复后的实现又做了一次**取证式**复核:不再靠通读,而是把 design 的判定语句直接做成可执行探针打在代码上。**未发现新的不一致**,全部符合。取证覆盖:
|
||||
|
||||
| 维度 | 取证方式 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| §5.1/§5.2 跟牌 | 层级 2/3/4(放着更长拖拉机不出→拒、拆对出散张→拒、留着对子只出散张→拒)、同花色不足全出+缺口任意、混合出牌牌面判 0、毙牌须完全缺门且牌型对应、首家出主必须跟主 | 15/15 符合 |
|
||||
| §8.1 算奖 | 三/四王、链从正 7 起、无正 7 链断、正 7-副 7-正 2-副 2-主 A-主 K 链 5、副 K 对(非主花色)不入链、六/七/八个 7 与 2、固定主 ≥10 不给奖 | 13/13 符合 |
|
||||
| §7 算子 | 常规与爬坡逐档抽查 18 格(含大光/小光/过庄/升 N 级、爬坡 45 独有档、10 分与 5 分无小光档) | 18/18 符合 |
|
||||
| §5.4 甩牌 | 单一牌型不算甩牌、分量被更大同长拖拉机压→甩错、对手仅持更大散张压不了对子、甩错只打最小一张、"持有即算被压" | 6/6 符合 |
|
||||
| §6.3 扣底 | 混合甩牌取最高规格倍数、末轮用副牌赢不扣底 | 符合 |
|
||||
| §8.4 结算 | **闲家也持奖时的三条独立支付线**(此前未覆盖):`N=[4,1,0]`→`aw=[28,-8,-20]`,与捡分子数叠加后零和 | 符合 |
|
||||
| §12 全流程 | 端到端跑通整局 + 18 局随机模糊(含甩牌轮),校验牌张守恒/捡分一致/结算零和/分配公式/扣底触发条件 | 无异常 |
|
||||
| §4/§9/§10/§11 | 叫分 6 种序列(含跳过已"不叫"者)、70 分暗牌下发与投降资格、重连各阶段门控(暗牌仅庄家、pushlist/seatlist/亮牌按 `nocheck`、当前轮桌面牌两种模式都有)、爬坡房 `multiple` 三处同源、提示只发对家、明牌返回另两家全部主牌、扣卡 4 组合 | 全部符合 |
|
||||
|
||||
固化产物:新增 `test/test_endgame.js`(L4 端到端,21 项,断言均与具体牌面无关、连跑 10 次无抖动),全套单测 **389 项**全绿。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ node server/games/erqiwang/test/test_arith.js # 单独跑某一组
|
||||
- `test_paiju.js`:亮牌 get_liangpai(§8.2,含「埋牌后静态快照、出牌后重连不缩水」回归)、结算 get_paiju_account 集成(§7/§8,大光/过庄/升级/爬坡/傍王/投降)、扣底触发 get_bottom_account(§6.3)。
|
||||
- `test_deal.js`:发牌构成——92张/去3-4/三家28+底8/分值(§2/§6.1)。
|
||||
- `test_desk.js`:坐庄轮换 do_prepare(庄赢连庄/闲赢·投降下家)(§4.6)。
|
||||
- `test_endgame.js`(L4 端到端,用真实发牌跑完整一局):叫分→上庄→选主→埋牌→28 轮出牌→小局结算的流程闭合(§12.1),以及与牌面无关的不变量(牌张守恒 84/8/16、捡分=闲家赢得分牌+扣底分、`X` 分配庄±2X/闲∓X、算奖 `X×(2Ni−Nj−Nk)`、结算零和、`result` 与判定一致);另覆盖 §4.4/§7.1 投降局与 §12.2 中途解散两条支线。
|
||||
- `test_rpc.js`(L3,用 `_rpc.js` 装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 seatlist/baozhu 查牌门控(§9,含「seatlist 整表带三家」)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。
|
||||
|
||||
## 约定(dev-guide §10 测试纪律)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// L4 端到端:用真实发牌跑完整一局(叫分→上庄→选主→埋牌→28轮出牌→小局结算),
|
||||
// 校验 design §12.1 的流程闭合与一批与牌面无关的不变量(牌张守恒/捡分一致/结算零和/分配公式)。
|
||||
// 另覆盖 §4.4 投降局与 §12.2 中途解散两条支线。
|
||||
// 说明:牌是随机发的,但断言的都是**与具体牌面无关**的不变量,不会因发牌不同而抖动。
|
||||
const { mod, setup } = require('./_rpc.js');
|
||||
const t = require('./_assert')();
|
||||
const P = global.cls_youle_erqiwang_paiju;
|
||||
const AR = global.cls_youle_erqiwang_arith;
|
||||
|
||||
const pk = (seat, d) => ({ conmode: 0, fromid: seat, data: Object.assign({ agentid: 1, playerid: seat, gameid: 1, roomcode: 1, seat: seat }, d || {}) });
|
||||
|
||||
// 开局并让 0 号位以 call 分坐庄(另两家不叫)
|
||||
function seatBanker(roomtype, call) {
|
||||
const pj = P.new({ paiju_list: [] }, 0);
|
||||
const ctx = setup(roomtype, pj);
|
||||
mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: call }));
|
||||
mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 }));
|
||||
mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 }));
|
||||
return { pj, ctx };
|
||||
}
|
||||
// 出牌驱动:首家出手上最小的单张,跟家取第一张能通过 can_followcard 的牌
|
||||
function playAll(pj) {
|
||||
let guard = 0;
|
||||
while (pj.step === 5 && ++guard < 200) {
|
||||
const seat = pj.playproc.currseat, hand = P.get_seat_cards(pj, seat);
|
||||
let pick = null;
|
||||
if (seat === pj.playproc.start) {
|
||||
pick = [hand[hand.length - 1]];
|
||||
} else {
|
||||
for (const c of hand) {
|
||||
if (AR.can_followcard(pj.flower, hand, [c], pj.playproc.startcount, pj.playproc.startflower, pj.playproc.starttype).result) { pick = [c]; break; }
|
||||
}
|
||||
}
|
||||
if (!pick) { return false; }
|
||||
mod.chupai(pk(seat, { cards: pick }));
|
||||
}
|
||||
return pj.step === 6;
|
||||
}
|
||||
|
||||
// ============ §12.1 正常局全流程 ============
|
||||
const { pj, ctx } = seatBanker("00000", 65);
|
||||
t.eq('§4.2 坐庄(banker/call/step)', [pj.banker, pj.call, pj.step], [0, 65, 2]);
|
||||
t.eq('§4.3 庄家摸暗牌后36张 / 闲家28张', [P.get_seat_cards(pj, 0).length, P.get_seat_cards(pj, 1).length], [36, 28]);
|
||||
mod.xuanzhu(pk(0, { flower: 1 }));
|
||||
t.eq('§4.4 选主后进埋牌阶段', [pj.flower, pj.step], [1, 3]);
|
||||
mod.maipai(pk(0, { cards: P.get_seat_cards(pj, 0).slice(-8) }));
|
||||
t.eq('§4.5 埋牌后庄家28张、进出牌阶段', [P.get_seat_cards(pj, 0).length, pj.step], [28, 5]);
|
||||
t.eq('§5.1 第一轮固定由庄家先出', [pj.playproc.round, pj.playproc.start], [1, 0]);
|
||||
t.eq('§5/§6 28轮打完进入结算', playAll(pj) && pj.playproc.round, 28);
|
||||
t.eq('§2 牌张守恒(出84 / 埋8 / 去掉3-4共16)',
|
||||
[pj.cards.filter(c => c.playround > 0).length, pj.cards.filter(c => c.playround === 0).length, pj.cards.filter(c => c.number === 3 || c.number === 4).length],
|
||||
[84, 8, 16]);
|
||||
|
||||
const last = ctx.sent[ctx.sent.length - 1];
|
||||
const aset = last.data.aset;
|
||||
t.eq('结算包 rpc/success', [last.rpc, last.data.success], ['jiesuan', true]);
|
||||
t.eq('§11 结束亮出底牌8张', last.data.bottom.cards.length, 8);
|
||||
// §6.2/§6.3:闲家捡分 = 闲家赢下的分牌 + 扣底(可能翻倍)分
|
||||
let jian = 0;
|
||||
for (const c of pj.cards) { if (c.playowner > -1 && c.playowner !== pj.banker) { jian += c.score; } }
|
||||
const bonus = last.data.bottom.grade2 || 0;
|
||||
t.eq('§6.2/§6.3 捡分 = 闲家赢得分牌 + 扣底分', aset.grade, jian + bonus);
|
||||
// §6.3:扣底只在「闲家用主牌赢下末轮」时发生
|
||||
const lastWinIsXian = pj.playproc.maxseat !== pj.banker;
|
||||
const lastMult = lastWinIsXian ? AR.get_bottom_multiple(pj.flower, pj.playproc.cards[pj.playproc.maxseat]) : 0;
|
||||
t.eq('§6.3 扣底触发条件(闲家 + 主牌)', lastMult > 0, !!last.data.bottom.multiple);
|
||||
// §7 第3层:X = 基础子数 × |判定倍率|,庄家 ±2X、闲家各 ∓X
|
||||
const X = aset.multiple * Math.abs(aset.upgrade);
|
||||
const win = aset.upgrade > 0 ? 1 : -1;
|
||||
t.eq('§7 捡分子数分配 庄±2X / 闲各∓X', aset.seatlist.map(p => p.grade_jf), [win * X * 2, -win * X, -win * X]);
|
||||
// §8.4 第4层:算奖线 X×(2Ni−Nj−Nk)
|
||||
const N = aset.seatlist.map(p => p.naward);
|
||||
t.eq('§8.4 算奖分配 X×(2Ni−Nj−Nk)', aset.seatlist.map(p => p.grade_aw),
|
||||
[X * (2 * N[0] - N[1] - N[2]), X * (2 * N[1] - N[0] - N[2]), X * (2 * N[2] - N[0] - N[1])]);
|
||||
t.eq('本局结算零和', aset.seatlist.reduce((s, p) => s + p.grade, 0), 0);
|
||||
t.eq('§4.6 result 与判定一致(0庄赢/1闲赢)', pj.result, aset.upgrade > 0 ? 0 : 1);
|
||||
|
||||
// ============ §4.4 / §7.1 投降局 ============
|
||||
const tx = seatBanker("00000", 70);
|
||||
t.eq('§4.3 70分坐庄:暗牌亮3秒标志 + 闲家也收到暗牌',
|
||||
[tx.ctx.sent[tx.ctx.sent.length - 1].data.ancard3s, tx.ctx.sent[tx.ctx.sent.length - 2].data.bottomcards.length], [1, 8]);
|
||||
mod.touxiang(pk(0));
|
||||
const txa = tx.ctx.sent[tx.ctx.sent.length - 1].data.aset;
|
||||
t.eq('§7.1 投降:base=1 / 庄-2、闲各+1', [txa.multiple, txa.seatlist.map(p => p.grade_jf)], [1, [-2, 1, 1]]);
|
||||
t.eq('§4.4 投降不选主、不埋牌', [tx.pj.flower, tx.pj.cards.filter(c => c.playround === 0).length], [-1, 0]);
|
||||
t.eq('§8.4 投降算奖按庄家36张快照', P.get_seat_cards_award(tx.pj, 0).length, 36);
|
||||
t.eq('§4.6 投降 result=2(下家坐庄)', tx.pj.result, 2);
|
||||
|
||||
// ============ §12.2 中途解散 ============
|
||||
const ds = seatBanker("00000", 65);
|
||||
mod.xuanzhu(pk(0, { flower: 1 }));
|
||||
const dsmsg = require('../class.export.js').new().get_disbandRoom(ds.ctx.o_room);
|
||||
t.eq('§12.2 解散:不结算子数、result=3', [dsmsg.data.aset.multiple, dsmsg.data.aset.seatlist.map(p => p.grade), ds.pj.result], [0, [0, 0, 0], 3]);
|
||||
|
||||
process.exit(t.done('endgame') ? 0 : 1);
|
||||
Reference in New Issue
Block a user