From 38b1845ed16ce7713b169d86d7ff0c2aab946e0a Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 18 Apr 2025 11:51:54 -0700 Subject: [PATCH 001/108] 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. --- src/core/GameRunner.ts | 10 +-- src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 4 ++ src/core/execution/MissileSiloExecution.ts | 5 +- src/core/game/Game.ts | 1 + src/core/game/PlayerImpl.ts | 84 +++++++++++++++++++--- 6 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 13d8fcc10..c4b3ab3f7 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -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; diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 2fdf9a813..aaed33112 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -123,6 +123,7 @@ export interface Config { nukeMagnitudes(unitType: UnitType): NukeMagnitude; defaultNukeSpeed(): number; nukeDeathFactor(humans: number, tilesOwned: number): number; + structureMinDist(): number; } export interface Theme { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index e0c4d240c..9b51ae7ca 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -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; + } } diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts index 928b8afdf..fd9bf2111 100644 --- a/src/core/execution/MissileSiloExecution.ts +++ b/src/core/execution/MissileSiloExecution.ts @@ -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(), }); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index b82016ed9..10a10af3d 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -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, diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 7f2786183..3c1de6733 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -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 = 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 { From 582ebf3151accded972ec5ddfe85cca0484acce3 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 18 Apr 2025 13:31:50 -0700 Subject: [PATCH 002/108] rebalance multiplayer maps: 1. reduce number of players per map 2. Modify map frequencies --- src/core/configuration/DefaultConfig.ts | 12 ++++++------ src/server/MapPlaylist.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 9b51ae7ca..03726a83b 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -69,7 +69,7 @@ export abstract class DefaultServerConfig implements ServerConfig { GameMapType.Europe, ].includes(map) ) { - return Math.random() < 0.2 ? 150 : 70; + return Math.random() < 0.2 ? 100 : 50; } // Maps with ~2.5 - ~3.5 mil pixels if ( @@ -80,7 +80,7 @@ export abstract class DefaultServerConfig implements ServerConfig { GameMapType.Asia, ].includes(map) ) { - return Math.random() < 0.2 ? 100 : 50; + return Math.random() < 0.3 ? 50 : 25; } // Maps with ~2 mil pixels if ( @@ -92,7 +92,7 @@ export abstract class DefaultServerConfig implements ServerConfig { GameMapType.FaroeIslands, ].includes(map) ) { - return Math.random() < 0.2 ? 70 : 40; + return Math.random() < 0.3 ? 50 : 25; } // Maps smaller than ~2 mil pixels if ( @@ -102,14 +102,14 @@ export abstract class DefaultServerConfig implements ServerConfig { GameMapType.Pangaea, ].includes(map) ) { - return Math.random() < 0.2 ? 60 : 35; + return Math.random() < 0.5 ? 30 : 15; } // world belongs with the ~2 mils, but these amounts never made sense so I assume the insanity is intended. if (map == GameMapType.World) { - return Math.random() < 0.2 ? 150 : 60; + return Math.random() < 0.2 ? 150 : 50; } // default return for non specified map - return Math.random() < 0.2 ? 85 : 45; + return Math.random() < 0.2 ? 50 : 20; } workerIndex(gameID: GameID): number { return simpleHash(gameID) % this.numWorkers(); diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index d26852460..91beb160a 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -81,25 +81,25 @@ export class MapPlaylist { // Big Maps are those larger than ~2.5 mil pixels case PlaylistType.BigMaps: return { - Europe: 3, - NorthAmerica: 2, + Europe: 2, + NorthAmerica: 1, Africa: 2, Britannia: 1, GatewayToTheAtlantic: 2, Australia: 2, Iceland: 2, - SouthAmerica: 3, + SouthAmerica: 1, KnownWorld: 2, }; case PlaylistType.SmallMaps: return { - World: 1, + World: 4, Mena: 2, Pangaea: 1, Asia: 1, Mars: 1, - BetweenTwoSeas: 3, - Japan: 3, + BetweenTwoSeas: 2, + Japan: 2, BlackSea: 1, FaroeIslands: 2, }; From 11a9b9e5b0ac9777e712548184c8a2c249fcaff2 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 18 Apr 2025 13:46:50 -0700 Subject: [PATCH 003/108] move "join discord button" to footer, this makes space for the login button --- src/client/index.html | 18 ++++-------------- src/client/styles/layout/footer.css | 2 +- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/client/index.html b/src/client/index.html index f0b8810f3..0de10d117 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -215,20 +215,7 @@ -
- - Discord - Join the Discord! - -
+
@@ -331,6 +318,9 @@ > Wiki + + Join the Discord! +