Skip to content

fix: guard division-by-zero in WinCheck when all tiles have fallout#3882

Open
berkelmali wants to merge 2 commits into
openfrontio:mainfrom
berkelmali:fix/wincheck-divzero-fallout
Open

fix: guard division-by-zero in WinCheck when all tiles have fallout#3882
berkelmali wants to merge 2 commits into
openfrontio:mainfrom
berkelmali:fix/wincheck-divzero-fallout

Conversation

@berkelmali
Copy link
Copy Markdown
Contributor

@berkelmali berkelmali commented May 7, 2026

Description: When numTilesWithoutFallout === 0 (all land tiles have fallout), the tile-percentage division produces Infinity, and Infinity > 80 is true in JS - falsely declaring a winner. This PR: - Guards with numTilesWithoutFallout > 0 before the percentage check - Uses integer cross-multiplication (tiles * 100 > threshold * total) to avoid floating-point division entirely - Adds tests for both FFA and Team modes verifying no winner is declared in the zero-fallout edge case

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Walkthrough

Win-detection guards were added: both FFA and team checks early-return when zero non-fallout tiles exist; team check uses a direct arithmetic comparison instead of an intermediate percentage. Tests verify no winner is declared when all land tiles have fallout.

Changes

Win-Check Threshold Refactor

Layer / File(s) Summary
Threshold Guard
src/core/execution/WinCheckExecution.ts
Added early-return when numTilesWithoutFallout <= 0 in checkWinnerFFA and checkWinnerTeam; removed intermediate percentage in team check and compare directly max.numTilesOwned() * 100 >= percentageTilesOwnedToWin() * numTilesWithoutFallout.
Fallout Tile Regression Tests
tests/core/executions/WinCheckExecution.test.ts
Added FFA and Team tests that assert mg.setWinner is not called when numTilesWithFallout equals total land tiles, even if ownership meets the percentage threshold.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

When every tile turns to dust and gray,
the win checks quietly step away.
A zero guard closes the gate,
no sudden victor decides your fate. 🛡️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix: guarding against division-by-zero when all tiles have fallout in the WinCheck logic.
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 PR description clearly explains the bug (division-by-zero when numTilesWithoutFallout === 0), describes the fix (guard check and integer cross-multiplication), and lists the test coverage added.

✏️ 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.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 7, 2026
Comment on lines 69 to 70
const numTilesWithoutFallout =
this.mg.numLandTiles() - this.mg.numTilesWithFallout();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why nut just do:

if numTilesWithoutFallout === this.mg.numLandTiles() {
return
}

@berkelmali
Copy link
Copy Markdown
Contributor Author

Thanks for the suggestion, @evanpelle! That is a much cleaner way to handle it. I've updated the PR to use the early return guard clause (numTilesWithoutFallout === 0) instead of the complex threshold logic, and I kept the tests verifying it.

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: 2

