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
This commit is contained in:
Evan
2025-10-13 19:54:05 -07:00
committed by GitHub
parent 136cfa1316
commit b58d140f94
2 changed files with 30 additions and 21 deletions
+14
View File
@@ -129,6 +129,20 @@ export function boundingBoxTiles(
return tiles;
}
export function getMode<T>(counts: Map<T, number>): 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<TileRef>,
+16 -21
View File
@@ -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<TileRef>): Player | null {
// Collect unique neighbor IDs (excluding self) as candidates
const candidatesIDs = new Set<number>();
const selfID = this.player.smallID();
const neighbors = new Map<Player, number>();
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<Player>();
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<TileRef>[] {