diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 6700dc073..2e36696d5 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -81,7 +81,7 @@ export class UnitLayer implements Layer { this.canvas.width = this.game.width(); this.canvas.height = this.game.height(); for (const unit of this.game.units()) { - // this.onUnitEvent(new UnitEvent(unit, unit.tile())) + this.onUnitEvent(unit); } } @@ -112,8 +112,12 @@ export class UnitLayer implements Layer { case UnitType.TradeShip: this.handleTradeShipEvent(unit); break; + case UnitType.MIRVWarhead: + this.handleMIRVWarhead(unit); + break; case UnitType.AtomBomb: case UnitType.HydrogenBomb: + case UnitType.MIRV: this.handleNuke(unit); break; } @@ -228,6 +232,23 @@ export class UnitLayer implements Layer { } } + private handleMIRVWarhead(unit: UnitView) { + const rel = this.relationship(unit); + + this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile())); + + if (unit.isActive()) { + // Paint area + this.paintCell( + this.game.x(unit.tile()), + this.game.y(unit.tile()), + rel, + this.theme.borderColor(unit.owner().info()), + 255 + ); + } + } + private handleTradeShipEvent(unit: UnitView) { const rel = this.relationship(unit); diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index 007764b9f..31218041f 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -28,7 +28,7 @@ interface BuildItemDisplay { const buildTable: BuildItemDisplay[][] = [ [ { unitType: UnitType.AtomBomb, icon: atomBombIcon }, - { unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon }, + { unitType: UnitType.MIRV, icon: hydrogenBombIcon }, { unitType: UnitType.Warship, icon: warshipIcon }, { unitType: UnitType.Port, icon: portIcon }, { unitType: UnitType.MissileSilo, icon: missileSiloIcon }, diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index d0735b6dc..17e1afc64 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -119,6 +119,16 @@ export class DefaultConfig implements Config { cost: () => 5_000_000, territoryBound: false, }; + case UnitType.MIRV: + return { + cost: () => 5_000_000, + territoryBound: false, + }; + case UnitType.MIRVWarhead: + return { + cost: () => 0, + territoryBound: false, + }; case UnitType.TradeShip: return { cost: () => 0, @@ -330,16 +340,11 @@ export class DefaultConfig implements Config { populationIncreaseRate(player: Player): number { let max = this.maxPopulation(player); - // const thing = Math.sqrt(player.population() + player.population() * player.workers()) - let toAdd = 10 + Math.pow(player.population(), 0.73) / 4; const ratio = 1 - player.population() / max; toAdd *= ratio; - if (player.type() == PlayerType.FakeHuman) { - toAdd *= 1.0; - } if (player.type() == PlayerType.Bot) { toAdd *= 0.7; } diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index f61eda79f..93f42456d 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -45,10 +45,10 @@ export class DevConfig extends DefaultConfig { // return 5000 // } - numBots(): number { - return 0; - } - spawnNPCs(): boolean { - return false; - } + // numBots(): number { + // return 0; + // } + // spawnNPCs(): boolean { + // return false; + // } } diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 94d07e91f..9564b3f8f 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -37,6 +37,7 @@ import { MissileSiloExecution } from "./MissileSiloExecution"; import { DefensePostExecution } from "./DefensePostExecution"; import { CityExecution } from "./CityExecution"; import { TileRef } from "../game/GameMap"; +import { MirvExecution } from "./MIRVExecution"; export class Executor { // private random = new PseudoRandom(999) @@ -113,6 +114,11 @@ export class Executor { intent.player, this.mg.ref(intent.x, intent.y) ); + case UnitType.MIRV: + return new MirvExecution( + intent.player, + this.mg.ref(intent.x, intent.y) + ); case UnitType.Warship: return new WarshipExecution( intent.player, @@ -155,7 +161,6 @@ export class Executor { fakeHumanExecutions(): Execution[] { const execs = []; for (const nation of this.mg.nations()) { - console.log(`got nation: ${nation.name}`); execs.push( new FakeHumanExecution( this.gameID, diff --git a/src/core/execution/MIRVExecution.ts b/src/core/execution/MIRVExecution.ts new file mode 100644 index 000000000..5ac288fb5 --- /dev/null +++ b/src/core/execution/MIRVExecution.ts @@ -0,0 +1,160 @@ +import { nextTick } from "process"; +import { + Cell, + Execution, + Game, + Player, + PlayerID, + Unit, + UnitType, + TerraNullius, +} from "../game/Game"; +import { PathFinder } from "../pathfinding/PathFinding"; +import { PathFindResultType } from "../pathfinding/AStar"; +import { PseudoRandom } from "../PseudoRandom"; +import { consolex } from "../Consolex"; +import { TileRef } from "../game/GameMap"; +import { simpleHash } from "../Util"; +import { NukeExecution } from "./NukeExecution"; + +export class MirvExecution implements Execution { + private player: Player; + + private active = true; + + private mg: Game; + + private nuke: Unit; + + private mirvRange = 350; + private warheadCount = 1000; + // private warheadRange = 5; + + private random: PseudoRandom; + + private pathFinder: PathFinder; + + private targetPlayer: Player | TerraNullius; + + constructor(private senderID: PlayerID, private dst: TileRef) {} + + init(mg: Game, ticks: number): void { + this.random = new PseudoRandom(mg.ticks() + simpleHash(this.senderID)); + this.mg = mg; + this.pathFinder = PathFinder.Mini(mg, 10_000, true); + this.player = mg.player(this.senderID); + this.targetPlayer = this.mg.owner(this.dst); + } + + tick(ticks: number): void { + if (this.nuke == null) { + const spawn = this.player.canBuild(UnitType.MIRV, this.dst); + if (spawn == false) { + consolex.warn(`cannot build MIRV`); + this.active = false; + return; + } + this.nuke = this.player.buildUnit(UnitType.MIRV, 0, spawn); + } + + for (let i = 0; i < 4; i++) { + const result = this.pathFinder.nextTile(this.nuke.tile(), this.dst); + switch (result.type) { + case PathFindResultType.Completed: + this.nuke.move(result.tile); + this.separate(); + this.active = false; + return; + case PathFindResultType.NextTile: + this.nuke.move(result.tile); + break; + case PathFindResultType.Pending: + break; + case PathFindResultType.PathNotFound: + consolex.warn( + `nuke cannot find path from ${this.nuke.tile()} to ${this.dst}` + ); + this.active = false; + return; + } + } + } + + private separate() { + const dsts: TileRef[] = [this.dst]; + let attempts = 1000; + while (attempts > 0 && dsts.length < this.warheadCount) { + attempts--; + const potential = this.randomLand(this.dst); + if (potential == null) { + continue; + } + dsts.push(potential); + } + console.log(`dsts: ${dsts.length}`); + + for (const dst of dsts) { + this.mg.addExecution( + new NukeExecution( + UnitType.MIRVWarhead, + this.senderID, + dst, + this.nuke.tile(), + this.random.nextInt(5, 9) + ) + ); + } + if (this.targetPlayer.isPlayer()) { + const alliance = this.player.allianceWith(this.targetPlayer); + if (alliance != null) { + this.player.breakAlliance(alliance); + } + if (this.targetPlayer != this.player) { + this.targetPlayer.updateRelation(this.player, -100); + } + } + this.nuke.delete(false); + } + + randomLand(ref: TileRef): TileRef | null { + let tries = 0; + while (tries < 25) { + tries++; + const x = this.random.nextInt( + this.mg.x(ref) - this.mirvRange, + this.mg.x(ref) + this.mirvRange + ); + const y = this.random.nextInt( + this.mg.y(ref) - this.mirvRange, + this.mg.y(ref) + this.mirvRange + ); + if (!this.mg.isValidCoord(x, y)) { + continue; + } + const tile = this.mg.ref(x, y); + if (!this.mg.isLand(tile)) { + continue; + } + if (this.mg.euclideanDist(tile, ref) > this.mirvRange) { + continue; + } + if (this.mg.owner(tile) != this.targetPlayer) { + continue; + } + return tile; + } + return null; + } + + owner(): Player { + return this.player; + } + + isActive(): boolean { + return this.active; + } + + activeDuringSpawnPhase(): boolean { + return false; + } +} diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index f2a892dcb..5db9fa619 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -1,4 +1,3 @@ -import { nextTick } from "process"; import { Cell, Execution, @@ -9,32 +8,33 @@ import { UnitType, TerraNullius, } from "../game/Game"; -import { PathFinder } from "../pathfinding/PathFinding"; -import { PathFindResultType } from "../pathfinding/AStar"; import { PseudoRandom } from "../PseudoRandom"; import { consolex } from "../Consolex"; import { TileRef } from "../game/GameMap"; export class NukeExecution implements Execution { private player: Player; - private active = true; - private mg: Game; - private nuke: Unit; - private pathFinder: PathFinder; + private random: PseudoRandom; + constructor( - private type: UnitType.AtomBomb | UnitType.HydrogenBomb, + private type: + | UnitType.AtomBomb + | UnitType.HydrogenBomb + | UnitType.MIRVWarhead, private senderID: PlayerID, private dst: TileRef, + private src?: TileRef, + private speed: number = 4 ) {} init(mg: Game, ticks: number): void { this.mg = mg; - this.pathFinder = PathFinder.Mini(mg, 10_000, true); this.player = mg.player(this.senderID); + this.random = new PseudoRandom(ticks); } public target(): Player | TerraNullius { @@ -43,7 +43,7 @@ export class NukeExecution implements Execution { tick(ticks: number): void { if (this.nuke == null) { - const spawn = this.player.canBuild(this.type, this.dst); + const spawn = this.src ?? this.player.canBuild(this.type, this.dst); if (spawn == false) { consolex.warn(`cannot build Nuke`); this.active = false; @@ -52,33 +52,60 @@ export class NukeExecution implements Execution { this.nuke = this.player.buildUnit(this.type, 0, spawn); } - for (let i = 0; i < 4; i++) { - const result = this.pathFinder.nextTile(this.nuke.tile(), this.dst); - switch (result.type) { - case PathFindResultType.Completed: - this.nuke.move(result.tile); - this.detonate(); - return; - case PathFindResultType.NextTile: - this.nuke.move(result.tile); - break; - case PathFindResultType.Pending: - break; - case PathFindResultType.PathNotFound: - consolex.warn( - `nuke cannot find path from ${this.nuke.tile()} to ${this.dst}`, - ); - this.active = false; - return; + for (let i = 0; i < this.speed; i++) { + const x = this.mg.x(this.nuke.tile()); + const y = this.mg.y(this.nuke.tile()); + const dstX = this.mg.x(this.dst); + const dstY = this.mg.y(this.dst); + + // If we've reached the destination, detonate + if (x === dstX && y === dstY) { + this.detonate(); + return; + } + + // Calculate next position + let nextX = x; + let nextY = y; + + const ratio = Math.floor( + 1 + Math.abs(dstY - y) / (Math.abs(dstX - x) + 1) + ); + + if (this.random.chance(ratio) && x != dstX) { + if (x < dstX) nextX++; + else if (x > dstX) nextX--; + } else { + if (y < dstY) nextY++; + else if (y > dstY) nextY--; + } + + // Move to next tile + const nextTile = this.mg.ref(nextX, nextY); + if (nextTile !== undefined) { + this.nuke.move(nextTile); + } else { + consolex.warn(`invalid tile position ${nextX},${nextY}`); + this.active = false; + return; } } } private detonate() { - const magnitude = - this.type == UnitType.AtomBomb - ? { inner: 15, outer: 40 } - : { inner: 140, outer: 160 }; + let magnitude; + switch (this.type) { + case UnitType.MIRVWarhead: + magnitude = { inner: 10, outer: 14 }; + break; + case UnitType.AtomBomb: + magnitude = { inner: 15, outer: 40 }; + break; + case UnitType.HydrogenBomb: + magnitude = { inner: 140, outer: 160 }; + break; + } + const rand = new PseudoRandom(this.mg.ticks()); const toDestroy = this.mg.bfs(this.dst, (_, n: TileRef) => { const d = this.mg.euclideanDist(this.dst, n); @@ -88,7 +115,7 @@ export class NukeExecution implements Execution { const ratio = Object.fromEntries( this.mg .players() - .map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()]), + .map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()]) ); const attacked = new Map(); for (const tile of toDestroy) { @@ -108,7 +135,8 @@ export class NukeExecution implements Execution { } } for (const [other, tilesDestroyed] of attacked) { - if (tilesDestroyed > 100) { + if (tilesDestroyed > 100 && this.nuke.type() != UnitType.MIRVWarhead) { + // Mirv warheads shouldn't break alliances const alliance = this.player.allianceWith(other); if (alliance != null) { this.player.breakAlliance(alliance); @@ -122,7 +150,9 @@ export class NukeExecution implements Execution { for (const unit of this.mg.units()) { if ( unit.type() != UnitType.AtomBomb && - unit.type() != UnitType.HydrogenBomb + unit.type() != UnitType.HydrogenBomb && + unit.type() != UnitType.MIRVWarhead && + unit.type() != UnitType.MIRV ) { if (this.mg.euclideanDist(this.dst, unit.tile()) < magnitude.outer) { unit.delete(); diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index af20372a2..3ee17aad8 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -59,7 +59,9 @@ export class PlayerExecution implements Execution { this.player.units().forEach((u) => { if ( u.type() != UnitType.AtomBomb && - u.type() != UnitType.HydrogenBomb + u.type() != UnitType.HydrogenBomb && + u.type() != UnitType.MIRVWarhead && + u.type() != UnitType.MIRV ) { u.delete(); } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 4ab38bf60..a7d243cef 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -71,6 +71,8 @@ export enum UnitType { MissileSilo = "Missile Silo", DefensePost = "Defense Post", City = "City", + MIRV = "MIRV", + MIRVWarhead = "MIRV Warhead", } export enum Relation { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 16abbf6fb..506b29f7f 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -562,9 +562,12 @@ export class PlayerImpl implements Player { return false; } switch (unitType) { + case UnitType.MIRV: case UnitType.AtomBomb: case UnitType.HydrogenBomb: return this.nukeSpawn(targetTile); + case UnitType.MIRVWarhead: + return targetTile; case UnitType.Port: return this.portSpawn(targetTile); case UnitType.Warship: