don't allow structures to spawn too close to each other. When choosing a spawn, canBuild() finds a suitable nearby tile if chosen tile is too close to an existing structure.

This commit is contained in:
Evan
2025-04-18 11:51:54 -07:00
parent ae4f4d3ed6
commit 38b1845ed1
6 changed files with 84 additions and 21 deletions
+1 -9
View File
@@ -4,7 +4,6 @@ import { Executor } from "./execution/ExecutionManager";
import { WinCheckExecution } from "./execution/WinCheckExecution";
import {
AllPlayers,
BuildableUnit,
Game,
GameUpdates,
NameViewData,
@@ -15,7 +14,6 @@ import {
PlayerInfo,
PlayerProfile,
PlayerType,
UnitType,
} from "./game/Game";
import { createGame } from "./game/GameImpl";
import {
@@ -161,13 +159,7 @@ export class GameRunner {
const actions = {
canBoat: player.canBoat(tile),
canAttack: player.canAttack(tile),
buildableUnits: Object.values(UnitType).map((u) => {
return {
type: u,
canBuild: player.canBuild(u, tile) != false,
cost: this.game.config().unitInfo(u).cost(player),
} as BuildableUnit;
}),
buildableUnits: player.buildableUnits(tile),
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
} as PlayerActions;
+1
View File
@@ -123,6 +123,7 @@ export interface Config {
nukeMagnitudes(unitType: UnitType): NukeMagnitude;
defaultNukeSpeed(): number;
nukeDeathFactor(humans: number, tilesOwned: number): number;
structureMinDist(): number;
}
export interface Theme {
+4
View File
@@ -676,4 +676,8 @@ export class DefaultConfig implements Config {
nukeDeathFactor(humans: number, tilesOwned: number): number {
return (5 * humans) / Math.max(1, tilesOwned);
}
structureMinDist(): number {
return 18;
}
}
+3 -2
View File
@@ -33,14 +33,15 @@ export class MissileSiloExecution implements Execution {
tick(ticks: number): void {
if (this.silo == null) {
if (!this.player.canBuild(UnitType.MissileSilo, this.tile)) {
const spawn = this.player.canBuild(UnitType.MissileSilo, this.tile);
if (spawn === false) {
consolex.warn(
`player ${this.player} cannot build missile silo at ${this.tile}`,
);
this.active = false;
return;
}
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile, {
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, spawn, {
cooldownDuration: this.mg.config().SiloCooldown(),
});
+1
View File
@@ -347,6 +347,7 @@ export interface Player {
// Units
units(...types: UnitType[]): Unit[];
unitsIncludingConstruction(type: UnitType): Unit[];
buildableUnits(tile: TileRef): BuildableUnit[];
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
buildUnit(
type: UnitType,
+74 -10
View File
@@ -20,6 +20,7 @@ import {
AllianceRequest,
AllPlayers,
Attack,
BuildableUnit,
Cell,
EmojiMessage,
GameMode,
@@ -729,7 +730,22 @@ export class PlayerImpl implements Player {
return b;
}
canBuild(unitType: UnitType, targetTile: TileRef): TileRef | false {
public buildableUnits(tile: TileRef): BuildableUnit[] {
const validTiles = this.validStructureSpawnTiles(tile);
return Object.values(UnitType).map((u) => {
return {
type: u,
canBuild: this.canBuild(u, tile, validTiles) != false,
cost: this.mg.config().unitInfo(u).cost(this),
} as BuildableUnit;
});
}
canBuild(
unitType: UnitType,
targetTile: TileRef,
validTiles: TileRef[] | null = null,
): TileRef | false {
// prevent the building of nukes and nuke related buildings
if (this.mg.config().disableNukes()) {
if (
@@ -761,7 +777,7 @@ export class PlayerImpl implements Player {
case UnitType.MIRVWarhead:
return targetTile;
case UnitType.Port:
return this.portSpawn(targetTile);
return this.portSpawn(targetTile, validTiles);
case UnitType.Warship:
return this.warshipSpawn(targetTile);
case UnitType.Shell:
@@ -776,7 +792,7 @@ export class PlayerImpl implements Player {
case UnitType.SAMLauncher:
case UnitType.City:
case UnitType.Construction:
return this.landBasedStructureSpawn(targetTile);
return this.landBasedStructureSpawn(targetTile, validTiles);
default:
assertNever(unitType);
}
@@ -802,7 +818,7 @@ export class PlayerImpl implements Player {
return spawns[0].tile();
}
portSpawn(tile: TileRef): TileRef | false {
portSpawn(tile: TileRef, validTiles: TileRef[]): TileRef | false {
const spawns = Array.from(
this.mg.bfs(
tile,
@@ -814,10 +830,15 @@ export class PlayerImpl implements Player {
(a, b) =>
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile),
);
if (spawns.length == 0) {
return false;
const validTileSet = new Set(
validTiles ?? this.validStructureSpawnTiles(tile),
);
for (const t of spawns) {
if (validTileSet.has(t)) {
return t;
}
}
return spawns[0];
return false;
}
warshipSpawn(tile: TileRef): TileRef | false {
@@ -835,11 +856,54 @@ export class PlayerImpl implements Player {
return spawns[0].tile();
}
landBasedStructureSpawn(tile: TileRef): TileRef | false {
if (this.mg.owner(tile) != this) {
landBasedStructureSpawn(
tile: TileRef,
validTiles: TileRef[] | null = null,
): TileRef | false {
const tiles = validTiles ?? this.validStructureSpawnTiles(tile);
if (tiles.length == 0) {
return false;
}
return tile;
return tiles[0];
}
private validStructureSpawnTiles(tile: TileRef): TileRef[] {
if (this.mg.owner(tile) != this) {
return [];
}
const searchRadius = 15;
const searchRadiusSquared = searchRadius ** 2;
const types = Object.values(UnitType).filter((unitTypeValue) => {
return this.mg.config().unitInfo(unitTypeValue).territoryBound;
});
const nearbyUnits = this.mg
.nearbyUnits(tile, searchRadius * 2, types)
.map((u) => u.unit);
const nearbyTiles = this.mg.bfs(tile, (gm, t) => {
return (
this.mg.euclideanDistSquared(tile, t) < searchRadiusSquared &&
gm.ownerID(t) == this.smallID()
);
});
const validSet: Set<TileRef> = new Set(nearbyTiles);
const minDistSquared = this.mg.config().structureMinDist() ** 2;
for (const t of nearbyTiles) {
for (const unit of nearbyUnits) {
if (this.mg.euclideanDistSquared(unit.tile(), t) < minDistSquared) {
validSet.delete(t);
break;
}
}
}
const valid = Array.from(validSet);
valid.sort(
(a, b) =>
this.mg.euclideanDistSquared(a, tile) -
this.mg.euclideanDistSquared(b, tile),
);
return valid;
}
transportShipSpawn(targetTile: TileRef): TileRef | false {