mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 22:51:57 +00:00
fix: allies cannot annex your clusters (#2158)
## 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: <img width="622" height="422" alt="Screenshot 2025-08-24 at 4 56 46 PM" src="https://github.com/user-attachments/assets/c5dd9c0d-0a3c-4657-8154-e114fa920689" /> ## 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
This commit is contained in:
@@ -114,27 +114,6 @@ export function inscribed(
|
||||
);
|
||||
}
|
||||
|
||||
export function getMode(list: Set<number>): number {
|
||||
// Count occurrences
|
||||
const counts = new Map<number, number>();
|
||||
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("")
|
||||
|
||||
@@ -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<TileRef>): Player | null {
|
||||
const neighborsIDs = new Set<number>();
|
||||
// Collect unique neighbor IDs (excluding self) as candidates
|
||||
const candidatesIDs = new Set<number>();
|
||||
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<Player>();
|
||||
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<TileRef>[] {
|
||||
|
||||
Reference in New Issue
Block a user