diff --git a/docs/REMEDIATION_PLAN.md b/docs/REMEDIATION_PLAN.md index 6681a75..40deffe 100644 --- a/docs/REMEDIATION_PLAN.md +++ b/docs/REMEDIATION_PLAN.md @@ -38,7 +38,7 @@ Key: `S` ≤ half day · `M` ~1–3 days · `L` ~1 week+ · 🔴 launch-blocker - [x] **2.3 M — Input validation + leak fixes.** ✅ `register_client` validates `clientId` (type/length/charset) before it enters `clientSocketMap` — closes the map-poisoning vector. `mempoolNotified` is now a Map with real TTL eviction in PaymentHandlers (the dead duplicate Set + its random-10% eviction removed from SocketHandlers). `sessions` cache evicted on disconnect (`removeSocket`, called after suspend reads it). `sessionManager.dispose()` / `suspendedGameManager.cleanup()` / `paymentHandlers.dispose()` wired into `shutdown()`. `checkGamesTimeout` snapshots before iterating + awaits sequentially (was mutate-during-`forEach` + fire-and-forget). New `inputValidation.test.js`. ## Phase 3 — Game integrity (🟠) -- [ ] **3.1 S — Uniform time budget** (elapsed/fixed-block, not "next block"); fix early-entry (`src/network/queueHandler.js:241`). +- [x] **3.1 S — Anti-instant-death grace only (NO fairness floor).** ✅ Random block timing is the game's core mechanic and is deliberately preserved — early entry is a knowingly-risky bet, not a bug. The only change is a tiny configurable grace (`GAME_START_GRACE_MS`, default 2000ms, 0 to disable) so a block landing the instant a game starts can't kill the player before the dungeon renders / before their first move is possible. _(Rejected the originally-planned uniform time budget — it would gut the random-timing mechanic.)_ - [ ] **3.2 S — Reachability guarantee** entrance→treasure→exit (`src/game/dungeon.js`). - [ ] **3.3 S — Swap/passthrough collision fix** (`src/game/game.js:181`). diff --git a/src/.env.example b/src/.env.example index 63d9075..6fc93a4 100644 --- a/src/.env.example +++ b/src/.env.example @@ -348,6 +348,12 @@ RPC_POLL_INTERVAL=2000 # req.ip. Leave unset/false for direct exposure, otherwise clients could spoof their IP. # TRUST_PROXY=true +# Anti-instant-death grace (ms) before a block-timeout can end a game. This is NOT a +# fairness floor — random block timing is the core mechanic. It only prevents the +# degenerate case where a block lands the instant a game starts (before the dungeon +# renders / before the first move is possible). Default 2000 (2s); set 0 to disable. +# GAME_START_GRACE_MS=2000 + # ============================================================================= # SECTION 12: SERVER & MULTI-INSTANCE # ============================================================================= diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js index 53f9ca9..fbf90bb 100644 --- a/src/network/socketHandlers.js +++ b/src/network/socketHandlers.js @@ -1038,10 +1038,18 @@ class SocketHandlers { // Snapshot the entries first: handleGameOver mutates activeGames (it deletes the // entry), so iterating the live Map could skip games. Process sequentially and // await each game-over rather than firing them all off concurrently on one tick. + // Anti-instant-death guard ONLY (default 2s, set 0 to disable). Random block timing + // is the game's core mechanic and is deliberately preserved — this is not a fairness + // floor. It only avoids the degenerate case where a block lands in the same instant + // the game starts and the player dies before the dungeon even renders / before their + // first move is possible (100ms move cooldown). + const graceMs = (() => { const v = parseInt(process.env.GAME_START_GRACE_MS, 10); return Number.isFinite(v) ? v : 2000; })(); + const now = Date.now(); const snapshot = Array.from(this.activeGames.entries()); for (const [socketId, game] of snapshot) { const user = this.connectionHandler.getUserBySocket(socketId); - if (user && user.blockRec && currentHeight > user.blockRec) { + const elapsedMs = now - (game.startedAt || now); + if (user && user.blockRec && currentHeight > user.blockRec && elapsedMs >= graceMs) { if (this.debugManager.CONSOLE_LOGGING) { console.log(`💀 GAME TIMEOUT for player ${socketId}: entered on block ${user.blockRec}, died on block ${currentHeight}`); }