二七王:修正 design 全量重核发现的 3 项不一致(亮牌快照/报无主下发面/甩牌张数上限)

- §8.2 亮牌改用埋牌后 28 张静态快照 get_seat_cards_award,不再用
  get_seat_cards(只含未出的牌)——否则闲家重连时 get_deskinfo 按庄家
  当前剩余手牌重算,亮牌统计缩水甚至变 null。
- §9 出牌包由只带出牌者一家的 info 改为整表下发三家 seatlist,与重连包
  PushCards.seatlist 同名同构;报无主后另两家的主牌数/对子数不再滞后两次出牌。
- §5.4.3 甩牌"无组合数量限制":can_playcard 去掉规则外的 14 张硬上限,
  改为结构性上界 28(单人手牌上限,牌型编码在此范围内不溢出)。

协议文档同步改写 chupai 三处 info 条目与 liangpai 口径;合规文档补记第四轮
核对(流程层无缺环,规则层 3 项已收口)。全套单测 368 项通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-16 19:33:06 +08:00
co-authored by Claude Opus 5
parent b6aeba412f
commit 916066ba96
9 changed files with 72 additions and 15 deletions
+2 -2
View File
@@ -32,10 +32,10 @@ node server/games/erqiwang/test/test_arith.js # 单独跑某一组
- `test_follow.js`:正常跟牌/毙牌/垫牌,单张/对子/拖拉机的必出与缺门毙牌数量对应(§5.1/5.2)。
- `test_callgrade.js`:叫分坐庄 do_callgrade(叫5立即/两家不叫/后叫更低)(§4.2)。
- `test_config.js`:roomtype 位串解析、局数/扣卡映射(§10.1)。
- `test_paiju.js`:亮牌 get_liangpai(§8.2)、结算 get_paiju_account 集成(§7/§8,大光/过庄/升级/爬坡/傍王/投降)、扣底触发 get_bottom_account(§6.3)。
- `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_rpc.js`(L3,用 `_rpc.js` 装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 info/baozhu 查牌门控(§9)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。
- `test_rpc.js`(L3,用 `_rpc.js` 装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 seatlist/baozhu 查牌门控(§9,含「seatlist 整表带三家」)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。
## 约定(dev-guide §10 测试纪律)
+7
View File
@@ -114,6 +114,13 @@ t.eq('甩牌 甩错·双对之一被压', r.shuaicuo, true);
t.eq('甩牌 合法·单张+拖拉机', play([big(1), K1, K2, Q1, Q2], [[P5, P6], []]).result, true);
t.eq('甩牌 合法·单+对+拖三混', play([big(1), id(1, 1, 10), id(2, 1, 10), K1, K2, Q1, Q2], [[P5, P6], []]).result, true);
t.eq('甩牌 副禁甩·对手非缺门仍禁', play([fK1, f9], [[fK2, id(1, 2, 10)], []]).result, false);
// design §5.4.3「无组合数量限制」:曾有「一次最多14张」的规则外硬上限,会把 15 张以上的
// 合法甩牌整个拒掉。16 张全固定主牌(4王 + 8个7 + 正2对 + 副2对)、两对手无主牌 → 必须可甩
const SHUAI16 = [big(1), big(2), small(1), small(2),
id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7), id(1, 4, 7), id(2, 4, 7),
id(1, 1, 2), id(2, 1, 2), id(1, 4, 2), id(2, 4, 2)];
t.eq('甩牌 16张(超14张上限)仍可甩', play(SHUAI16, [[], []]).result, true);
t.eq('甩牌 16张 有分量需求下发', !!play(SHUAI16, [[], []]).shuai_demand, true);
// ============ §5.4.4 跟甩牌强制分量拆解 flush_follow_ok ============
const J1 = id(1, 1, 11), N9a = id(1, 1, 9), N9b = id(2, 1, 9), P8 = id(1, 1, 8);
+14
View File
@@ -27,6 +27,20 @@ t.eq('亮牌 2王2个7(不达标)', lp([big(1), small(1), id(1, 1, 7), id(2, 1,
t.eq('亮牌 6个7(仅qi)', lp([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]), { qi: 6 });
t.eq('亮牌 固定主9不达标→null', lp([big(1), small(1), 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)]), null);
t.eq('亮牌 固定主10边界(仅zhu)', lp([big(1), small(1), 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)]), { zhu: 10, zhupair: 4, zhutuo: 1 });
// design §8.2:亮牌依据是【庄家埋牌后】的静态手牌快照,全程固定、不随出牌缩水
// (闲家断线重连时 get_deskinfo 会再取一次;若按当前剩余手牌重算,亮牌会缩水甚至变 null)
const LIANG28 = fill([big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)], 28);
const EXPECT_LIANG = { zhu: 10, zhupair: 5, zhutuo: 1, wang: 4, qi: 6 };
t.eq('亮牌 埋牌后快照(出牌开始)', P.get_liangpai(mkHand(LIANG28)), EXPECT_LIANG);
const played = mkHand(LIANG28); // 同一手牌,已打出2大王+1对♦7
for (const c of played.cards){
if (c.id === big(1) || c.id === big(2) || c.id === id(1, 1, 7) || c.id === id(2, 1, 7)){ c.playround = 1; }
}
t.eq('亮牌 打出4张后重连仍按快照', P.get_liangpai(played), EXPECT_LIANG);
// 埋下的牌(playround==0)不算在快照里
const buried = mkHand(LIANG28);
for (const c of buried.cards){ if (c.id === big(1) || c.id === big(2)){ c.playround = 0; } }
t.eq('亮牌 已埋的牌不计入快照', P.get_liangpai(buried), { qi: 6 });
// ============ §7/§8 结算 get_paiju_account 集成 ============
// 受控 o_paiju/o_desk/o_room:控制庄家算奖手牌(dealowner=1)与闲1捡分(playowner=1,score)
+7 -3
View File
@@ -36,7 +36,7 @@ t.eq('投降 埋牌后step3 拒(回失败包STEP)', rej(ctx, ERR.STEP), true);
p = mkTX(2, 1, 70); ctx = setup("00000", p); mod.touxiang(pack(0));
t.eq('投降 非庄家 拒(回失败包SEAT)', rej(ctx, ERR.SEAT), true);
// ===================== §9 出牌 chupai 的查牌门控(info/baozhu)=====================
// ===================== §9 出牌 chupai 的查牌门控(seatlist/baozhu)=====================
// 造真实牌局 → 强制到首出(step5),庄家(0)出一张副牌单张;seat1 已报无主
function mkChupai(roomtype) {
const o_desk_stub = { paiju_list: [] };
@@ -55,10 +55,14 @@ function mkChupai(roomtype) {
return c;
}
let cv = mkChupai("00000"); // 可查牌
t.eq('出牌 可查牌 有info', cv.sent[0] && cv.sent[0].data.info !== undefined, true);
t.eq('出牌 可查牌 有seatlist', cv.sent[0] && cv.sent[0].data.seatlist !== undefined, true);
// design §9:报无主后要为全体三人显示【另外两家】的主牌数/对子数 → 下发面必须整表带三家,
// 不能只带出牌者一家(否则另两家的统计要滞后到它们各自出牌才补上)
t.eq('出牌 可查牌 seatlist 带三家', cv.sent[0] && cv.sent[0].data.seatlist.length, 3);
t.eq('出牌 可查牌 seatlist 含非出牌者的报副统计', cv.sent[0] && cv.sent[0].data.seatlist[1][4][0], 0);
t.eq('出牌 可查牌 baozhu=1(有人报无主)', cv.sent[0] && cv.sent[0].data.baozhu, 1);
let cn = mkChupai("00001"); // 不查牌(位4=1)
t.eq('出牌 不查牌 无info', cn.sent[0] && cn.sent[0].data.info, undefined);
t.eq('出牌 不查牌 无seatlist', cn.sent[0] && cn.sent[0].data.seatlist, undefined);
t.eq('出牌 不查牌 baozhu=0', cn.sent[0] && cn.sent[0].data.baozhu, 0);
// ===================== §4.2 mod.jiaofen 入参校验 =====================