二七王:C-1 修复 room.mySeat 永远为 -1

平台 12_Logic.js 在第 389 行调 Game_Modify.appStart(),而 C_Player = new Player(-1)
要到同函数第 480 行才执行,此刻 typeof C_Player === 'undefined',appStart 整体空转;
之后玩家登录 SetSeat 拿到真座位,appStart 再也不会被调用。于是 mySeat 永远停在 -1:
发包带 seat:-1 被服务端 check_player 全部拒绝、StartWar 差异化下发认领不到本座位而
整包丢弃、SeatMap.toDisplay(-1) 抛错——且全程无一处报错。

- 新增 SubGameHooks._syncMySeat():room.mySeat 的唯一写入处(SSOT),幂等、
  座位非法或 C_Player 缺席时保持原值不倒退
- 在 setRoomDes / StartWar / Reconnect / ReconnectNoMakewar / appStart 各调用一次,
  不赌「所有路径都经过某一个入口」
- 新增 SubGameHooks.changeSeat 并同步座位:Desk.change_seat 是唯一「座位中途变化
  且不重走 setRoomDes」的路径,漏掉会让 mySeat 静默过期
- Rpc._seat() 改 fail-fast:座位非 0/1/2 即抛错,不再无声透传 -1
- 新增 client/tests/test_appstart_timing.js:复刻平台真实初始化时序,
  全程不直接赋值 mySeat;已反向验证(退回 6 个调用点后核心断言转红)
- test_rpc_send 补座位非法抛错断言;test_consistency 删除空转的 my.mustCard EXCLUDE

client 23 脚本全绿,server 17 脚本 / 680 checks 全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 21:05:46 +08:00
co-authored by Claude Opus 5
parent 22e43cf2e6
commit af779f473a
5 changed files with 224 additions and 7 deletions
+22
View File
@@ -85,4 +85,26 @@ t.eq('牌 id 107 合法', throws(() => EQW_Rpc.chupai([107])), false);
t.eq('叫分 5 合法', throws(() => EQW_Rpc.jiaofen(5)), false);
t.eq('叫分 70 合法', throws(() => EQW_Rpc.jiaofen(70)), false);
// ---- 【C-1】座位校验:非 0/1/2 一律 fail-fast,绝不无声透传 ----
// mySeat 初始值是 -1(平台 appStart 早于 C_Player 创建,见 test_appstart_timing.js)。
// seat:-1 发出去,服务端 check_player 必然返回 falsy → 每个操作都回 ERR.PLAYER,
// 而前端一声不吭——整局玩不了却查不出原因。所以在最近处抛错。
const badSeats = [-1, 3, 1.5, undefined, null, '1', NaN];
badSeats.forEach(s => {
sent.length = 0;
EQW_GameState.room.mySeat = s;
t.eq('座位 ' + JSON.stringify(s) + ' 发包抛错', throws(() => EQW_Rpc.zhunbei()), true);
t.eq('座位 ' + JSON.stringify(s) + ' 时一个包都没发', sent.length, 0);
});
// 非法座位对每个发包方法都拦(校验在 _send 的公共入口,不是逐方法漏配)
EQW_GameState.room.mySeat = -1;
t.eq('非法座位时出牌也抛错', throws(() => EQW_Rpc.chupai([1, 2])), true);
t.eq('非法座位时叫分也抛错', throws(() => EQW_Rpc.jiaofen(5)), true);
t.eq('非法座位时埋牌也抛错', throws(() => EQW_Rpc.maipai([1,2,3,4,5,6,7,8])), true);
[0, 1, 2].forEach(s => {
EQW_GameState.room.mySeat = s;
t.eq('座位 ' + s + ' 合法不抛', throws(() => EQW_Rpc.zhunbei()), false);
});
process.exit(t.done('rpc_send') ? 0 : 1);