🤖 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/WinCheckExecution.ts`:
- Around line 72-74: In WinCheckExecution.ts adjust the guard so timer-based win
conditions still run when all tiles have fallout: in the win-checking logic
(functions checkWinner and checkWinnerTeam) remove the early "if
(numTilesWithoutFallout === 0) return;" that short-circuits the whole combined
if, and instead apply the numTilesWithoutFallout === 0 check only to the
tile-percentage term inside the combined if (i.e., wrap the tile-percentage
comparison with "&& numTilesWithoutFallout > 0" or equivalent). Keep the other
terms (maxTimerValue countdown and HARD_TIME_LIMIT_SECONDS) evaluated normally
so timer expiries can still declare a winner.
- Around line 118-119: The comparison uses floating-point division in
WinCheckExecution (the expression "(max[1] / numTilesWithoutFallout) * 100")
which violates src/core/ determinism rules; replace it with an integer
cross-multiplication to avoid floats: compare max[1] * 100 against
this.mg.config().percentageTilesOwnedToWin() * numTilesWithoutFallout (using the
same identifiers max, numTilesWithoutFallout and
this.mg.config().percentageTilesOwnedToWin()) so the logic is identical but uses
only integer arithmetic.
🪄 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: 395e9281-12ba-4f07-b9a9-2990faf2c836

📥 Commits

Reviewing files that changed from the base of the PR and between 82dc05d and dd3ec83.

📒 Files selected for processing (2)
  • src/core/execution/WinCheckExecution.ts
  • tests/core/executions/WinCheckExecution.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/core/executions/WinCheckExecution.test.ts

Comment thread src/core/execution/WinCheckExecution.ts Outdated
Comment on lines +72 to +74
if (numTilesWithoutFallout === 0) {
return;
}
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 | ⚡ Quick win

Timer-based win conditions are suppressed when all tiles have fallout.

The early return before the combined if block bypasses all three win conditions — tile-percentage, maxTimerValue countdown, and HARD_TIME_LIMIT_SECONDS. The timer conditions have nothing to do with numTilesWithoutFallout, so a game where all tiles have fallout when the timer expires (or the hard limit fires) will never declare a winner. The server's hard kill then ends the game without a graceful winner, which defeats the purpose of HARD_TIME_LIMIT_SECONDS.

The guard should wrap only the tile-percentage term:

-    if (numTilesWithoutFallout === 0) {
-      return;
-    }
-
     if (
-      (max.numTilesOwned() / numTilesWithoutFallout) * 100 >
-        this.mg.config().percentageTilesOwnedToWin() ||
+      (numTilesWithoutFallout > 0 &&
+        (max.numTilesOwned() / numTilesWithoutFallout) * 100 >
+          this.mg.config().percentageTilesOwnedToWin()) ||
       (this.mg.config().gameConfig().maxTimerValue !== undefined &&
         timeElapsed - this.mg.config().gameConfig().maxTimerValue! * 60 >= 0) ||
       timeElapsed >= WinCheckExecution.HARD_TIME_LIMIT_SECONDS

Apply the same pattern in checkWinnerTeam (lines 113-115 and 117-119).

Also applies to: 113-115

🤖 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/WinCheckExecution.ts` around lines 72 - 74, In
WinCheckExecution.ts adjust the guard so timer-based win conditions still run
when all tiles have fallout: in the win-checking logic (functions checkWinner
and checkWinnerTeam) remove the early "if (numTilesWithoutFallout === 0)
return;" that short-circuits the whole combined if, and instead apply the
numTilesWithoutFallout === 0 check only to the tile-percentage term inside the
combined if (i.e., wrap the tile-percentage comparison with "&&
numTilesWithoutFallout > 0" or equivalent). Keep the other terms (maxTimerValue
countdown and HARD_TIME_LIMIT_SECONDS) evaluated normally so timer expiries can
still declare a winner.

Comment on lines +118 to +119
(max[1] / numTilesWithoutFallout) * 100 >
this.mg.config().percentageTilesOwnedToWin() ||
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 | ⚡ Quick win

Floating-point division on a changed line violates the src/core/ determinism guideline.

Line 118 still computes (max[1] / numTilesWithoutFallout) * 100, which is floating-point division. The PR description said this would be replaced with integer cross-multiplication, but it was not. The coding guideline for src/core/ requires avoiding floating-point math to keep simulation deterministic.

Replace with an integer comparison (no division, no floats):

-      (max[1] / numTilesWithoutFallout) * 100 >
-        this.mg.config().percentageTilesOwnedToWin() ||
+      max[1] * 100 >
+        this.mg.config().percentageTilesOwnedToWin() * numTilesWithoutFallout ||

As per coding guidelines: "Ensure deterministic behavior in src/core/ by using seeded PRNG and avoiding floating-point math."

📝 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
(max[1] / numTilesWithoutFallout) * 100 >
this.mg.config().percentageTilesOwnedToWin() ||
max[1] * 100 >
this.mg.config().percentageTilesOwnedToWin() * numTilesWithoutFallout ||
🤖 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/WinCheckExecution.ts` around lines 118 - 119, The
comparison uses floating-point division in WinCheckExecution (the expression
"(max[1] / numTilesWithoutFallout) * 100") which violates src/core/ determinism
rules; replace it with an integer cross-multiplication to avoid floats: compare
max[1] * 100 against this.mg.config().percentageTilesOwnedToWin() *
numTilesWithoutFallout (using the same identifiers max, numTilesWithoutFallout
and this.mg.config().percentageTilesOwnedToWin()) so the logic is identical but
uses only integer arithmetic.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 8, 2026
When numTilesWithoutFallout === 0, the tile-percentage division
produces Infinity, and Infinity > 80 is true in JS — falsely
declaring a winner.

Use an early return guard (numTilesWithoutFallout === 0) to avoid
the division entirely.

Added tests for both FFA and Team modes.
More defensive check: while numTilesWithFallout shouldn't exceed
numLandTiles, using <= 0 guards against any edge case where it
might, preventing both division-by-zero and negative-denominator
bugs in both FFA and Team win checks.
@berkelmali berkelmali force-pushed the fix/wincheck-divzero-fallout branch from b434170 to c3477f2 Compare May 16, 2026 14:18
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.

2 participants