Skip to content

fix: prevent iterator invalidation in handleDeadDefender (BUG-02)#3943

Open
berkelmali wants to merge 2 commits into
openfrontio:mainfrom
berkelmali:fix/bug02-dead-defender-iterator
Open

fix: prevent iterator invalidation in handleDeadDefender (BUG-02)#3943
berkelmali wants to merge 2 commits into
openfrontio:mainfrom
berkelmali:fix/bug02-dead-defender-iterator

Conversation

@berkelmali
Copy link
Copy Markdown
Contributor

@berkelmali berkelmali commented May 16, 2026

Description: In AttackExecution.handleDeadDefender(), target.tiles() returns a live collection that is modified during iteration when conquer(tile) is called. This can cause iterator invalidation and unpredictable behavior in for...of loops over Sets. Fix: Snapshot target.tiles() into an array via Array.from() before iterating, and increase the loop limit from 10 to 100 with an early break when no tiles remain -- ensuring large empires are fully absorbed.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

barfires

Snapshot target.tiles() into an array before iterating to prevent
modifying the collection during iteration when conquer() is called.
Also increase loop limit from 10 to 100 with early break on empty
to properly handle large empires.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

Review Change Stack

Walkthrough

The change refines the post-defender-conquest sweep in handleDeadDefender by increasing the outer loop limit from 10 to 100 iterations, snapshotting remaining defender tiles into an array each iteration, and breaking early when no tiles remain. Each snapshot is then processed to apply border and interior conquest logic. New regression tests exercise the sweep to ensure completion and correct tile transfers.

Changes

Post-defender conquest sweep

Layer / File(s) Summary
Conquest sweep iteration logic
src/core/execution/AttackExecution.ts
The loop that processes conquered tiles after a defender dies now snapshots target.tiles() into remainingTiles each iteration, runs up to 100 iterations (early exit when empty), and applies border vs interior ownership handling to tiles in each snapshot.
Regression tests for sweep behavior
tests/HandleDeadDefender.test.ts
Adds test setup and three regression tests that spawn attacker/defender, expand defender territory in scenarios, run ticks until defender dies, assert the defender ends with zero tiles, assert the sweep completes within a bounded time, and assert attacker gains the tiles.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

Bugfix

Suggested reviewers

  • evanpelle
  • ryanbarlow97

Poem

