mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 17:56:43 +00:00
097c42740c
## Description: Fixing https://discord.com/channels/1359946986937258015/1360078040222142564/1463898386854973642 Now, if not all tiles on the spawn circle can be owned, the algorithm tries to select another random spawn tile. ## 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: nikolaj_mykola --------- Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
282 lines
7.4 KiB
TypeScript
282 lines
7.4 KiB
TypeScript
import { NukeMagnitude } from "../configuration/Config";
|
|
import { Game, Player, StructureTypes } from "../game/Game";
|
|
import { euclDistFN, GameMap, TileRef } from "../game/GameMap";
|
|
import { GameView } from "../game/GameView";
|
|
|
|
export interface NukeBlastParams {
|
|
gm: GameMap;
|
|
targetTile: TileRef;
|
|
magnitude: NukeMagnitude;
|
|
}
|
|
|
|
/**
|
|
* Counts how many tiles each player has in the nuke's blast zone.
|
|
*
|
|
* returns Map of player ID and weighted tile count
|
|
*/
|
|
export function computeNukeBlastCounts(
|
|
params: NukeBlastParams,
|
|
): Map<number, number> {
|
|
const { gm, targetTile, magnitude } = params;
|
|
|
|
const inner2 = magnitude.inner * magnitude.inner;
|
|
const counts = new Map<number, number>();
|
|
|
|
gm.circleSearch(targetTile, magnitude.outer, (tile: TileRef, d2: number) => {
|
|
const ownerSmallId = gm.ownerID(tile);
|
|
if (ownerSmallId > 0) {
|
|
const weight = d2 <= inner2 ? 1 : 0.5;
|
|
const prev = counts.get(ownerSmallId) ?? 0;
|
|
counts.set(ownerSmallId, prev + weight);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return counts;
|
|
}
|
|
|
|
export interface NukeAllianceCheckParams {
|
|
game: Game | GameView;
|
|
targetTile: TileRef;
|
|
magnitude: NukeMagnitude;
|
|
allySmallIds: Set<number>;
|
|
threshold: number;
|
|
}
|
|
|
|
// Checks if nuking this tile would break an alliance.
|
|
// Returns true if either:
|
|
// 1. The weighted tile count for any ally exceeds the threshold
|
|
// 2. Any allied structure would be destroyed
|
|
export function wouldNukeBreakAlliance(
|
|
params: NukeAllianceCheckParams,
|
|
): boolean {
|
|
const { game, targetTile, magnitude, allySmallIds, threshold } = params;
|
|
|
|
if (allySmallIds.size === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check if any allied structure would be destroyed
|
|
const wouldDestroyAlliedStructure = game.anyUnitNearby(
|
|
targetTile,
|
|
magnitude.outer,
|
|
StructureTypes,
|
|
(unit) =>
|
|
unit.owner().isPlayer() && allySmallIds.has(unit.owner().smallID()),
|
|
);
|
|
if (wouldDestroyAlliedStructure) return true;
|
|
|
|
const inner2 = magnitude.inner * magnitude.inner;
|
|
const allyTileCounts = new Map<number, number>();
|
|
|
|
let result = false;
|
|
|
|
game.circleSearch(
|
|
targetTile,
|
|
magnitude.outer,
|
|
(tile: TileRef, d2: number) => {
|
|
const ownerSmallId = game.ownerID(tile);
|
|
if (ownerSmallId > 0 && allySmallIds.has(ownerSmallId)) {
|
|
const weight = d2 <= inner2 ? 1 : 0.5;
|
|
const newCount = (allyTileCounts.get(ownerSmallId) ?? 0) + weight;
|
|
allyTileCounts.set(ownerSmallId, newCount);
|
|
|
|
if (newCount > threshold) {
|
|
result = true;
|
|
return false; // Found one! Stop searching.
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
// Same as wouldNukeBreakAlliance(), but takes time to find every player
|
|
// that would be "angered" from this nuke.
|
|
// This includes unallied players!
|
|
export function listNukeBreakAlliance(
|
|
params: NukeAllianceCheckParams,
|
|
): Set<number> {
|
|
const { game, targetTile, magnitude, threshold } = params;
|
|
|
|
// Collect all players that should have alliance broken:
|
|
// either exceeds tile threshold OR has a structure in blast radius
|
|
const playersToBreakAllianceWith = new Set<number>();
|
|
|
|
// compute tile breakage threshold
|
|
const blastCounts = computeNukeBlastCounts({
|
|
gm: game,
|
|
targetTile,
|
|
magnitude,
|
|
});
|
|
for (const [playerSmallId, totalWeight] of blastCounts) {
|
|
if (totalWeight > threshold) {
|
|
playersToBreakAllianceWith.add(playerSmallId);
|
|
}
|
|
}
|
|
|
|
// Also check if any allied structures would be destroyed
|
|
game
|
|
.nearbyUnits(targetTile, magnitude.outer, StructureTypes)
|
|
.forEach(({ unit }) =>
|
|
playersToBreakAllianceWith.add(unit.owner().smallID()),
|
|
);
|
|
|
|
return playersToBreakAllianceWith;
|
|
}
|
|
export function getSpawnTiles(
|
|
gm: GameMap,
|
|
tile: TileRef,
|
|
requireAllValid: true,
|
|
): TileRef[] | null;
|
|
export function getSpawnTiles(
|
|
gm: GameMap,
|
|
tile: TileRef,
|
|
requireAllValid?: false,
|
|
): TileRef[];
|
|
export function getSpawnTiles(
|
|
gm: GameMap,
|
|
tile: TileRef,
|
|
requireAllValid = false,
|
|
): TileRef[] | null {
|
|
const spawnTiles = Array.from(gm.bfs(tile, euclDistFN(tile, 4, true)));
|
|
|
|
const isInvalid = (t: TileRef) => gm.hasOwner(t) || !gm.isLand(t);
|
|
|
|
if (!requireAllValid) {
|
|
return spawnTiles.filter((t) => !isInvalid(t));
|
|
}
|
|
|
|
if (spawnTiles.some(isInvalid)) {
|
|
return null;
|
|
}
|
|
|
|
return spawnTiles;
|
|
}
|
|
|
|
export function closestTile(
|
|
gm: GameMap,
|
|
refs: Iterable<TileRef>,
|
|
tile: TileRef,
|
|
): [TileRef | null, number] {
|
|
let minDistance = Infinity;
|
|
let minRef: TileRef | null = null;
|
|
for (const ref of refs) {
|
|
const distance = gm.manhattanDist(ref, tile);
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
minRef = ref;
|
|
}
|
|
}
|
|
return [minRef, minDistance];
|
|
}
|
|
|
|
export function closestTwoTiles(
|
|
gm: GameMap,
|
|
x: Iterable<TileRef>,
|
|
y: Iterable<TileRef>,
|
|
): { x: TileRef; y: TileRef } | null {
|
|
const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b));
|
|
const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b));
|
|
|
|
if (xSorted.length === 0 || ySorted.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
let i = 0;
|
|
let j = 0;
|
|
let minDistance = Infinity;
|
|
let result = { x: xSorted[0], y: ySorted[0] };
|
|
|
|
while (i < xSorted.length && j < ySorted.length) {
|
|
const currentX = xSorted[i];
|
|
const currentY = ySorted[j];
|
|
|
|
const distance =
|
|
Math.abs(gm.x(currentX) - gm.x(currentY)) +
|
|
Math.abs(gm.y(currentX) - gm.y(currentY));
|
|
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
result = { x: currentX, y: currentY };
|
|
}
|
|
|
|
// If we're at the end of X, must move Y forward
|
|
if (i === xSorted.length - 1) {
|
|
j++;
|
|
}
|
|
// If we're at the end of Y, must move X forward
|
|
else if (j === ySorted.length - 1) {
|
|
i++;
|
|
}
|
|
// Otherwise, move whichever pointer has smaller x value
|
|
else if (gm.x(currentX) < gm.x(currentY)) {
|
|
i++;
|
|
} else {
|
|
j++;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculates the center of a player's territory using geometric approach.
|
|
* Uses the bounding box center and verifies ownership, falling back to nearest border tile if necessary.
|
|
*
|
|
* @param game - The game instance
|
|
* @param target - The player whose territory center to calculate
|
|
* @returns The tile reference for the territory center, or null if no valid center found
|
|
*/
|
|
export function calculateTerritoryCenter(
|
|
game: Game,
|
|
target: Player,
|
|
): TileRef | null {
|
|
const borderTiles = target.borderTiles();
|
|
if (borderTiles.size === 0) return null;
|
|
|
|
// Calculate bounding box center in a single pass through border tiles
|
|
let minX = Infinity,
|
|
maxX = -Infinity;
|
|
let minY = Infinity,
|
|
maxY = -Infinity;
|
|
|
|
for (const tile of borderTiles) {
|
|
const x = game.x(tile);
|
|
const y = game.y(tile);
|
|
if (x < minX) minX = x;
|
|
if (x > maxX) maxX = x;
|
|
if (y < minY) minY = y;
|
|
if (y > maxY) maxY = y;
|
|
}
|
|
|
|
const centerX = Math.floor((minX + maxX) / 2);
|
|
const centerY = Math.floor((minY + maxY) / 2);
|
|
|
|
const centerTile = game.ref(centerX, centerY);
|
|
|
|
// Verify ownership of the center tile
|
|
if (game.owner(centerTile) === target) {
|
|
return centerTile;
|
|
}
|
|
|
|
// Fall back to nearest border tile if center is not owned
|
|
let closestTile: TileRef | null = null;
|
|
let closestDistanceSquared = Infinity;
|
|
|
|
for (const tile of borderTiles) {
|
|
const dx = game.x(tile) - centerX;
|
|
const dy = game.y(tile) - centerY;
|
|
const distSquared = dx * dx + dy * dy;
|
|
|
|
if (distSquared < closestDistanceSquared) {
|
|
closestDistanceSquared = distSquared;
|
|
closestTile = tile;
|
|
}
|
|
}
|
|
|
|
return closestTile;
|
|
}
|