From c109d23f9f7f523b076233b2b0b182f907d2eede Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 2 Feb 2025 14:31:30 -0800 Subject: [PATCH] combine battleship + destroyer => warship. --- .../graphics/layers/PlayerInfoOverlay.ts | 6 +- src/client/graphics/layers/UnitLayer.ts | 96 +++------- .../graphics/layers/radial/BuildMenu.ts | 15 +- src/core/configuration/DefaultConfig.ts | 26 +-- src/core/configuration/DevConfig.ts | 4 + src/core/execution/BattleshipExecution.ts | 173 ------------------ src/core/execution/ExecutionManager.ts | 49 ++--- src/core/execution/FakeHumanExecution.ts | 31 +--- src/core/execution/PortExecution.ts | 15 +- ...troyerExecution.ts => WarshipExecution.ts} | 108 ++++++----- src/core/game/Game.ts | 3 +- src/core/game/GameView.ts | 26 +-- src/core/game/PlayerImpl.ts | 3 +- src/core/game/UnitImpl.ts | 7 +- 14 files changed, 159 insertions(+), 403 deletions(-) delete mode 100644 src/core/execution/BattleshipExecution.ts rename src/core/execution/{DestroyerExecution.ts => WarshipExecution.ts} (57%) diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index c1e8b1e37..4c66905df 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -22,7 +22,7 @@ import { renderNumber, renderTroops } from "../../Utils"; function euclideanDistWorld( coord: { x: number; y: number }, tileRef: TileRef, - game: GameView, + game: GameView ): number { const x = game.x(tileRef); const y = game.y(tileRef); @@ -71,7 +71,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { init() { this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) => - this.onMouseEvent(e), + this.onMouseEvent(e) ); this._isActive = true; } @@ -111,7 +111,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { this.setVisible(true); } else if (!this.game.isLand(tile)) { const units = this.game - .units(UnitType.Destroyer, UnitType.Battleship, UnitType.TradeShip) + .units(UnitType.Warship, UnitType.TradeShip, UnitType.TransportShip) .filter((u) => euclideanDistWorld(worldCoord, u.tile(), this.game) < 50) .sort(distSortUnitWorld(worldCoord, this.game)); diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 9a676f1d0..6700dc073 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -36,7 +36,7 @@ export class UnitLayer implements Layer { constructor( private game: GameView, private eventBus: EventBus, - private clientID: ClientID, + private clientID: ClientID ) { this.theme = game.config().theme(); } @@ -65,7 +65,7 @@ export class UnitLayer implements Layer { -this.game.width() / 2, -this.game.height() / 2, this.game.width(), - this.game.height(), + this.game.height() ); } @@ -103,11 +103,8 @@ export class UnitLayer implements Layer { case UnitType.TransportShip: this.handleBoatEvent(unit); break; - case UnitType.Destroyer: - this.handleDestroyerEvent(unit); - break; - case UnitType.Battleship: - this.handleBattleshipEvent(unit); + case UnitType.Warship: + this.handleWarShipEvent(unit); break; case UnitType.Shell: this.handleShellEvent(unit); @@ -122,54 +119,13 @@ export class UnitLayer implements Layer { } } - private handleDestroyerEvent(unit: UnitView) { + private handleWarShipEvent(unit: UnitView) { const rel = this.relationship(unit); // Clear previous area for (const t of this.game.bfs( unit.lastTile(), - euclDistFN(unit.lastTile(), 4), - )) { - this.clearCell(this.game.x(t), this.game.y(t)); - } - - if (!unit.isActive()) { - return; - } - - // Paint border - for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 4))) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.borderColor(unit.owner().info()), - 255, - ); - } - - // Paint territory - for (const t of this.game.bfs( - unit.tile(), - manhattanDistFN(unit.tile(), 3), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.territoryColor(unit.owner().info()), - 255, - ); - } - } - - private handleBattleshipEvent(unit: UnitView) { - const rel = this.relationship(unit); - - // Clear previous area - for (const t of this.game.bfs( - unit.lastTile(), - euclDistFN(unit.lastTile(), 6), + euclDistFN(unit.lastTile(), 6) )) { this.clearCell(this.game.x(t), this.game.y(t)); } @@ -185,21 +141,21 @@ export class UnitLayer implements Layer { this.game.y(t), rel, this.theme.territoryColor(unit.owner().info()), - 255, + 255 ); } // Paint border for (const t of this.game.bfs( unit.tile(), - manhattanDistFN(unit.tile(), 4), + manhattanDistFN(unit.tile(), 4) )) { this.paintCell( this.game.x(t), this.game.y(t), rel, this.theme.borderColor(unit.owner().info()), - 255, + 255 ); } @@ -210,7 +166,7 @@ export class UnitLayer implements Layer { this.game.y(t), rel, this.theme.territoryColor(unit.owner().info()), - 255, + 255 ); } } @@ -236,14 +192,14 @@ export class UnitLayer implements Layer { this.game.y(unit.tile()), rel, this.theme.borderColor(unit.owner().info()), - 255, + 255 ); this.paintCell( this.game.x(unit.lastTile()), this.game.y(unit.lastTile()), rel, this.theme.borderColor(unit.owner().info()), - 255, + 255 ); } @@ -253,7 +209,7 @@ export class UnitLayer implements Layer { // Clear previous area for (const t of this.game.bfs( unit.lastTile(), - euclDistFN(unit.lastTile(), 2), + euclDistFN(unit.lastTile(), 2) )) { this.clearCell(this.game.x(t), this.game.y(t)); } @@ -266,7 +222,7 @@ export class UnitLayer implements Layer { this.game.y(t), rel, this.theme.borderColor(unit.owner().info()), - 255, + 255 ); } } @@ -278,7 +234,7 @@ export class UnitLayer implements Layer { // Clear previous area for (const t of this.game.bfs( unit.lastTile(), - euclDistFN(unit.lastTile(), 3), + euclDistFN(unit.lastTile(), 3) )) { this.clearCell(this.game.x(t), this.game.y(t)); } @@ -287,28 +243,28 @@ export class UnitLayer implements Layer { // Paint territory for (const t of this.game.bfs( unit.tile(), - manhattanDistFN(unit.tile(), 2), + manhattanDistFN(unit.tile(), 2) )) { this.paintCell( this.game.x(t), this.game.y(t), rel, this.theme.territoryColor(unit.owner().info()), - 255, + 255 ); } // Paint border for (const t of this.game.bfs( unit.tile(), - manhattanDistFN(unit.tile(), 1), + manhattanDistFN(unit.tile(), 1) )) { this.paintCell( this.game.x(t), this.game.y(t), rel, this.theme.borderColor(unit.owner().info()), - 255, + 255 ); } } @@ -326,7 +282,7 @@ export class UnitLayer implements Layer { // Clear previous area for (const t of this.game.bfs( unit.lastTile(), - manhattanDistFN(unit.lastTile(), 3), + manhattanDistFN(unit.lastTile(), 3) )) { this.clearCell(this.game.x(t), this.game.y(t)); } @@ -339,35 +295,35 @@ export class UnitLayer implements Layer { this.game.y(t), rel, this.theme.territoryColor(unit.owner().info()), - 150, + 150 ); } // Paint border for (const t of this.game.bfs( unit.tile(), - manhattanDistFN(unit.tile(), 2), + manhattanDistFN(unit.tile(), 2) )) { this.paintCell( this.game.x(t), this.game.y(t), rel, this.theme.borderColor(unit.owner().info()), - 255, + 255 ); } // Paint territory for (const t of this.game.bfs( unit.tile(), - manhattanDistFN(unit.tile(), 1), + manhattanDistFN(unit.tile(), 1) )) { this.paintCell( this.game.x(t), this.game.y(t), rel, this.theme.territoryColor(unit.owner().info()), - 255, + 255 ); } } else { @@ -383,7 +339,7 @@ export class UnitLayer implements Layer { y: number, relationship: Relationship, color: Colord, - alpha: number, + alpha: number ) { this.clearCell(x, y); if (this.alternateView) { diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index 3e4820ecc..007764b9f 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -11,15 +11,13 @@ import { import { BuildUnitIntentEvent } from "../../../Transport"; import atomBombIcon from "../../../../../resources/images/NukeIconWhite.svg"; import hydrogenBombIcon from "../../../../../resources/images/MushroomCloudIconWhite.svg"; -import destroyerIcon from "../../../../../resources/images/DestroyerIconWhite.svg"; -import battleshipIcon from "../../../../../resources/images/BattleshipIconWhite.svg"; +import warshipIcon from "../../../../../resources/images/BattleshipIconWhite.svg"; import missileSiloIcon from "../../../../../resources/images/MissileSiloIconWhite.svg"; import goldCoinIcon from "../../../../../resources/images/GoldCoinIcon.svg"; import portIcon from "../../../../../resources/images/PortIcon.svg"; import shieldIcon from "../../../../../resources/images/ShieldIconWhite.svg"; import cityIcon from "../../../../../resources/images/CityIconWhite.svg"; import { renderNumber } from "../../../Utils"; -import { ContextMenuEvent } from "../../../InputHandler"; import { GameView, PlayerView } from "../../../../core/game/GameView"; interface BuildItemDisplay { @@ -31,8 +29,7 @@ const buildTable: BuildItemDisplay[][] = [ [ { unitType: UnitType.AtomBomb, icon: atomBombIcon }, { unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon }, - { unitType: UnitType.Destroyer, icon: destroyerIcon }, - { unitType: UnitType.Battleship, icon: battleshipIcon }, + { unitType: UnitType.Warship, icon: warshipIcon }, { unitType: UnitType.Port, icon: portIcon }, { unitType: UnitType.MissileSilo, icon: missileSiloIcon }, // { unitType: UnitType.DefensePost, icon: shieldIcon }, @@ -201,7 +198,7 @@ export class BuildMenu extends LitElement { public onBuildSelected = (item: BuildItemDisplay) => { this.eventBus.emit( - new BuildUnitIntentEvent(item.unitType, this.clickedCell), + new BuildUnitIntentEvent(item.unitType, this.clickedCell) ); this.hideMenu(); }; @@ -233,7 +230,7 @@ export class BuildMenu extends LitElement { ? this.game .unitInfo(item.unitType) .cost(this.myPlayer) - : 0, + : 0 )} - `, + ` )} - `, + ` )} `; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 76e1f2aae..f4556d551 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -33,7 +33,7 @@ export abstract class DefaultServerConfig implements ServerConfig { export class DefaultConfig implements Config { constructor( private _serverConfig: ServerConfig, - private _gameConfig: GameConfig, + private _gameConfig: GameConfig ) {} gameConfig(): GameConfig { @@ -88,20 +88,12 @@ export class DefaultConfig implements Config { cost: () => 0, territoryBound: false, }; - case UnitType.Destroyer: + case UnitType.Warship: return { - cost: (p: Player) => - (p.units(UnitType.Destroyer).length + 1) * 250_000, + cost: (p: Player) => (p.units(UnitType.Warship).length + 1) * 250_000, territoryBound: false, maxHealth: 1000, }; - case UnitType.Battleship: - return { - cost: (p: Player) => - (p.units(UnitType.Battleship).length + 1) * 500_000, - territoryBound: false, - maxHealth: 5000, - }; case UnitType.Shell: return { cost: () => 0, @@ -113,7 +105,7 @@ export class DefaultConfig implements Config { cost: (p: Player) => Math.min( 1_000_000, - Math.pow(2, p.units(UnitType.Port).length) * 250_000, + Math.pow(2, p.units(UnitType.Port).length) * 250_000 ), territoryBound: true, }; @@ -142,7 +134,7 @@ export class DefaultConfig implements Config { cost: (p: Player) => Math.min( 250_000, - (p.units(UnitType.DefensePost).length + 1) * 50_000, + (p.units(UnitType.DefensePost).length + 1) * 50_000 ), territoryBound: true, }; @@ -151,7 +143,7 @@ export class DefaultConfig implements Config { cost: (p: Player) => Math.min( 1_000_000, - Math.pow(2, p.units(UnitType.City).length) * 125_000, + Math.pow(2, p.units(UnitType.City).length) * 125_000 ), territoryBound: true, }; @@ -207,7 +199,7 @@ export class DefaultConfig implements Config { attackTroops: number, attacker: Player, defender: Player | TerraNullius, - tileToConquer: TileRef, + tileToConquer: TileRef ): { attackerTroopLoss: number; defenderTroopLoss: number; @@ -271,7 +263,7 @@ export class DefaultConfig implements Config { tilesPerTickUsed: within( (2000 * Math.max(10, speed)) / attackTroops, 5, - 100, + 100 ), }; } @@ -281,7 +273,7 @@ export class DefaultConfig implements Config { attackTroops: number, attacker: Player, defender: Player | TerraNullius, - numAdjacentTilesWithEnemy: number, + numAdjacentTilesWithEnemy: number ): number { if (defender.isPlayer()) { return ( diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index ff4e9f11c..8aa48b593 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -29,6 +29,10 @@ export class DevConfig extends DefaultConfig { return info; } + tradeShipSpawnRate(): number { + return 10; + } + // percentageTilesOwnedToWin(): number { // return 1 // } diff --git a/src/core/execution/BattleshipExecution.ts b/src/core/execution/BattleshipExecution.ts deleted file mode 100644 index b08f75c1d..000000000 --- a/src/core/execution/BattleshipExecution.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { - Cell, - Execution, - Game, - Player, - Unit, - PlayerID, - TerrainType, - UnitType, -} from "../game/Game"; -import { PathFinder } from "../pathfinding/PathFinding"; -import { PathFindResultType } from "../pathfinding/AStar"; -import { PseudoRandom } from "../PseudoRandom"; -import { distSort, distSortUnit } from "../Util"; -import { ShellExecution } from "./ShellExecution"; -import { consolex } from "../Consolex"; -import { TileRef } from "../game/GameMap"; - -export class BattleshipExecution implements Execution { - private random: PseudoRandom; - - private _owner: Player; - private active = true; - private battleship: Unit = null; - private mg: Game = null; - - private pathfinder: PathFinder; - - private patrolTile: TileRef; - - // TODO: put in config - private searchRange = 100; - private attackRate = 5; - private lastAttack = 0; - - private alreadyTargeted = new Set(); - - constructor( - private playerID: PlayerID, - private patrolCenterTile: TileRef, - ) {} - - init(mg: Game, ticks: number): void { - this.pathfinder = PathFinder.Mini(mg, 5000, false); - this._owner = mg.player(this.playerID); - this.mg = mg; - this.patrolTile = this.patrolCenterTile; - this.random = new PseudoRandom(mg.ticks()); - } - - tick(ticks: number): void { - this.alreadyTargeted.forEach((u) => { - if (!u.isActive()) { - this.alreadyTargeted.delete(u); - } - }); - if (this.battleship == null) { - const spawn = this._owner.canBuild(UnitType.Battleship, this.patrolTile); - if (spawn == false) { - this.active = false; - return; - } - this.battleship = this._owner.buildUnit(UnitType.Battleship, 0, spawn); - return; - } - if (!this.battleship.isActive()) { - this.active = false; - return; - } - - if (this.mg.ticks() % 2 == 0) { - const result = this.pathfinder.nextTile( - this.battleship.tile(), - this.patrolTile, - ); - switch (result.type) { - case PathFindResultType.Completed: - this.patrolTile = this.randomTile(); - break; - case PathFindResultType.NextTile: - this.battleship.move(result.tile); - break; - case PathFindResultType.Pending: - return; - case PathFindResultType.PathNotFound: - consolex.log(`path not found to patrol tile`); - this.patrolTile = this.randomTile(); - break; - } - } - - if (this.mg.ticks() - this.lastAttack < this.attackRate) { - return; - } - - let ships = this.mg - .units( - UnitType.TransportShip, - UnitType.Destroyer, - UnitType.TradeShip, - UnitType.Battleship, - ) - .filter( - (u) => this.mg.manhattanDist(u.tile(), this.battleship.tile()) < 100, - ) - .filter((u) => u.owner() != this.battleship.owner()) - .filter((u) => u != this.battleship) - .filter((u) => !u.owner().isAlliedWith(this.battleship.owner())) - .filter((u) => !this.alreadyTargeted.has(u)) - .sort(distSortUnit(this.mg, this.battleship)); - - const friendlyDestroyerNearby = - this.battleship - .owner() - .units(UnitType.Destroyer) - .filter( - (d) => this.mg.manhattanDist(d.tile(), this.battleship.tile()) < 120, - ).length > 0; - - if (friendlyDestroyerNearby) { - // Don't attack trade ships to allow friendly destroyer to capture them - ships = ships.filter((s) => s.type() != UnitType.TradeShip); - } - - if (ships.length > 0) { - const toAttack = ships[0]; - if (!toAttack.hasHealth()) { - // Don't send multiple shells to target if it can be one-shotted. - this.alreadyTargeted.add(toAttack); - } - this.lastAttack = this.mg.ticks(); - this.mg.addExecution( - new ShellExecution( - this.battleship.tile(), - this.battleship.owner(), - this.battleship, - toAttack, - ), - ); - } - } - - owner(): Player { - return this._owner; - } - - isActive(): boolean { - return this.active; - } - - activeDuringSpawnPhase(): boolean { - return false; - } - - randomTile(): TileRef { - while (true) { - const x = - this.mg.x(this.patrolCenterTile) + - this.random.nextInt(-this.searchRange / 2, this.searchRange / 2); - const y = - this.mg.y(this.patrolCenterTile) + - this.random.nextInt(-this.searchRange / 2, this.searchRange / 2); - if (!this.mg.isValidCoord(x, y)) { - continue; - } - const tile = this.mg.ref(x, y); - if (!this.mg.isOcean(tile)) { - continue; - } - return tile; - } - } -} diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index fb0c34d37..6851c19e0 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -31,10 +31,9 @@ import { EmojiExecution } from "./EmojiExecution"; import { DonateExecution } from "./DonateExecution"; import { NukeExecution } from "./NukeExecution"; import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution"; -import { DestroyerExecution } from "./DestroyerExecution"; +import { WarshipExecution } from "./WarshipExecution"; import { PortExecution } from "./PortExecution"; import { MissileSiloExecution } from "./MissileSiloExecution"; -import { BattleshipExecution } from "./BattleshipExecution"; import { DefensePostExecution } from "./DefensePostExecution"; import { CityExecution } from "./CityExecution"; import { TileRef } from "../game/GameMap"; @@ -43,10 +42,7 @@ export class Executor { // private random = new PseudoRandom(999) private random: PseudoRandom = null; - constructor( - private mg: Game, - private gameID: GameID, - ) { + constructor(private mg: Game, private gameID: GameID) { // Add one to avoid id collisions with bots. this.random = new PseudoRandom(simpleHash(gameID) + 1); } @@ -62,7 +58,7 @@ export class Executor { intent.troops, intent.attackerID, intent.targetID, - null, + null ); } case "spawn": @@ -71,16 +67,16 @@ export class Executor { sanitize(intent.name), intent.playerType, intent.clientID, - intent.playerID, + intent.playerID ), - this.mg.ref(intent.x, intent.y), + this.mg.ref(intent.x, intent.y) ); case "boat": return new TransportShipExecution( intent.attackerID, intent.targetID, this.mg.ref(intent.x, intent.y), - intent.troops, + intent.troops ); case "allianceRequest": return new AllianceRequestExecution(intent.requestor, intent.recipient); @@ -88,7 +84,7 @@ export class Executor { return new AllianceRequestReplyExecution( intent.requestor, intent.recipient, - intent.accept, + intent.accept ); case "breakAlliance": return new BreakAllianceExecution(intent.requestor, intent.recipient); @@ -98,13 +94,13 @@ export class Executor { return new EmojiExecution( intent.sender, intent.recipient, - intent.emoji, + intent.emoji ); case "donate": return new DonateExecution( intent.sender, intent.recipient, - intent.troops, + intent.troops ); case "troop_ratio": return new SetTargetTroopRatioExecution(intent.player, intent.ratio); @@ -115,37 +111,32 @@ export class Executor { return new NukeExecution( intent.unit, intent.player, - this.mg.ref(intent.x, intent.y), + this.mg.ref(intent.x, intent.y) ); - case UnitType.Destroyer: - return new DestroyerExecution( + case UnitType.Warship: + return new WarshipExecution( intent.player, - this.mg.ref(intent.x, intent.y), - ); - case UnitType.Battleship: - return new BattleshipExecution( - intent.player, - this.mg.ref(intent.x, intent.y), + this.mg.ref(intent.x, intent.y) ); case UnitType.Port: return new PortExecution( intent.player, - this.mg.ref(intent.x, intent.y), + this.mg.ref(intent.x, intent.y) ); case UnitType.MissileSilo: return new MissileSiloExecution( intent.player, - this.mg.ref(intent.x, intent.y), + this.mg.ref(intent.x, intent.y) ); case UnitType.DefensePost: return new DefensePostExecution( intent.player, - this.mg.ref(intent.x, intent.y), + this.mg.ref(intent.x, intent.y) ); case UnitType.City: return new CityExecution( intent.player, - this.mg.ref(intent.x, intent.y), + this.mg.ref(intent.x, intent.y) ); default: throw Error(`unit type ${intent.unit} not supported`); @@ -171,14 +162,14 @@ export class Executor { nation.name, PlayerType.FakeHuman, null, - this.random.nextID(), + this.random.nextID() ), nation.cell, nation.strength * this.mg .config() - .difficultyModifier(this.mg.config().gameConfig().difficulty), - ), + .difficultyModifier(this.mg.config().gameConfig().difficulty) + ) ); } return execs; diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 2ac120274..795b4c87e 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -18,8 +18,7 @@ import { AttackExecution } from "./AttackExecution"; import { TransportShipExecution } from "./TransportShipExecution"; import { SpawnExecution } from "./SpawnExecution"; import { PortExecution } from "./PortExecution"; -import { DestroyerExecution } from "./DestroyerExecution"; -import { BattleshipExecution } from "./BattleshipExecution"; +import { WarshipExecution } from "./WarshipExecution"; import { GameID } from "../Schemas"; import { consolex } from "../Consolex"; import { CityExecution } from "./CityExecution"; @@ -321,10 +320,7 @@ export class FakeHumanExecution implements Execution { 2, (t) => new CityExecution(this.player.id(), t) ); - if (this.maybeSpawnWarship(UnitType.Destroyer)) { - return; - } - if (this.maybeSpawnWarship(UnitType.Battleship)) { + if (this.maybeSpawnWarship()) { return; } this.maybeSpawnStructure( @@ -359,41 +355,28 @@ export class FakeHumanExecution implements Execution { this.mg.addExecution(build(tile)); } - private maybeSpawnWarship( - shipType: UnitType.Destroyer | UnitType.Battleship - ): boolean { + private maybeSpawnWarship(): boolean { if (!this.random.chance(50)) { return false; } const ports = this.player.units(UnitType.Port); - const ships = this.player.units(shipType); + const ships = this.player.units(UnitType.Warship); if ( ports.length > 0 && ships.length == 0 && - this.player.gold() > this.cost(shipType) + this.player.gold() > this.cost(UnitType.Warship) ) { const port = this.random.randElement(ports); const targetTile = this.warshipSpawnTile(port.tile()); if (targetTile == null) { return false; } - const canBuild = this.player.canBuild(UnitType.Destroyer, targetTile); + const canBuild = this.player.canBuild(UnitType.Warship, targetTile); if (canBuild == false) { consolex.warn("cannot spawn destroyer"); return false; } - switch (shipType) { - case UnitType.Destroyer: - this.mg.addExecution( - new DestroyerExecution(this.player.id(), targetTile) - ); - break; - case UnitType.Battleship: - this.mg.addExecution( - new BattleshipExecution(this.player.id(), targetTile) - ); - break; - } + this.mg.addExecution(new WarshipExecution(this.player.id(), targetTile)); return true; } return false; diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 0f4f91034..b338c7a92 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -25,10 +25,7 @@ export class PortExecution implements Execution { private portPaths = new Map(); private computingPaths = new Map(); - constructor( - private _owner: PlayerID, - private tile: TileRef, - ) {} + constructor(private _owner: PlayerID, private tile: TileRef) {} init(mg: Game, ticks: number): void { this.mg = mg; @@ -49,7 +46,7 @@ export class PortExecution implements Execution { .filter((t) => this.mg.isOceanShore(t) && this.mg.owner(t) == player) .sort( (a, b) => - this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile), + this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile) ); if (spawns.length == 0) { @@ -71,7 +68,7 @@ export class PortExecution implements Execution { const alliedPortsSet = new Set(alliedPorts); const allyConnections = new Set( - Array.from(this.portPaths.keys()).map((p) => p.owner()), + Array.from(this.portPaths.keys()).map((p) => p.owner()) ); allyConnections; @@ -103,7 +100,7 @@ export class PortExecution implements Execution { port.tile(), (tr: TileRef) => this.mg.miniMap().isOcean(tr), 10_000, - 25, + 25 ); this.computingPaths.set(port, pf); } @@ -124,9 +121,9 @@ export class PortExecution implements Execution { const port = this.random.randElement(portConnections); const path = this.portPaths.get(port); if (path != null) { - const pf = PathFinder.Mini(this.mg, 10, false); + const pf = PathFinder.Mini(this.mg, 10000, false); this.mg.addExecution( - new TradeShipExecution(this.player().id(), this.port, port, pf, path), + new TradeShipExecution(this.player().id(), this.port, port, pf, path) ); } } diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/WarshipExecution.ts similarity index 57% rename from src/core/execution/DestroyerExecution.ts rename to src/core/execution/WarshipExecution.ts index 11f1cb88b..810d1ae48 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -14,13 +14,14 @@ import { PseudoRandom } from "../PseudoRandom"; import { distSort, distSortUnit } from "../Util"; import { consolex } from "../Consolex"; import { TileRef } from "../game/GameMap"; +import { ShellExecution } from "./ShellExecution"; -export class DestroyerExecution implements Execution { +export class WarshipExecution implements Execution { private random: PseudoRandom; private _owner: Player; private active = true; - private destroyer: Unit = null; + private warship: Unit = null; private mg: Game = null; private target: Unit = null; @@ -31,10 +32,12 @@ export class DestroyerExecution implements Execution { // TODO: put in config private searchRange = 100; - constructor( - private playerID: PlayerID, - private patrolCenterTile: TileRef, - ) {} + private shellAttackRate = 5; + private lastShellAttack = 0; + + private alreadySentShell = new Set(); + + constructor(private playerID: PlayerID, private patrolCenterTile: TileRef) {} init(mg: Game, ticks: number): void { this.pathfinder = PathFinder.Mini(mg, 5000, false); @@ -45,16 +48,16 @@ export class DestroyerExecution implements Execution { } tick(ticks: number): void { - if (this.destroyer == null) { - const spawn = this._owner.canBuild(UnitType.Destroyer, this.patrolTile); + if (this.warship == null) { + const spawn = this._owner.canBuild(UnitType.Warship, this.patrolTile); if (spawn == false) { this.active = false; return; } - this.destroyer = this._owner.buildUnit(UnitType.Destroyer, 0, spawn); + this.warship = this._owner.buildUnit(UnitType.Warship, 0, spawn); return; } - if (!this.destroyer.isActive()) { + if (!this.warship.isActive()) { this.active = false; return; } @@ -63,34 +66,28 @@ export class DestroyerExecution implements Execution { } if (this.target == null) { const ships = this.mg - .units( - UnitType.TransportShip, - UnitType.Destroyer, - UnitType.TradeShip, - UnitType.Battleship, - ) + .units(UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip) .filter( - (u) => this.mg.manhattanDist(u.tile(), this.destroyer.tile()) < 100, + (u) => this.mg.manhattanDist(u.tile(), this.warship.tile()) < 100 ) - .filter( - (u) => - u.type() != UnitType.Destroyer || - u.health() < this.destroyer.health(), - ) // only attack Destroyers weaker than it. - .filter((u) => u.owner() != this.destroyer.owner()) - .filter((u) => u != this.destroyer) - .filter((u) => !u.owner().isAlliedWith(this.destroyer.owner())); - if (ships.length == 0) { + .filter((u) => u.owner() != this.warship.owner()) + .filter((u) => u != this.warship) + .filter((u) => !u.owner().isAlliedWith(this.warship.owner())) + .filter((u) => !this.alreadySentShell.has(u)); + + this.target = ships.sort(distSortUnit(this.mg, this.warship))[0] ?? null; + if (this.target == null || this.target.type() != UnitType.TradeShip) { + // Patrol unless we are hunting down a tradeship const result = this.pathfinder.nextTile( - this.destroyer.tile(), - this.patrolTile, + this.warship.tile(), + this.patrolTile ); switch (result.type) { case PathFindResultType.Completed: this.patrolTile = this.randomTile(); break; case PathFindResultType.NextTile: - this.destroyer.move(result.tile); + this.warship.move(result.tile); break; case PathFindResultType.Pending: return; @@ -99,42 +96,53 @@ export class DestroyerExecution implements Execution { this.patrolTile = this.randomTile(); break; } - return; } - this.target = ships.sort(distSortUnit(this.mg, this.destroyer))[0]; } - if (!this.target.isActive() || this.target.owner() == this._owner) { - // Incase another destroyer captured or destroyed target + if ( + this.target == null || + !this.target.isActive() || + this.target.owner() == this._owner + ) { + // In case another destroyer captured or destroyed target this.target = null; return; } + if (this.target.type() != UnitType.TradeShip) { + if (this.mg.ticks() - this.lastShellAttack > this.shellAttackRate) { + this.lastShellAttack = this.mg.ticks(); + this.mg.addExecution( + new ShellExecution( + this.warship.tile(), + this.warship.owner(), + this.warship, + this.target + ) + ); + if (!this.target.hasHealth()) { + // Don't send multiple shells to target that can be oneshotted + this.alreadySentShell.add(this.target); + this.target = null; + return; + } + } + // Only hunt down tradeships + return; + } for (let i = 0; i < 2; i++) { + // target is trade ship so capture it. const result = this.pathfinder.nextTile( - this.destroyer.tile(), + this.warship.tile(), this.target.tile(), - 5, + 5 ); switch (result.type) { case PathFindResultType.Completed: - switch (this.target.type()) { - case UnitType.TransportShip: - case UnitType.Battleship: - this.target.delete(); - break; - case UnitType.TradeShip: - this.owner().captureUnit(this.target); - break; - case UnitType.Destroyer: - const health = this.target.health(); - this.target.modifyHealth(-this.destroyer.health()); - this.destroyer.modifyHealth(-health); - break; - } + this.owner().captureUnit(this.target); this.target = null; return; case PathFindResultType.NextTile: - this.destroyer.move(result.tile); + this.warship.move(result.tile); break; case PathFindResultType.Pending: break; diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index a99772144..4fe660954 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -62,8 +62,7 @@ export interface UnitInfo { export enum UnitType { TransportShip = "Transport", - Destroyer = "Destroyer", - Battleship = "Battleship", + Warship = "Warship", Shell = "Shell", Port = "Port", AtomBomb = "Atom Bomb", diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index b6cdbab10..f28e824a8 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -42,10 +42,7 @@ export class UnitView { public _wasUpdated = true; public lastPos: MapPos[] = []; - constructor( - private gameView: GameView, - private data: UnitUpdate, - ) { + constructor(private gameView: GameView, private data: UnitUpdate) { this.lastPos.push(data.pos); } @@ -101,14 +98,14 @@ export class PlayerView { constructor( private game: GameView, public data: PlayerUpdate, - public nameData: NameViewData, + public nameData: NameViewData ) {} async actions(tile: TileRef): Promise { return this.game.worker.playerInteraction( this.id(), this.game.x(tile), - this.game.y(tile), + this.game.y(tile) ); } @@ -151,12 +148,12 @@ export class PlayerView { } allies(): PlayerView[] { return this.data.allies.map( - (a) => this.game.playerBySmallID(a) as PlayerView, + (a) => this.game.playerBySmallID(a) as PlayerView ); } targets(): PlayerView[] { return this.data.targets.map( - (id) => this.game.playerBySmallID(id) as PlayerView, + (id) => this.game.playerBySmallID(id) as PlayerView ); } gold(): Gold { @@ -211,7 +208,7 @@ export class GameView implements GameMap { public worker: WorkerClient, private _config: Config, private _map: GameMap, - private _myClientID: ClientID, + private _myClientID: ClientID ) { this.lastUpdate = { tick: 0, @@ -242,7 +239,7 @@ export class GameView implements GameMap { } else { this._players.set( pu.id, - new PlayerView(this, pu, gu.playerNameViewData[pu.id]), + new PlayerView(this, pu, gu.playerNameViewData[pu.id]) ); } }); @@ -321,7 +318,12 @@ export class GameView implements GameMap { return this._config; } units(...types: UnitType[]): UnitView[] { - return Array.from(this._units.values()); + if (types.length == 0) { + return Array.from(this._units.values()); + } + return Array.from(this._units.values()).filter((u) => + types.includes(u.type()) + ); } unit(id: number): UnitView { return this._units.get(id); @@ -416,7 +418,7 @@ export class GameView implements GameMap { } bfs( tile: TileRef, - filter: (gm: GameMap, tile: TileRef) => boolean, + filter: (gm: GameMap, tile: TileRef) => boolean ): Set { return this._map.bfs(tile, filter); } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 74c4a3c5b..16abbf6fb 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -567,8 +567,7 @@ export class PlayerImpl implements Player { return this.nukeSpawn(targetTile); case UnitType.Port: return this.portSpawn(targetTile); - case UnitType.Destroyer: - case UnitType.Battleship: + case UnitType.Warship: return this.warshipSpawn(targetTile); case UnitType.Shell: return targetTile; diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 6fb7ec90f..f31089e8b 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -18,7 +18,7 @@ export class UnitImpl implements Unit { private _tile: TileRef, private _troops: number, private _id: number, - public _owner: PlayerImpl, + public _owner: PlayerImpl ) { // default to half health (or 1 is no health specified) this._health = (this.mg.unitInfo(_type).maxHealth ?? 2) / 2; @@ -35,6 +35,7 @@ export class UnitImpl implements Unit { isActive: this._active, pos: { x: this.mg.x(this._tile), y: this.mg.y(this._tile) }, lastPos: { x: this.mg.x(this._lastTile), y: this.mg.y(this._lastTile) }, + health: this.hasHealth() ? this._health : undefined, }; } @@ -85,7 +86,7 @@ export class UnitImpl implements Unit { this.mg.displayMessage( `Your ${this.type()} was captured by ${newOwner.displayName()}`, MessageType.ERROR, - oldOwner.id(), + oldOwner.id() ); } @@ -104,7 +105,7 @@ export class UnitImpl implements Unit { this.mg.displayMessage( `Your ${this.type()} was destroyed`, MessageType.ERROR, - this.owner().id(), + this.owner().id() ); } }