🏰 The tiles are snapshotted, the loops now run wide,
A hundred passes check what's left inside.
When no more remains, the sweep will cease—
Territories change and order finds peace.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing iterator invalidation in handleDeadDefender by snapshotting the live collection.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly explains the bug (iterator invalidation in handleDeadDefender), describes the fix (snapshot tiles into array and increase loop limit), and relates directly to the code changes shown in the summary.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/execution/AttackExecution.ts`:
- Around line 370-373: Add deterministic regression tests for the new sweep
iteration logic in AttackExecution (the loop that calls target.tiles() and does
up to 100 passes). Create tests that (1) mutate the live collection during
iteration to verify safe snapshot behavior by asserting no runtime errors and
that mutated tiles are processed as expected, (2) simulate a larger
dead-defender tile set to confirm full absorption (all defender tiles removed)
after the sweep completes, and (3) verify stable termination when no tiles
remain by asserting the loop exits early (fewer than 100 passes) and no infinite
loop occurs; use a fixed board/state or seeded RNG to ensure determinism and
reference the AttackExecution class and its sweep behavior (the for-loop using
target.tiles()) in test names and assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dadb6691-141d-4eea-93f3-a13671286c01

📥 Commits

Reviewing files that changed from the base of the PR and between b813792 and 2570c30.

📒 Files selected for processing (1)
  • src/core/execution/AttackExecution.ts

Comment on lines +370 to +373
for (let i = 0; i < 100; i++) {
const remainingTiles = Array.from(target.tiles());
if (remainingTiles.length === 0) break;
for (const tile of remainingTiles) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add regression tests for this core simulation change

This fixes a real runtime edge case, but the PR includes no tests for the new sweep behavior (snapshot iteration + 100-pass/early-break flow). Please add deterministic tests that cover at least: (1) live-collection mutation safety, (2) full absorption for larger dead-defender tile sets, and (3) stable termination when no tiles remain.

As per coding guidelines: “All changes to src/core/ must include tests”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/execution/AttackExecution.ts` around lines 370 - 373, Add
deterministic regression tests for the new sweep iteration logic in
AttackExecution (the loop that calls target.tiles() and does up to 100 passes).
Create tests that (1) mutate the live collection during iteration to verify safe
snapshot behavior by asserting no runtime errors and that mutated tiles are
processed as expected, (2) simulate a larger dead-defender tile set to confirm
full absorption (all defender tiles removed) after the sweep completes, and (3)
verify stable termination when no tiles remain by asserting the loop exits early
(fewer than 100 passes) and no infinite loop occurs; use a fixed board/state or
seeded RNG to ensure determinism and reference the AttackExecution class and its
sweep behavior (the for-loop using target.tiles()) in test names and assertions.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 16, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/HandleDeadDefender.test.ts`:
- Line 141: Capture the attacker's tile count before performing the sweep/attack
by storing attacker.numTilesOwned() in a variable (e.g., preTiles), then run the
existing sweep/attack call in the test, then get a new count (postTiles =
attacker.numTilesOwned()) and replace the current loose assertion
expect(attacker.numTilesOwned()).toBeGreaterThan(0) with
expect(postTiles).toBeGreaterThan(preTiles) to assert tiles were transferred
from the defender; use variable names like preTiles/postTiles to make intent
clear.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1dcb017-7bd9-43d3-aab8-e5e6a2973fd0

📥 Commits

Reviewing files that changed from the base of the PR and between 2570c30 and b73fe47.

📒 Files selected for processing (1)
  • tests/HandleDeadDefender.test.ts

expect(defender.numTilesOwned()).toBe(0);

// Attacker should have gained tiles
expect(attacker.numTilesOwned()).toBeGreaterThan(0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen the assertion to verify tile transfer.

The assertion checks that the attacker has more than zero tiles, but the attacker already owns tiles from spawning (line 48-50). This doesn't prove that tiles were transferred from the defender during the sweep.

Consider capturing the attacker's tile count before the attack and asserting that it increased:

💪 Suggested improvement
+    const attackerTilesBefore = attacker.numTilesOwned();
+
     // Attack and kill the defender
     game.addExecution(new AttackExecution(null, attacker, defender.id(), null));

     for (let i = 0; i < 500; i++) {
       game.executeNextTick();
       if (!defender.isAlive()) break;
     }

     // All defender tiles should now belong to someone other than defender
     expect(defender.numTilesOwned()).toBe(0);

     // Attacker should have gained tiles
-    expect(attacker.numTilesOwned()).toBeGreaterThan(0);
+    expect(attacker.numTilesOwned()).toBeGreaterThan(attackerTilesBefore);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(attacker.numTilesOwned()).toBeGreaterThan(0);
const attackerTilesBefore = attacker.numTilesOwned();
// Attack and kill the defender
game.addExecution(new AttackExecution(null, attacker, defender.id(), null));
for (let i = 0; i < 500; i++) {
game.executeNextTick();
if (!defender.isAlive()) break;
}
// All defender tiles should now belong to someone other than defender
expect(defender.numTilesOwned()).toBe(0);
// Attacker should have gained tiles
expect(attacker.numTilesOwned()).toBeGreaterThan(attackerTilesBefore);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/HandleDeadDefender.test.ts` at line 141, Capture the attacker's tile
count before performing the sweep/attack by storing attacker.numTilesOwned() in
a variable (e.g., preTiles), then run the existing sweep/attack call in the
test, then get a new count (postTiles = attacker.numTilesOwned()) and replace
the current loose assertion expect(attacker.numTilesOwned()).toBeGreaterThan(0)
with expect(postTiles).toBeGreaterThan(preTiles) to assert tiles were
transferred from the defender; use variable names like preTiles/postTiles to
make intent clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant