From 3680d9cc1663a22f0e174d2c2de806c0ee78b923 Mon Sep 17 00:00:00 2001
From: Britton Fischer <55821122+nottirb@users.noreply.github.com>
Date: Wed, 8 Oct 2025 18:28:26 -0700
Subject: [PATCH] fix: allies cannot annex your clusters (#2158)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Description:
Fixes #1685
Continuation from #1924, which was auto-closed after the upstream repo
force-pushed main and I synced my fork.
This change ensures that allies are excluded from the `getMode()` call
made by `getCapturingPlayer()` inside `removeCluster()`.
- Previously, friendly neighbors were treated as candidates for
capturing, leading to incorrect annexations in certain edge cases.
- Added a small efficiency improvement by filtering out non-player and
friendly neighbors up front to reduce total computations down-the-line.
- Important: we can’t simply check if the `getMode(neighborsIDs)` result
is a friendly. Doing so would cause the territory to go to nobody until
the user is attacked. I believe the expected behavior is the largest
neighboring enemy should take it automatically.
Here's an example of the new behavior in an extreme edge case:
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] 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:
nottirb
---
src/core/Util.ts | 21 ------------
src/core/execution/PlayerExecution.ts | 47 +++++++++++++++------------
2 files changed, 26 insertions(+), 42 deletions(-)
diff --git a/src/core/Util.ts b/src/core/Util.ts
index d74d27b11..fc5e6c58d 100644
--- a/src/core/Util.ts
+++ b/src/core/Util.ts
@@ -114,27 +114,6 @@ export function inscribed(
);
}
-export function getMode(list: Set): number {
- // Count occurrences
- const counts = new Map();
- for (const item of list) {
- counts.set(item, (counts.get(item) ?? 0) + 1);
- }
-
- // Find the item with the highest count
- let mode = 0;
- let maxCount = 0;
-
- for (const [item, count] of counts) {
- if (count > maxCount) {
- maxCount = count;
- mode = item;
- }
- }
-
- return mode;
-}
-
export function sanitize(name: string): string {
return Array.from(name)
.join("")
diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts
index 66f5c6a57..32935eabb 100644
--- a/src/core/execution/PlayerExecution.ts
+++ b/src/core/execution/PlayerExecution.ts
@@ -2,7 +2,7 @@ import { Config } from "../configuration/Config";
import { Execution, Game, Player, UnitType } from "../game/Game";
import { GameImpl } from "../game/GameImpl";
import { GameMap, TileRef } from "../game/GameMap";
-import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
+import { calculateBoundingBox, inscribed, simpleHash } from "../Util";
export class PlayerExecution implements Execution {
private readonly ticksPerClusterCalc = 20;
@@ -214,22 +214,37 @@ export class PlayerExecution implements Execution {
}
private getCapturingPlayer(cluster: Set): Player | null {
- const neighborsIDs = new Set();
+ // Collect unique neighbor IDs (excluding self) as candidates
+ const candidatesIDs = new Set();
+ const selfID = this.player.smallID();
+
for (const t of cluster) {
for (const neighbor of this.mg.neighbors(t)) {
- if (this.mg.ownerID(neighbor) !== this.player.smallID()) {
- neighborsIDs.add(this.mg.ownerID(neighbor));
+ if (this.mg.ownerID(neighbor) !== selfID) {
+ candidatesIDs.add(this.mg.ownerID(neighbor));
}
}
}
- let largestNeighborAttack: Player | null = null;
- let largestTroopCount: number = 0;
- for (const id of neighborsIDs) {
+ // Filter out friendly and non-player candidates
+ const neighbors = new Set();
+ for (const id of candidatesIDs) {
const neighbor = this.mg.playerBySmallID(id);
- if (!neighbor.isPlayer() || this.player.isFriendly(neighbor)) {
+ if (!neighbor.isPlayer() || neighbor.isFriendly(this.player)) {
continue;
}
+ neighbors.add(neighbor);
+ }
+
+ // If there are no enemies, return null
+ if (neighbors.size === 0) {
+ return null;
+ }
+
+ // Get the largest attack from the neighbors
+ let largestNeighborAttack: Player | null = null;
+ let largestTroopCount = 0;
+ for (const neighbor of neighbors) {
for (const attack of neighbor.outgoingAttacks()) {
if (attack.target() === this.player) {
if (attack.troops() > largestTroopCount) {
@@ -239,20 +254,10 @@ export class PlayerExecution implements Execution {
}
}
}
- if (largestNeighborAttack !== null) {
- return largestNeighborAttack;
- }
- // fall back to getting mode if no attacks
- const mode = getMode(neighborsIDs);
- if (!this.mg.playerBySmallID(mode).isPlayer()) {
- return null;
- }
- const capturing = this.mg.playerBySmallID(mode);
- if (!capturing.isPlayer()) {
- return null;
- }
- return capturing;
+ // Return the largest neighbor attack
+ // If there is no largest neighbor attack, this will return null
+ return largestNeighborAttack;
}
private calculateClusters(): Set[] {