From e4a4c5dc12ebafa327ddf15a65c8fadd1a815510 Mon Sep 17 00:00:00 2001
From: Scott Anderson <662325+scottanderson@users.noreply.github.com>
Date: Mon, 25 Aug 2025 18:07:57 -0400
Subject: [PATCH] Nations build SAM launchers (#1931)
## Description:
Fixes #201 by adding the ability for nations to build SAM launchers.
## 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 | 1 +
src/core/execution/SAMLauncherExecution.ts | 3 +-
.../nation/structureSpawnTileValue.ts | 79 +++++++++++++++----
3 files changed, 66 insertions(+), 17 deletions(-)
diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts
index d55161654..083f23e6a 100644
--- a/src/core/execution/FakeHumanExecution.ts
+++ b/src/core/execution/FakeHumanExecution.ts
@@ -437,6 +437,7 @@ export class FakeHumanExecution implements Execution {
this.maybeSpawnStructure(UnitType.Port) ||
this.maybeSpawnWarship() ||
this.maybeSpawnStructure(UnitType.Factory) ||
+ this.maybeSpawnStructure(UnitType.SAMLauncher) ||
this.maybeSpawnStructure(UnitType.MissileSilo)
);
}
diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts
index 0c444ef91..c1b3f71c8 100644
--- a/src/core/execution/SAMLauncherExecution.ts
+++ b/src/core/execution/SAMLauncherExecution.ts
@@ -50,7 +50,8 @@ class SAMTargetingSystem {
private isInRange(tile: TileRef) {
const samTile = this.sam.tile();
- const rangeSquared = this.mg.config().defaultSamRange() ** 2;
+ const range = this.mg.config().defaultSamRange();
+ const rangeSquared = range * range;
return this.mg.euclideanDistSquared(samTile, tile) <= rangeSquared;
}
diff --git a/src/core/execution/nation/structureSpawnTileValue.ts b/src/core/execution/nation/structureSpawnTileValue.ts
index 563f42f80..55e1cfa90 100644
--- a/src/core/execution/nation/structureSpawnTileValue.ts
+++ b/src/core/execution/nation/structureSpawnTileValue.ts
@@ -13,24 +13,9 @@ export function structureSpawnTileValue(
const borderSpacing = 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:
+ case UnitType.MissileSilo: {
return (tile) => {
let w = 0;
@@ -56,6 +41,68 @@ export function structureSpawnTileValue(
// TODO: Cities and factories should consider train range limits
return w;
};
+ }
+ 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.SAMLauncher: {
+ const protectTiles: Set = new Set();
+ for (const unit of player.units()) {
+ switch(unit.type()) {
+ case UnitType.City:
+ case UnitType.Factory:
+ case UnitType.MissileSilo:
+ case UnitType.Port:
+ protectTiles.add(unit.tile());
+ }
+ }
+ const range = mg.config().defaultSamRange();
+ const rangeSquared = range * range;
+ 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 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);
+ }
+
+ // Prefer to be in range of other structures
+ for (const maybeProtected of protectTiles) {
+ const distanceSquared = mg.euclideanDistSquared(tile, maybeProtected);
+ if (distanceSquared > rangeSquared) continue;
+ w += structureSpacing;
+ }
+
+ return w;
+ };
+ }
default:
throw new Error(`Value function not implemented for ${type}`);
}