比賽進行¶
本檔角色:從等待房鎖定資料的 preloading 二次驗證、GO 前 race-ready 屏障到完賽 / 超時的所有同步機制。 結算流程見 比賽結算.md;演算法見 ../算式表.md。
1. 鎖定裝備的二次驗證與參與證明(LoadoutSubmission)¶
等待房的 ReadyDeclaration 已經是最終選車,並已收進不可變的 LockedStartPackage。preloading 狀態不再允許換車;仍在線的 survivor 在固定時間內重送 matchId-bound LoadoutSubmission,用途是取得 ledger 參與證明並對鎖定資料作第二道驗證。倒數中已確認的 preStartDisconnects 不建立 race transport、不要求提交,也不提供本場重連;他們仍保留在 original roster、matchId 與原起跑格。提交與 survivor roster 交叉核對完成後才建立本回合 world/network/presentation;GO 倒數與模擬 frame pump仍須再通過 §1.7。
1.1 提交時間窗¶
| 階段 | 時長 | 行為 |
|---|---|---|
| 等待提交 | LOADOUT_SUBMIT_WINDOW_SEC(60 秒) |
收集 survivor participants 的 LoadoutSubmission;窗內收件即驗,無獨立互驗窗 |
| 完成提交階段 | — | 收到且精確符合 LockedStartPackage → 進 survivor roster 交叉核對;未提交者列為 disconnect,仍須滿足 original roster 的至少 2 人嚴格多數 |
提交窗以注入的單調時鐘記錄絕對 deadline;setTimeout 只負責喚醒並在 callback 時重算剩餘時間,不能把背景分頁被瀏覽器夾長的 timer 當成協議時鐘。回到前景時立即重評 deadline;尚未到期但 2 秒重送已逾期,只補送目前提交一次並從當下排下一次,不追補成 burst。
1.2 提交訊息¶
interface LoadoutSubmission {
type: 'loadout-submit';
matchId: string;
participant: PeerId;
loadout: MatchParticipantLoadout; // carRotation(一車 / 回合);見 車輛組裝.md
submittedAt: Timestamp;
signature: Signature;
}
1.3 驗證項¶
| 項 | 規則 |
|---|---|
| 簽章 | Ed25519 驗章,綁 matchId(簽 loadoutSigningMessage(matchId, loadout)=跨場不可重放;賽後隨結算存 loadoutSignatures=參與證明,見 §1.4) |
| 鎖定資料一致 | participant 與 carRotation 必須精確等於 LockedStartPackage 的 ReadyDeclaration;車庫後續修改或替換提交一律 fail closed |
| Loadout 結構 | 8 類 part 對應、Mount 配對演算法通過、passive_weight_split_pct 值域與 passive-only(../車輛組裝.md §3.5) |
| 整車約束 | 依 零件與共用介面.md §7.1 的單一權威值檢核 |
| 黑名單檢查 | 用到的 CID 不在 moderation 黑名單 |
| B 軸資產版本 | loadout 各 CID 的 schema 版本可用性檢核(基準 = Room.pinnedCheckpointCid、新鮮度 ≤ ROOM_PINNED_CHECKPOINT_MAX_AGE_HOURS 72h)——../版本規範.md §24・../程式架構/matchmaking.md §5 |
| 場地相容性 | 車輛 AABB.x + NARROWEST_PATH_MARGIN_M(0.05)≤ auto_narrowest_path_m(場地)— 警告但不阻擋 |
1.4 不上鏈¶
LoadoutSubmission 不單獨寫進 ledger,賽後 loadout 與其 matchId-bound 簽章分別納入 MatchResultEvent.loadouts 與 loadoutSignatures 一併儲存(省 ledger 空間 + 賽後可審計)。簽章 = 參與證明:收件 ⓪-loadout 驗其有效性、applier 只罰 / 只評有 loadout 者——擋憑空自造一場塞任意玩家 disconnects 投毒信譽 / 灌斷線(../程式架構/ledger-admission.md §1);absent(未交 loadout= 無承諾)者不受斷線罰。
1.5 重連時的 loadout 處理¶
本節只適用 GO 後的中途斷線。倒數中列入 preStartDisconnects 者本場不提供重連;GO 後中途斷線 < 15 秒可重連的玩家沿用原 loadout(避免重連時偷換)。重連時 peer 端比對 loadoutHash:
if (reconnectingPeer.loadoutHash !== originalSubmission.loadoutHash) {
// 拒絕重連,視為新連線(重新加入需新一場比賽)
}
1.6 名單交叉核對(開賽前共識輪)¶
loadout 交換各端的提交窗獨立定案——名單剪刀差(某端視 X 在場、他端視 X 逾時 disconnect)若不擋下,開賽後各端模擬的車數不同 = 決定性立即發散。交換定案後、世界構建前:
%%{init: {"theme":"base","themeVariables":{"fontSize":"14px","primaryTextColor":"#1f2937","lineColor":"#64748b"},"flowchart":{"curve":"basis"}}}%%
flowchart TD
A["各端算名單指紋 rosterHash"] --> B["廣播 roster-confirm<br/>含週期重送・首見回打"]
B --> C{"全員指紋一致?"}
C -->|"一致"| D(["進 race-ready 屏障"])
C -->|"不一致/逾時未齊"| X["fail-closed 全員棄賽<br/>重新配對"]
classDef local fill:#e8f3ec,stroke:#3f8f5f,stroke-width:1.4px,color:#173525;
classDef consensus fill:#fdf3df,stroke:#c08a2d,stroke-width:1.4px,color:#3d2c0d;
classDef fail fill:#fae9e7,stroke:#b4544a,stroke-width:1.4px,color:#471d18;
class A,B,C consensus
class D local
class X fail
- 各端算 survivor 名單指紋——rosterHash = sha256(sorted
${peerId}:${loadoutHash}行 join '\n');matchId、grid proof 與 original roster 不重算。 - 廣播 roster-confirm(含週期重送;首見對端指紋 = 回打自己的——對端可能漏收首發)。
- 全員指紋一致 → 進 racing;任一不一致或逾時未齊 → fail-closed 全員棄賽(重新配對)。
寄件者身分 = 傳輸層連線歸屬認證(同 control 訊息慣例)。已知取捨:房內任一成員可蓄意送錯指紋炸掉開賽(等價於它拒不提交 loadout 的擾亂能力、不放大)。
1.7 GO 前 race-ready 屏障¶
survivor roster 共識完成後,每一回合都建立新的 RaceReadyCoordinator。屏障只計已通過上述交叉核對的 survivor participants;original roster 內的 preStartDisconnects 與 preloading 缺席者不送、不等,觀戰者也不能阻塞 GO。訊息綁定 matchId、roundIndex、傳輸來源與 sender,非 survivor roster、錯場、錯回合或來源冒名一律丟棄。
type RaceReadyMilestone = 'world-built' | 'network-ready' | 'presentation-ready';
interface RaceReadyMessage {
type: 'race-ready';
matchId: string;
roundIndex: number;
sender: PeerId;
milestone: RaceReadyMilestone;
}
milestone 為有限、嚴格單調序列:
world-built:本回合物理 world 與所有車輛 rig 已建立。network-ready:本回合 rollback network 已可用。presentation-ready:場地資產、全 survivor roster 車資產皆成功完成,且 viewport 在完成後實際 render 至少一幀;proxy/失敗資產不得假冒完成。同賽道下一回合仍須重開 readiness 世代。
每端只接受高於既有 rank 的進度;重送或倒退不算進度、不得延長 stall 窗。最新本機 milestone 每 2 秒重送以容忍 control message 遺失。stall 與 resend 都保存單調時鐘絕對 deadline,timer callback 只重算/重排;回到前景時立即檢查 stall,若仍有效但 resend 已逾期則只補送一次。只有全 survivor roster 都達 presentation-ready 才建立 countdown/frame pump 並進 GO。
RacePage 在 readiness barrier 期間以局部「巡禮」導播場內車群,不把巡禮放回 RoomPage,也不用全場 bounds fit。每回合 countdown 開始時 participant 一次切到跟車自己;之後由使用者控制,淘汰或完賽不自動切鏡。外部 spectator 初始為自由。
取消語意:任一 survivor peer 在 GO 前觸發 peer-left,或 RACE_READY_STALL_TIMEOUT_MS(30 秒)內沒有任何嚴格進度,立即取消屏障。此時不倒數、不 step、不產生 round/match settlement,也不記 forfeit、斷線或信譽變動。房主批次移除 missing 的非 host participant(host 永遠保留),房間回 waiting、清 currentMatchId、LockedStartPackage 作廢、全員 Ready 歸零並廣播單一新 snapshot;所有仍在線 participant 返回 /room/:roomId。其後須重新 Ready、驗證並自動倒數,以新 startedAt 推導新 id。
2. 高層概念¶
| 機制 | 目的 |
|---|---|
| Rollback Netcode | input 預測 + 錯誤回滾 |
| Fixed timestep 60Hz | 跨平台 deterministic |
| Snapshot Checksum | 偵測 desync |
| 回合 Consensus Anchor | 終局尾窗唯一多簽證書 |
3. 每幀(16ms)¶
%%{init: {"theme":"base","themeVariables":{"fontSize":"14px","primaryTextColor":"#1f2937","lineColor":"#64748b"},"flowchart":{"curve":"basis"}}}%%
flowchart TD
A(["玩家 input<br/>鍵盤/手把"]) --> B["本地 client 預測 step<br/>並廣播我方 input"]
B --> C{"peer 比對<br/>預測 vs 實際 input"}
C -->|"不符"| D["rollback<br/>回 snapshot・重 step 到當前"]
C -->|"符合"| E["繼續"]
D --> F["渲染當前 state<br/>固定頻率插值平滑"]
E --> F
classDef local fill:#e8f3ec,stroke:#3f8f5f,stroke-width:1.4px,color:#173525;
classDef consensus fill:#fdf3df,stroke:#c08a2d,stroke-width:1.4px,color:#3d2c0d;
classDef ui fill:#efeafa,stroke:#7a5cb8,stroke-width:1.4px,color:#2b1d4d;
class A,B local
class C,D,E consensus
class F ui
- 玩家 input(鍵盤 / 手把)→ 本地 client——用我方 input 預測本幀 step;用對手「最近收到的 input」當預測;廣播我方 input 到 mesh DataChannel。
- 其他 peer 接收——比對我方預測 vs 實際對手 input;不符 → rollback:回到 N frame 前的 snapshot、用正確 input 重新 step 到當前 frame;符合 → 繼續。
- 渲染當前 state(含 60Hz 插值平滑)。
輸入採純 rollback,不另加 input delay。
4. 每 120 frame(2 秒)— Snapshot Checksum¶
%%{init: {"theme":"base","themeVariables":{"fontSize":"14px","primaryTextColor":"#1f2937","lineColor":"#64748b"},"flowchart":{"curve":"basis"}}}%%
flowchart TD
A["所有 peer 各自計算<br/>當前世界狀態 hash"] --> B["廣播 my_checksum"]
B --> C["接收其他 peer 的 checksum"]
C --> D{"全部相同?"}
D -->|"是"| E["一致"]
D -->|"有差異"| X["觸發 desync 處理"]
classDef consensus fill:#fdf3df,stroke:#c08a2d,stroke-width:1.4px,color:#3d2c0d;
classDef fail fill:#fae9e7,stroke:#b4544a,stroke-width:1.4px,color:#471d18;
class A,B,C,D,E consensus
class X fail
hash 演算法 =SHA-256(32 bytes)。
SNAPSHOT_INTERVAL_FRAMES: 120、PEER_DESYNC_TOLERANCE_FRAMES: 30(等待容差窗:某 peer 的 checksum 逾 30 frame 未到 → 該輪視同缺席、不阻塞多數決)。
5. 每回合終局 — 唯一 Consensus Anchor¶
每 120 frame 的 checksum 票只做 ephemeral desync 偵測。每回合終局在 bounded window 內建立至多一張寫入 ledger 的嚴格多數證書,公式與事件 schema 見 ../資料系統.md §6。
%%{init: {"theme":"base","themeVariables":{"fontSize":"14px","primaryTextColor":"#1f2937","lineColor":"#64748b"},"flowchart":{"curve":"basis"}}}%%
flowchart TD
A["穩定順序 proposer<br/>提出終局 tail 候選"] --> B{"其他 peer 驗本機保留/final<br/>checksum、roster、每回合未簽?"}
B -->|"一致"| C["簽章背書並鎖定本回合"]
B -->|"不一致/已簽"| D["拒簽"]
C --> E{"湊到嚴格多數?"}
E -->|"是"| F["廣播唯一完整證書"]
F --> G["保留證書<br/>結算時 inline 進 MatchResult"]
E -->|"否且 proposer 逾時"| H["下一位 deterministic fallback"]
H -.->|"bounded window 到期"| I["該回合 roundAnchors[] = null<br/>整場禁止鑄幣"]
classDef consensus fill:#fdf3df,stroke:#c08a2d,stroke-width:1.4px,color:#3d2c0d;
classDef chain fill:#e7eefb,stroke:#3f6bb0,stroke-width:1.4px,color:#152848;
classDef fail fill:#fae9e7,stroke:#b4544a,stroke-width:1.4px,color:#471d18;
class A,B,C,D,E,H consensus
class F,G chain
class I fail
步驟細目:
- proposer 依 original roster 的 PeerId code-unit 順序選出;逾時每
CONSENSUS_ANCHOR_FALLBACK_SLOT_MS(2000ms)換下一位。 - 候選 frame 只能是 rollback finality 之外最後 120-frame 對齊邊界或其前一個邊界。signer 必須仍保留該幀、checksum 相同、roster/離場時序一致,且本回合尚未簽過。
- 湊到
floor(presentPeers.length/2)+1個不同 signer 後廣播完成證書;結算時將完整證書 inline 放入MatchResultEvent.roundAnchors[roundIndex]。證書簽gridContextDigest + gridSeed綁定外層唯一完整gridProof,不重複攜帶大型 commitment/reveal payload;未達 quorum 填null。 CONSENSUS_ANCHOR_COLLECTION_TIMEOUT_MS(12000ms)到期仍無 quorum → 對應roundAnchors[roundIndex]填null;不得單簽 fallback,canonical fold 只能產生mintEligible=false。
6. Desync 偵測與處理¶
%%{init: {"theme":"base","themeVariables":{"fontSize":"14px","primaryTextColor":"#1f2937","lineColor":"#64748b"},"flowchart":{"curve":"basis"}}}%%
flowchart TD
A["checksum 廣播後等待窗收齊<br/>逾窗視同缺席"] --> B{"有嚴格多數 checksum?"}
B -->|"有"| C["少數 peer 累積 desync strike<br/>一致時 strike 歸零"]
B -->|"無"| X["全端結束整場<br/>consensus-invalid"]
C --> D{"連續 strike 達上限?"}
D -->|"是"| E["persistent-desync<br/>自報 leave・踢出計棄賽"]
E --> F[("DesyncEvent 進 ledger<br/>被踢者自簽自報")]
classDef consensus fill:#fdf3df,stroke:#c08a2d,stroke-width:1.4px,color:#3d2c0d;
classDef chain fill:#e7eefb,stroke:#3f6bb0,stroke-width:1.4px,color:#152848;
classDef fail fill:#fae9e7,stroke:#b4544a,stroke-width:1.4px,color:#471d18;
class A,B,C,D consensus
class E,X fail
class F chain
步驟細目:
- Snapshot Checksum 廣播後,等待窗 PEER_DESYNC_TOLERANCE_FRAMES(30 frame ≈ 0.5s)收齊——逾窗未到的 peer 該輪視同缺席、不阻塞多數決。
- 只接受嚴格多數 checksum。2–2、3–3、2–2–1 等平手無法辨認哪端是少數,所有端一致結束整場
consensus-invalid,不各自把自己或對手當成被驅逐者。 - 有嚴格多數時,與多數派不一致的 peer → 累積 1 次 desync strike;一致時歸零——不 rollback、不中斷比賽(決定性重播同一批 input 必得同一結果,治不了真分歧)。
- 連續 strike ≥ DESYNC_STRIKE_LIMIT(3、防假陽性)→ persistent-desync——該玩家廣播 leave(reason: 'desync')→ 被踢出 race、計棄賽;DesyncEvent 進 ledger(被踢者自簽自報、含其觀測的 hash 票面)。
DesyncEvent 不影響玩家信譽、純供開發者調查(../程式架構/ledger.md §9;作弊由自動機制當場處置——持續 desync 踢出計棄賽即其代價,作弊非檢舉類型,../信譽系統.md §5.1)。簽章 = 被踢者單簽(無法強制他人為調查記錄簽章;事件無 state reducer、收件放行)——signatures[] 欄容多簽、他端自願附簽為開放項。
7. Anchor 衝突處理¶
同一 (matchId, roundIndex) 若收到兩張不同且各自完整有效的嚴格多數證書,代表 per-round sign-once equivocation 或 membership view 分裂。各端保存兩張原始證書與重疊 signer 至 RaceAbortEvidenceEvent.anchorConflicts,整場 consensus-invalid、禁止鑄幣;不採最長鏈、first-seen 或 checksum 字典序選邊,也不做 majority state overwrite。
8. 觀戰整合¶
觀戰者:
- 接收 world descriptor/checkpoint/已簽 input stream,或能力不足時的精簡 fallback
- 不簽章(不影響共識)
- 主模式執行唯讀、非權威 deterministic replay;不送 input、不參與 checksum 多數決
- 共用 CameraController:跟車 / 自由 / 巡禮;淘汰與完賽不自動切鏡
詳見 觀戰.md。
9. HUD(不顯示數字預警)¶
HUD 欄位 / 錨點 /widget 行為權威 = ../賽內機制.md §2.4(本檔不重列)。設計選擇:過熱 / 損耗走視覺 mask 不顯示數字、無 cooldown timer、無「即將過熱」預警(殺手鐧靠玩家判斷溫度餘額)。
10. 比賽中圖示訊息¶
%%{init: {"theme":"base","themeVariables":{"fontSize":"14px","primaryTextColor":"#1f2937","lineColor":"#64748b"},"flowchart":{"curve":"basis"}}}%%
flowchart TD
A(["participant 展開快捷選單"]) --> B["選取凍結 loadout slot<br/>立即收合"]
B --> C["簽章 RaceMessageWire<br/>ordered app-control 廣播"]
C --> D["participant 驗 match・roster・loadout<br/>sequence・簽章・限速"]
C --> E["participant source 中繼<br/>原始簽章給 spectator"]
E --> F["spectator 重驗原 signer"]
D --> G["固定 sender HUD<br/>依顯示規則取代與退場"]
F --> G
classDef local fill:#e8f3ec,stroke:#3f8f5f,stroke-width:1.4px,color:#173525;
classDef consensus fill:#fdf3df,stroke:#c08a2d,stroke-width:1.4px,color:#3d2c0d;
classDef ui fill:#efeafa,stroke:#7a5cb8,stroke-width:1.4px,color:#2b1d4d;
class A,B local
class C,D,E,F consensus
class G ui
步驟細目:
- participant 以單一按鈕展開 3×3 九宮格;選取凍結 slot 後立即收合。
- 送端以平均每 2 秒一則、burst 2 限速,簽章只含 match、loadout digest、slot、sequence 與 messageId 的
RaceMessageWire;至少一條 control link 可寫才回報成功。 - participant 驗原 signer、match、固定 roster、loadout、單調 sequence 與簽章;source 向 spectator 中繼同一份原始簽章,spectator 不信任 source 重包身分。
- participant 與 spectator 都依 canonical roster 顯示固定 HUD。每 sender 最多一條,新接受訊息立即取代並獲得完整 5 秒。
- 賽內不建立自由文字輸入、文字歷史或 spectator chat;等待房文字聊天另見 ../程式架構/chat-system.md。
11. 反作弊(賽內層)¶
| 攻擊 | 防禦 |
|---|---|
| 修改 client 物理 | snapshot checksum 多簽偵測 |
| Input 偽造 | 每個 input 帶 sign + nonce + timestamp |
| Rapier collider 順序差異 | mesh fingerprint 跨 peer 比對 |
| 卡關(卡空 / 卡牆) | 卡空 → 脫離 / 掉出自動重定位(../遊戲機制.md §12);卡牆不另偵測 = 無法完賽(賽程時間上限兜底) |
| 連續 desync | 當場踢出計棄賽(斷線 −5 / forfeit 自然承接);DesyncEvent 純記錄供調查、不進信譽 / 黑名單(黑名單 = 純仲裁三振,../程式架構/ledger.md §9) |
詳見 ../資安規範.md §12。
12. 中途斷線與重連¶
12.1 Reconnect 流程¶
%%{init: {"theme":"base","themeVariables":{"fontSize":"14px","primaryTextColor":"#1f2937","lineColor":"#64748b"},"flowchart":{"curve":"basis"}}}%%
flowchart TD
A(["玩家斷線"]) --> B["其他 peer 啟動 ReconnectHandler<br/>在重連窗內輪詢"]
B -->|"重連成功"| C["沿用原 loadout・比對 loadoutHash<br/>+定向 state catch-up"]
C -->|"catch-up 收斂"| D["續賽"]
C -->|"仍落後/無人應答"| E["後續 checksum 分歧<br/>走 desync 自我驅逐"]
B -->|"重連窗期滿"| X["視為異常斷線"]
classDef local fill:#e8f3ec,stroke:#3f8f5f,stroke-width:1.4px,color:#173525;
classDef consensus fill:#fdf3df,stroke:#c08a2d,stroke-width:1.4px,color:#3d2c0d;
classDef fail fill:#fae9e7,stroke:#b4544a,stroke-width:1.4px,color:#471d18;
class A local
class B,C consensus
class D local
class E,X fail
步驟細目:
- 玩家斷線 → 其他 peer 啟動 ReconnectHandler(RECONNECT_TIMEOUT_MS: 15_000);15 秒內持續嘗試 reconnect(500ms 輪詢)。
- 重連成功 → 沿用原 loadout(比對 loadoutHash 防偷換,§1.5)+ 催定向 state catch-up(best-effort:帶 requestId/target 的 sync-request → 同一 peer 關聯 sync-response → 原子 loadState;未請求 / 錯來源 / 超過 900 幀拒收;見 ../程式架構/network-sync.md §7.1)。
- catch-up 收斂則續賽;仍落後 / 無人應答 → 後續 checksum 分歧走 desync 自我驅逐(≡ 異常斷線)。
- 15 秒未重連 → 視為異常斷線。
12.2 異常斷線後處理¶
- 15 秒後的 race-mesh
offline不代表 room-control/spectator-stream 離線;三平面以(peerId, plane, generation)各自裁定。 - GO 後由固定 original active roster 的嚴格多數守門:本端可達數
≥ floor(N/2)+1才移除失聯者並續賽;不足則partition-void,不產生 MatchResult/信譽/TrueSkill。 - 可續賽側仍須由原 roster 嚴格多數對包含
disconnects的完整 candidate 簽章;此簽章集即 removal certificate。3 人 2–1 可 2 簽;4 人 3–1 要 3 簽;4 人 2–2、2 人 1–1 均無法定稿。 - 合法 removal 的玩家位置為最後完賽者之後(依斷線時間排序);已交本場簽章 loadout 者信譽扣 −5(absent 不罰,見 ../信譽系統.md §2)。
12.3 中途斷線者壓 σ 權重¶
| 條件 | 行為 |
|---|---|
| 累積斷線次數 < 5 | TrueSkill 正常更新 |
| 累積斷線次數 ≥ 5 | σ 維持較大(不收斂,σ ≥ 6.0),配對時與資深玩家避開 |
adjustRatingForFrequentDisconnects(rating, disconnectCount, threshold = 5)
12.4 Graceful Leave vs 異常斷線¶
| 類別 | 訊息 | 信譽影響 | 經濟結算 |
|---|---|---|---|
| Graceful Leave | GO 後先送 { type: 'leave', reason: 'voluntary' },再 best-effort 上鏈本人單簽 RaceLeaveEvent |
本人 disconnectCounts +1、frequent-disconnect -5;與同場 MatchResultEvent.disconnects 以 (matchId, peerId) 持久去重 |
RaceLeaveEvent 本身不產生排名、TrueSkill 或經濟效果;若後續 MatchResult 達原 active roster 嚴格多數,才以 forfeit 記最後名次、不計 finisherCount、不發獎金(經濟系統.md §6.2) |
| 異常斷線 | race-mesh 15 秒未重連;由原 active roster 嚴格多數納入 MatchResultEvent.disconnects |
disconnectCounts +1、frequent-disconnect -5;與同場 RaceLeaveEvent 共用去重鍵 |
合法 MatchResult 以 forfeit 記最後名次並累積影響 σ;無 quorum 則整場 fail-closed,不得由少數替第三人扣分 |
13. 回合結束觸發¶
| 觸發 | 處理 |
|---|---|
完成 lap_count 圈 |
玩家進入「已完賽」;其他人繼續 |
chassis broken(physicsRetired) |
玩家自動切觀戰;battery/motor 失能不切觀戰 |
| 場上無未完賽存活車 | battery/motor 失能車進入最多 120 幀的回合級終局窗;全部低於速度門檻連續 15 幀可提前結束 |
| 玩家主動退出 | 計為棄賽(§12.4) |
| 達房間設定的賽程時間上限(預設 600s、範圍 60–1800) | 強制結算依當前名次 |
進入 比賽結算.md。
14. 跨模組對接¶
跨模組名稱、責任與實作路徑只由 程式架構.md §1 的模組表定義;本流程各步 在首次使用處連到相應實作 authority,模組摘要以該表為準。