From b58d140f9458bb34a30d9389f4d4ad8df5bae238 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 13 Oct 2025 19:54:05 -0700 Subject: [PATCH] fix encirclement issues (#2191) ## Description: Players with no ongoing attacks were ignored during cluster calculations in https://github.com/openfrontio/OpenFrontIO/commit/3680d9cc1663a22f0e174d2c2de806c0ee78b923 This PR has it fallback to neighbor with largest border if no ongoing attacks ## 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: evan --- src/core/Util.ts | 14 ++++++++++ src/core/execution/PlayerExecution.ts | 37 ++++++++++++--------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/core/Util.ts b/src/core/Util.ts index 69317accf..c5cbe044e 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -129,6 +129,20 @@ export function boundingBoxTiles( return tiles; } +export function getMode(counts: Map): T | null { + let mode: T | null = null; + let maxCount = 0; + + for (const [item, count] of counts) { + if (count > maxCount) { + maxCount = count; + mode = item; + } + } + + return mode; +} + export function calculateBoundingBoxCenter( gm: GameMap, borderTiles: ReadonlySet, diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index fed23323d..66e10c854 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, inscribed, simpleHash } from "../Util"; +import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util"; export class PlayerExecution implements Execution { private readonly ticksPerClusterCalc = 20; @@ -221,28 +221,20 @@ export class PlayerExecution implements Execution { } private getCapturingPlayer(cluster: Set): Player | null { - // Collect unique neighbor IDs (excluding self) as candidates - const candidatesIDs = new Set(); - const selfID = this.player.smallID(); - + const neighbors = new Map(); for (const t of cluster) { for (const neighbor of this.mg.neighbors(t)) { - if (this.mg.ownerID(neighbor) !== selfID) { - candidatesIDs.add(this.mg.ownerID(neighbor)); + const owner = this.mg.owner(neighbor); + if ( + owner.isPlayer() && + owner !== this.player && + !owner.isFriendly(this.player) + ) { + neighbors.set(owner, (neighbors.get(owner) ?? 0) + 1); } } } - // 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() || neighbor.isFriendly(this.player)) { - continue; - } - neighbors.add(neighbor); - } - // If there are no enemies, return null if (neighbors.size === 0) { return null; @@ -251,7 +243,7 @@ export class PlayerExecution implements Execution { // Get the largest attack from the neighbors let largestNeighborAttack: Player | null = null; let largestTroopCount = 0; - for (const neighbor of neighbors) { + for (const [neighbor] of neighbors) { for (const attack of neighbor.outgoingAttacks()) { if (attack.target() === this.player) { if (attack.troops() > largestTroopCount) { @@ -262,9 +254,12 @@ export class PlayerExecution implements Execution { } } - // Return the largest neighbor attack - // If there is no largest neighbor attack, this will return null - return largestNeighborAttack; + if (largestNeighborAttack !== null) { + return largestNeighborAttack; + } + + // There are no ongoing attacks, so find the enemy with the largest border. + return getMode(neighbors); } private calculateClusters(): Set[] {