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 {