From 9ada16a2d38634fb61eeca6fd37eddc08a734484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berk=20Elmal=C4=B1?= Date: Thu, 6 Aug 2026 03:52:34 +0300 Subject: [PATCH 1/2] fix(core): guard against division by zero on 100% fallout coverage in WinCheckExecution --- src/core/execution/WinCheckExecution.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/core/execution/WinCheckExecution.ts b/src/core/execution/WinCheckExecution.ts index 0cb8c1dabc..fb25ba8cab 100644 --- a/src/core/execution/WinCheckExecution.ts +++ b/src/core/execution/WinCheckExecution.ts @@ -104,8 +104,10 @@ export class WinCheckExecution implements Execution { const max = sorted[0]; const timeElapsed = this.mg.elapsedGameSeconds(); - const numTilesWithoutFallout = - this.mg.numLandTiles() - this.mg.numTilesWithFallout(); + const numTilesWithoutFallout = Math.max( + 1, + this.mg.numLandTiles() - this.mg.numTilesWithFallout(), + ); if ( (max.numTilesOwned() / numTilesWithoutFallout) * 100 > this.mg.config().percentageTilesOwnedToWin() || @@ -162,8 +164,10 @@ export class WinCheckExecution implements Execution { } const max = sorted[0]; const timeElapsed = this.mg.elapsedGameSeconds(); - const numTilesWithoutFallout = - this.mg.numLandTiles() - this.mg.numTilesWithFallout(); + const numTilesWithoutFallout = Math.max( + 1, + this.mg.numLandTiles() - this.mg.numTilesWithFallout(), + ); const percentage = (max[1] / numTilesWithoutFallout) * 100; if ( percentage > this.mg.config().percentageTilesOwnedToWin() || From e6663c9bae1834fc72b0e8142760275cb5001e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berk=20Elmal=C4=B1?= Date: Thu, 6 Aug 2026 04:00:29 +0300 Subject: [PATCH 2/2] fix(core): guard territory win check on positive non-fallout tiles count --- src/core/execution/WinCheckExecution.ts | 26 ++++++++------- .../core/executions/WinCheckExecution.test.ts | 32 +++++++++++++++++++ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/core/execution/WinCheckExecution.ts b/src/core/execution/WinCheckExecution.ts index fb25ba8cab..8e50bd3c87 100644 --- a/src/core/execution/WinCheckExecution.ts +++ b/src/core/execution/WinCheckExecution.ts @@ -104,13 +104,14 @@ export class WinCheckExecution implements Execution { const max = sorted[0]; const timeElapsed = this.mg.elapsedGameSeconds(); - const numTilesWithoutFallout = Math.max( - 1, - this.mg.numLandTiles() - this.mg.numTilesWithFallout(), - ); - if ( + const numTilesWithoutFallout = + this.mg.numLandTiles() - this.mg.numTilesWithFallout(); + const isTerritoryWin = + numTilesWithoutFallout > 0 && (max.numTilesOwned() / numTilesWithoutFallout) * 100 > - this.mg.config().percentageTilesOwnedToWin() || + this.mg.config().percentageTilesOwnedToWin(); + if ( + isTerritoryWin || (this.mg.config().gameConfig().maxTimerValue !== undefined && timeElapsed - this.mg.config().gameConfig().maxTimerValue! * 60 >= 0) || timeElapsed >= WinCheckExecution.HARD_TIME_LIMIT_SECONDS @@ -164,13 +165,14 @@ export class WinCheckExecution implements Execution { } const max = sorted[0]; const timeElapsed = this.mg.elapsedGameSeconds(); - const numTilesWithoutFallout = Math.max( - 1, - this.mg.numLandTiles() - this.mg.numTilesWithFallout(), - ); - const percentage = (max[1] / numTilesWithoutFallout) * 100; + const numTilesWithoutFallout = + this.mg.numLandTiles() - this.mg.numTilesWithFallout(); + const isTerritoryWin = + numTilesWithoutFallout > 0 && + (max[1] / numTilesWithoutFallout) * 100 > + this.mg.config().percentageTilesOwnedToWin(); if ( - percentage > this.mg.config().percentageTilesOwnedToWin() || + isTerritoryWin || (this.mg.config().gameConfig().maxTimerValue !== undefined && timeElapsed - this.mg.config().gameConfig().maxTimerValue! * 60 >= 0) || timeElapsed >= WinCheckExecution.HARD_TIME_LIMIT_SECONDS diff --git a/tests/core/executions/WinCheckExecution.test.ts b/tests/core/executions/WinCheckExecution.test.ts index 798d1d5f99..669f6c3593 100644 --- a/tests/core/executions/WinCheckExecution.test.ts +++ b/tests/core/executions/WinCheckExecution.test.ts @@ -82,6 +82,38 @@ describe("WinCheckExecution", () => { expect(mg.setWinner).not.toHaveBeenCalled(); }); + it("should not set territory winner in FFA when non-fallout tiles is zero", () => { + const player = { + numTilesOwned: vi.fn(() => 10), + name: vi.fn(() => "P1"), + }; + mg.players = vi.fn(() => [player]); + mg.numLandTiles = vi.fn(() => 100); + mg.numTilesWithFallout = vi.fn(() => 100); + winCheck.checkWinnerFFA(); + expect(mg.setWinner).not.toHaveBeenCalled(); + }); + + it("should not set territory winner in Team mode when non-fallout tiles is zero", () => { + mg.config = vi.fn(() => ({ + gameConfig: vi.fn(() => ({ + gameMode: GameMode.Team, + })), + percentageTilesOwnedToWin: vi.fn(() => 50), + })); + const player = { + numTilesOwned: vi.fn(() => 10), + team: vi.fn(() => ColoredTeams.Red), + name: vi.fn(() => "P1"), + }; + mg.players = vi.fn(() => [player]); + mg.numLandTiles = vi.fn(() => 100); + mg.numTilesWithFallout = vi.fn(() => 100); + winCheck.init(mg, 0); + winCheck.checkWinnerTeam(); + expect(mg.setWinner).not.toHaveBeenCalled(); + }); + it("should return false for activeDuringSpawnPhase", () => { expect(winCheck.activeDuringSpawnPhase()).toBe(false); });