From 2db29073255c2ed4c4d1e4a56d44ef5d2b461f05 Mon Sep 17 00:00:00 2001
From: Scott Anderson <662325+scottanderson@users.noreply.github.com>
Date: Mon, 18 Aug 2025 18:21:03 -0400
Subject: [PATCH] Smarter nation structure placement (#1851)
## Description:
Smarter nation structure placement.
Fixes #881
## 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
---
src/core/execution/FakeHumanExecution.ts | 86 +++++++++++++++++++++++-
1 file changed, 85 insertions(+), 1 deletion(-)
diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts
index 958ff8e6f..a550c2b19 100644
--- a/src/core/execution/FakeHumanExecution.ts
+++ b/src/core/execution/FakeHumanExecution.ts
@@ -458,7 +458,91 @@ export class FakeHumanExecution implements Execution {
)
: Array.from(this.player.tiles());
if (tiles.length === 0) return null;
- return this.random.randElement(tiles);
+ const valueFunction = this.structureSpawnTileValue(type);
+ let bestTile: TileRef | null = null;
+ let bestValue = 0;
+ const sampledTiles = this.arraySampler(tiles);
+ for (const t of sampledTiles) {
+ const v = valueFunction(t);
+ if (v <= bestValue && bestTile !== null) continue;
+ if (!this.player.canBuild(type, t)) continue;
+ // Found a better tile
+ bestTile = t;
+ bestValue = v;
+ }
+ return bestTile;
+ }
+
+ private * arraySampler(a: T[], sampleSize = 50): Generator {
+ if (a.length <= sampleSize) {
+ // Return all elements
+ yield* a;
+ } else {
+ // Sample `sampleSize` elements
+ const remaining = new Set(a);
+ while (sampleSize--) {
+ const t = this.random.randFromSet(remaining);
+ remaining.delete(t);
+ yield t;
+ }
+ }
+ }
+
+ private structureSpawnTileValue(type: UnitType): (tile: TileRef) => number {
+ if (this.player === null) throw new Error("not initialized");
+ const borderTiles = this.player.borderTiles();
+ const mg = this.mg;
+ const otherUnits = this.player.units(type);
+ // Prefer spacing structures out of atom bomb range
+ const borderSpacing = this.mg.config().nukeMagnitudes(UnitType.AtomBomb).outer;
+ const structureSpacing = borderSpacing * 2;
+ switch (type) {
+ case UnitType.Port:
+ return (tile) => {
+ let w = 0;
+
+ // Prefer to be far away from other structures of the same type
+ const otherTiles: Set = new Set(otherUnits.map((u) => u.tile()));
+ otherTiles.delete(tile);
+ const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
+ if (closestOther !== null) {
+ const d = mg.manhattanDist(closestOther.x, tile);
+ w += Math.min(d, structureSpacing);
+ }
+
+ return w;
+ };
+ case UnitType.City:
+ case UnitType.Factory:
+ case UnitType.MissileSilo:
+ return (tile) => {
+ let w = 0;
+
+ // Prefer higher elevations
+ w += mg.magnitude(tile);
+
+ // Prefer to be away from the border
+ const closestBorder = closestTwoTiles(mg, borderTiles, [tile]);
+ if (closestBorder !== null) {
+ const d = mg.manhattanDist(closestBorder.x, tile);
+ w += Math.min(d, borderSpacing);
+ }
+
+ // Prefer to be away from other structures of the same type
+ const otherTiles: Set = new Set(otherUnits.map((u) => u.tile()));
+ otherTiles.delete(tile);
+ const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
+ if (closestOther !== null) {
+ const d = mg.manhattanDist(closestOther.x, tile);
+ w += Math.min(d, structureSpacing);
+ }
+
+ // TODO: Cities and factories should consider train range limits
+ return w;
+ };
+ default:
+ throw new Error(`Value function not implemented for ${type}`);
+ }
}
private maybeSpawnWarship(): boolean {