Prevent AI from placing ports on small lakes 🚢 (#4429)

## Description:

AI nations were placing ports on small decorative ponds scattered across
maps (Missisipi for example), wasting structure slots on strategically
useless water bodies. This fix adds a water component size check to the
port placement logic so the AI skips lakes that are too small for
meaningful port use. We already had a check for available trade
partners, but trading in small lakes is usually stupid.

**How it works:**
- `ConnectedComponents` now tracks component sizes during its existing
flood-fill (zero extra cost - counts tiles as they're visited)
- `AbstractGraph`, `WaterManager`, and the `Game` interface expose
`getWaterComponentSize(tile)` so callers can query the size of any water
body
- `NationStructureBehavior.randCoastalTileArray()` filters out non-ocean
water components below `MIN_PORT_WATER_COMPONENT_SIZE` (3000 minimap
tiles, ~12000 full-map tiles)
- Ocean tiles bypass the check entirely since they're always large
enough

## 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

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin
This commit is contained in:
FloPinguin
2026-06-28 19:19:58 -07:00
committed by GitHub
parent 06d505ebc9
commit ccd0745ad4
7 changed files with 142 additions and 19 deletions
+6
View File
@@ -799,6 +799,12 @@ export interface Game extends GameMap {
miniWaterGraph(): AbstractGraph | null;
getWaterComponent(tile: TileRef): number | null;
hasWaterComponent(tile: TileRef, component: number): boolean;
/**
* Returns the approximate number of water tiles in the component
* containing `tile`, or null if the tile has no water component. Useful for
* filtering tiny water bodies (e.g. preventing AI port placement on ponds).
*/
getWaterComponentSize(tile: TileRef): number | null;
/**
* Returns the set of water components that `player` shares with at least one
* valid trade partner (cached). Used by nation AI for port-placement
+3
View File
@@ -1204,6 +1204,9 @@ export class GameImpl implements Game {
hasWaterComponent(tile: TileRef, component: number): boolean {
return this._waterManager.hasWaterComponent(tile, component);
}
getWaterComponentSize(tile: TileRef): number | null {
return this._waterManager.getWaterComponentSize(tile);
}
sharedWaterComponents(player: Player): Set<number> | null {
return this._sharedWaterCache.get(player);
}
+15
View File
@@ -182,6 +182,21 @@ export class WaterManager {
return false;
}
/**
* Returns the approximate number of water tiles in the component
* containing `tile`, or null if the tile has no water component.
*
* The underlying ConnectedComponents are computed on the 2× downsampled
* minimap, so each minimap tile represents up to 4 full-map tiles. We
* multiply by 4 to give callers a value in full-map-tile units.
*/
getWaterComponentSize(tile: TileRef): number | null {
const componentId = this.getWaterComponent(tile);
if (componentId === null) return null;
if (!this._miniWaterGraph) return 0;
return this._miniWaterGraph.getComponentSize(componentId) * 4;
}
private finalizeWaterChanges(
convertedTiles: TileRef[],
changedTiles: TileRef[],