fix(core): reject spawn intents after the spawn phase (anti-teleport)#4657
Conversation
SpawnExecution.tick() relinquished the player's territory and re-conquered it at the intent's tile with no spawn-phase gate, and the engine ticks the execution both during and after the spawn phase. A crafted `spawn` intent sent mid-game therefore teleported an already-established player anywhere on the map. Ignore a spawn intent when the game is no longer in the spawn phase and the player has already spawned, so the intent is a deterministic no-op on every client. First spawns and in-phase re-picks are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WalkthroughAdds a guard that ignores post-spawn relocation intents outside the spawn phase. Updates spawn coverage and adds an anti-teleport test confirming the player’s spawn tile and owned territory remain unchanged. ChangesSpawn security
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…#4657) ## Problem Patches sending the `spawn` intent over the game WebSocket while a match is underway: ```js ws.send(JSON.stringify({ type: "intent", intent: { type: "spawn", tile: tileRef } })); ``` The server relays it like any intent, and `SpawnExecution.tick()` then **relinquishes the player's entire territory and re-conquers it at the target tile** — instant teleport anywhere on the map. The root cause is that `SpawnExecution.tick()` had no spawn-phase gate (only a random-spawn re-roll guard), and the engine ticks the execution **both during and after** the spawn phase (`activeDuringSpawnPhase()` is `true`, and after the phase `!inSpawnPhase()` is also `true`). ## Fix Ignore a spawn intent when the game is no longer in the spawn phase **and** the player has already spawned: ```js if (!this.mg.inSpawnPhase() && player.hasSpawned()) { return; } ``` Because `inSpawnPhase()` / `hasSpawned()` are deterministic game state, the intent becomes a no-op identically on every client — no desync. Unaffected: - First-time spawns (`hasSpawned()` is false) - In-phase spawn picks and re-picks (`inSpawnPhase()` is true) - Existing random-spawn re-roll protection (kept as-is) ## Tests - Added `Spawn intent after the spawn phase cannot relocate territory (anti-teleport)` — establishes a player, then asserts a later `spawn` intent leaves `spawnTile()` and `numTilesOwned()` unchanged. - Simplified `Spawn on specific tile` to a single first spawn (the previous version relied on an out-of-phase re-pick, which this fix now correctly forbids). - Full `tests/core` suite green (259 tests). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Problem
Patches sending the
spawnintent over the game WebSocket while a match is underway:The server relays it like any intent, and
SpawnExecution.tick()then relinquishes the player's entire territory and re-conquers it at the target tile — instant teleport anywhere on the map.The root cause is that
SpawnExecution.tick()had no spawn-phase gate (only a random-spawn re-roll guard), and the engine ticks the execution both during and after the spawn phase (activeDuringSpawnPhase()istrue, and after the phase!inSpawnPhase()is alsotrue).Fix
Ignore a spawn intent when the game is no longer in the spawn phase and the player has already spawned:
Because
inSpawnPhase()/hasSpawned()are deterministic game state, the intent becomes a no-op identically on every client — no desync.Unaffected:
hasSpawned()is false)inSpawnPhase()is true)Tests
Spawn intent after the spawn phase cannot relocate territory (anti-teleport)— establishes a player, then asserts a laterspawnintent leavesspawnTile()andnumTilesOwned()unchanged.Spawn on specific tileto a single first spawn (the previous version relied on an out-of-phase re-pick, which this fix now correctly forbids).tests/coresuite green (259 tests).🤖 Generated with Claude Code