From 564ebcab4da9a320dad60765777cd27260a928a5 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Mon, 31 Mar 2025 21:31:09 -0400 Subject: [PATCH 01/11] Nations target structures with nukes --- src/core/execution/FakeHumanExecution.ts | 102 ++++++++++++++++++++--- 1 file changed, 90 insertions(+), 12 deletions(-) diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 8565d9566..46daf8dae 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -13,9 +13,10 @@ import { TerrainType, TerraNullius, Tick, + Unit, UnitType, } from "../game/Game"; -import { andFN, manhattanDistFN, TileRef } from "../game/GameMap"; +import { andFN, euclDistFN, manhattanDistFN, TileRef } from "../game/GameMap"; import { PseudoRandom } from "../PseudoRandom"; import { GameID } from "../Schemas"; import { calculateBoundingBox, simpleHash } from "../Util"; @@ -40,6 +41,7 @@ export class FakeHumanExecution implements Execution { private lastEnemyUpdateTick: number = 0; private lastEmojiSent = new Map(); + private lastNukeSent: [Tick, TileRef][] = []; private embargoMalusApplied = new Set(); constructor( @@ -289,32 +291,108 @@ export class FakeHumanExecution implements Execution { } private maybeSendNuke(other: Player) { + const silos = this.player.units(UnitType.MissileSilo); if ( - this.player.units(UnitType.MissileSilo).length == 0 || + silos.length == 0 || this.player.gold() < this.mg.config().unitInfo(UnitType.AtomBomb).cost(this.player) || this.player.isOnSameTeam(other) ) { return; } - outer: for (let i = 0; i < 10; i++) { - const tile = this.randTerritoryTile(other); - if (tile == null) { - return; - } + + const structures = other.units( + UnitType.City, + UnitType.DefensePost, + UnitType.MissileSilo, + UnitType.Port, + UnitType.SAMLauncher, + ); + const structureTiles = structures.map((u) => u.tile()); + const randomTiles: TileRef[] = new Array(10); + for (let i = 0; i < randomTiles.length; i++) { + randomTiles[i] = this.randTerritoryTile(other); + } + const allTiles = randomTiles.concat(structureTiles); + + let bestTile = null; + let bestValue = 0; + this.removeOldNukeEvents(); + outer: for (const tile of new Set(allTiles)) { + if (tile == null) continue; for (const t of this.mg.bfs(tile, manhattanDistFN(tile, 15))) { // Make sure we nuke at least 15 tiles in border if (this.mg.owner(t) != other) { continue outer; } } - if (this.player.canBuild(UnitType.AtomBomb, tile)) { - this.mg.addExecution( - new NukeExecution(UnitType.AtomBomb, this.player.id(), tile), - ); - return; + if (!this.player.canBuild(UnitType.AtomBomb, tile)) continue; + const value = this.nukeTileScore(tile, silos, structures); + if (value > bestTile) { + bestTile = tile; + bestValue = value; } } + if (bestTile != null) { + this.sendNuke(bestTile); + } + } + + private removeOldNukeEvents() { + const maxAge = 500; + const tick = this.mg.ticks(); + while ( + this.lastNukeSent.length > 0 && + this.lastNukeSent[0][0] + maxAge < tick + ) { + this.lastNukeSent.shift(); + } + } + + private sendNuke(tile: TileRef) { + const tick = this.mg.ticks(); + this.lastNukeSent.push([tick, tile]); + this.mg.addExecution( + new NukeExecution(UnitType.AtomBomb, this.player.id(), tile), + ); + } + + private nukeTileScore(tile: TileRef, silos: Unit[], targets: Unit[]): number { + // Potential damage in a 25-tile radius + const dist = euclDistFN(tile, 25, false); + let tileValue = targets + .filter((unit) => dist(this.mg, unit.tile())) + .map((unit) => { + switch (unit.type()) { + case UnitType.City: + return 25_000; + case UnitType.DefensePost: + return 5_000; + case UnitType.MissileSilo: + return 50_000; + case UnitType.Port: + return 10_000; + case UnitType.SAMLauncher: + return 5_000; + default: + return 0; + } + }) + .reduce((prev, cur) => prev + cur, 0); + + // Prefer tiles that are closer to a silo + const siloTiles = silos.map((u) => u.tile()); + const { x: closestSilo } = closestTwoTiles(this.mg, siloTiles, [tile]); + const distanceToClosestSilo = this.mg.euclideanDist(tile, closestSilo); + tileValue -= distanceToClosestSilo * 30; + + // Don't target near recent targets + tileValue -= this.lastNukeSent + .filter(([_tick, tile]) => dist(this.mg, tile)) + .map((_) => 1_000_000) + .reduce((prev, cur) => prev + cur, 0); + + return tileValue; } private maybeSendBoatAttack(other: Player) { From fe9b6c2801bd534ef7449d3e0c9fb8494af22dc8 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Wed, 2 Apr 2025 21:57:04 -0400 Subject: [PATCH 02/11] bug: lastEmojiSet is never used --- src/core/execution/FakeHumanExecution.ts | 31 ++++++++++++------------ 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 8565d9566..74ce32f6b 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -254,22 +254,6 @@ export class FakeHumanExecution implements Execution { if (mostHated != null && mostHated.relation == Relation.Hostile) { this.enemy = mostHated.player; this.lastEnemyUpdateTick = this.mg.ticks(); - if (this.enemy.type() == PlayerType.Human) { - let lastSent = -300; - if (this.lastEmojiSent.has(this.enemy)) { - lastSent = this.lastEmojiSent.get(this.enemy); - this.lastEmojiSent.set(this.enemy, this.mg.ticks()); - } - if (this.mg.ticks() - lastSent > 300) { - this.mg.addExecution( - new EmojiExecution( - this.player.id(), - this.enemy.id(), - this.random.randElement(["🤡", "😡"]), - ), - ); - } - } } } @@ -278,6 +262,7 @@ export class FakeHumanExecution implements Execution { this.enemy = null; return; } + this.maybeSendEmoji(); this.maybeSendNuke(this.enemy); if (this.player.sharesBorderWith(this.enemy)) { this.sendAttack(this.enemy); @@ -288,6 +273,20 @@ export class FakeHumanExecution implements Execution { } } + private maybeSendEmoji() { + if (this.enemy.type() != PlayerType.Human) return; + const lastSent = this.lastEmojiSent.get(this.enemy) ?? -300; + if (this.mg.ticks() - lastSent <= 300) return; + this.lastEmojiSent.set(this.enemy, this.mg.ticks()); + this.mg.addExecution( + new EmojiExecution( + this.player.id(), + this.enemy.id(), + this.random.randElement(["🤡", "😡"]), + ), + ); + } + private maybeSendNuke(other: Player) { if ( this.player.units(UnitType.MissileSilo).length == 0 || From 2778d1e4808de4fc794a41462bea36e575d2a38f Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Wed, 2 Apr 2025 23:23:40 -0400 Subject: [PATCH 03/11] Refactor attack rate --- src/core/execution/BotExecution.ts | 6 +++--- src/core/execution/FakeHumanExecution.ts | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index f7321ef93..b6fd7399f 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -13,12 +13,14 @@ export class BotExecution implements Execution { private active = true; private random: PseudoRandom; private attackRate: number; + private attackTick: number; private mg: Game; private neighborsTerraNullius = true; constructor(private bot: Player) { this.random = new PseudoRandom(simpleHash(bot.id())); this.attackRate = this.random.nextInt(10, 50); + this.attackTick = this.random.nextInt(0, this.attackRate - 1); } activeDuringSpawnPhase(): boolean { return false; @@ -36,9 +38,7 @@ export class BotExecution implements Execution { return; } - if (ticks % this.attackRate != 0) { - return; - } + if (ticks % this.attackRate != this.attackTick) return; this.bot.incomingAllianceRequests().forEach((ar) => { if (ar.requestor().isTraitor()) { diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 59320edeb..43e3e997e 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -44,6 +44,9 @@ export class FakeHumanExecution implements Execution { private lastNukeSent: [Tick, TileRef][] = []; private embargoMalusApplied = new Set(); + private attackRate: number; + private attackTick: number; + constructor( gameID: GameID, private playerInfo: PlayerInfo, @@ -51,6 +54,8 @@ export class FakeHumanExecution implements Execution { this.random = new PseudoRandom( simpleHash(playerInfo.id) + simpleHash(gameID), ); + this.attackRate = this.random.nextInt(40, 80); + this.attackTick = this.random.nextInt(0, this.attackRate - 1); } init(mg: Game, ticks: number) { @@ -128,9 +133,7 @@ export class FakeHumanExecution implements Execution { return; } - if (ticks % this.random.nextInt(40, 80) != 0) { - return; - } + if (ticks % this.attackRate != this.attackTick) return; if ( this.player.troops() > 100_000 && From b27ac3ae87efacfa3a587d9eb5598992ce9e1c66 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:59:31 -0400 Subject: [PATCH 04/11] refactor tick --- src/core/execution/BotExecution.ts | 23 +++++++++++++++++------ src/core/execution/FakeHumanExecution.ts | 24 +++++++++++++----------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index b6fd7399f..90da61cd6 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -3,6 +3,7 @@ import { Game, Player, PlayerType, + Relation, TerraNullius, } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; @@ -12,15 +13,15 @@ import { AttackExecution } from "./AttackExecution"; export class BotExecution implements Execution { private active = true; private random: PseudoRandom; - private attackRate: number; - private attackTick: number; + private updateRate: number; + private updateTick: number; private mg: Game; private neighborsTerraNullius = true; constructor(private bot: Player) { this.random = new PseudoRandom(simpleHash(bot.id())); - this.attackRate = this.random.nextInt(10, 50); - this.attackTick = this.random.nextInt(0, this.attackRate - 1); + this.updateRate = this.random.nextInt(10, 50); + this.updateTick = this.random.nextInt(0, this.updateRate); } activeDuringSpawnPhase(): boolean { return false; @@ -33,21 +34,31 @@ export class BotExecution implements Execution { } tick(ticks: number) { + if (ticks % this.updateRate != this.updateTick) return; + if (!this.bot.isAlive()) { this.active = false; return; } - if (ticks % this.attackRate != this.attackTick) return; + this.handleAllianceRequests(); + this.maybeAttack(); + } + private handleAllianceRequests() { this.bot.incomingAllianceRequests().forEach((ar) => { - if (ar.requestor().isTraitor()) { + if ( + ar.requestor().isTraitor() || + this.bot.relation(ar.requestor()) <= Relation.Distrustful + ) { ar.reject(); } else { ar.accept(); } }); + } + private maybeAttack() { const traitors = this.bot .neighbors() .filter((n) => n.isPlayer() && n.isTraitor()) as Player[]; diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 43e3e997e..e286a0991 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -55,7 +55,7 @@ export class FakeHumanExecution implements Execution { simpleHash(playerInfo.id) + simpleHash(gameID), ); this.attackRate = this.random.nextInt(40, 80); - this.attackTick = this.random.nextInt(0, this.attackRate - 1); + this.attackTick = this.random.nextInt(0, this.attackRate); } init(mg: Game, ticks: number) { @@ -106,17 +106,18 @@ export class FakeHumanExecution implements Execution { } tick(ticks: number) { + if (ticks % this.attackRate != this.attackTick) return; + if (this.mg.inSpawnPhase()) { - if (ticks % this.random.nextInt(5, 30) == 0) { - const rl = this.randomLand(); - if (rl == null) { - consolex.warn(`cannot spawn ${this.playerInfo.name}`); - return; - } - this.mg.addExecution(new SpawnExecution(this.playerInfo, rl)); + const rl = this.randomLand(); + if (rl == null) { + consolex.warn(`cannot spawn ${this.playerInfo.name}`); + return; } + this.mg.addExecution(new SpawnExecution(this.playerInfo, rl)); return; } + if (this.player == null) { this.player = this.mg.players().find((p) => p.id() == this.playerInfo.id); if (this.player == null) { @@ -133,8 +134,6 @@ export class FakeHumanExecution implements Execution { return; } - if (ticks % this.attackRate != this.attackTick) return; - if ( this.player.troops() > 100_000 && this.player.targetTroopRatio() > 0.7 @@ -147,7 +146,10 @@ export class FakeHumanExecution implements Execution { this.handleEnemies(); this.handleUnits(); this.handleEmbargoesToHostileNations(); + this.maybeAttack(); + } + private maybeAttack() { const enemyborder = Array.from(this.player.borderTiles()) .flatMap((t) => this.mg.neighbors(t)) .filter( @@ -554,7 +556,7 @@ export class FakeHumanExecution implements Execution { return this.mg.unitInfo(type).cost(this.player); } - handleAllianceRequests() { + private handleAllianceRequests() { for (const req of this.player.incomingAllianceRequests()) { if (req.requestor().isTraitor()) { this.replyToAllianceRequest(req, false); From 6ee58f4191c5b7acca5427b2fbcdc8482411b47f Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Thu, 3 Apr 2025 01:23:42 -0400 Subject: [PATCH 05/11] cost --- src/core/execution/FakeHumanExecution.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index e286a0991..36082ebae 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -298,8 +298,7 @@ export class FakeHumanExecution implements Execution { const silos = this.player.units(UnitType.MissileSilo); if ( silos.length == 0 || - this.player.gold() < - this.mg.config().unitInfo(UnitType.AtomBomb).cost(this.player) || + this.player.gold() < this.cost(UnitType.AtomBomb) || this.player.isOnSameTeam(other) ) { return; @@ -462,9 +461,7 @@ export class FakeHumanExecution implements Execution { if (units.length >= maxNum) { return; } - if ( - this.player.gold() < this.mg.config().unitInfo(type).cost(this.player) - ) { + if (this.player.gold() < this.cost(type)) { return; } const tile = this.randTerritoryTile(this.player); From 7df6f271ed5dbeee720995277e542fd0740f5b79 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Thu, 3 Apr 2025 01:24:02 -0400 Subject: [PATCH 06/11] maybeSpawnStructure --- src/core/execution/FakeHumanExecution.ts | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 36082ebae..cb04d3bb4 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -434,29 +434,16 @@ export class FakeHumanExecution implements Execution { } return; } - this.maybeSpawnStructure( - UnitType.City, - 2, - (t) => new ConstructionExecution(this.player.id(), t, UnitType.City), - ); + this.maybeSpawnStructure(UnitType.City, 2); if (this.maybeSpawnWarship()) { return; } if (!this.mg.config().disableNukes()) { - this.maybeSpawnStructure( - UnitType.MissileSilo, - 1, - (t) => - new ConstructionExecution(this.player.id(), t, UnitType.MissileSilo), - ); + this.maybeSpawnStructure(UnitType.MissileSilo, 1); } } - private maybeSpawnStructure( - type: UnitType, - maxNum: number, - build: (tile: TileRef) => Execution, - ) { + private maybeSpawnStructure(type: UnitType, maxNum: number) { const units = this.player.units(type); if (units.length >= maxNum) { return; @@ -472,7 +459,9 @@ export class FakeHumanExecution implements Execution { if (canBuild == false) { return; } - this.mg.addExecution(build(tile)); + this.mg.addExecution( + new ConstructionExecution(this.player.id(), tile, type), + ); } private maybeSpawnWarship(): boolean { From 52542baa636006faf0ee86016ec621287b83f325 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Thu, 3 Apr 2025 01:35:29 -0400 Subject: [PATCH 07/11] Remove unnecessary casts --- src/core/execution/FakeHumanExecution.ts | 1 - src/core/game/PlayerImpl.ts | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index cb04d3bb4..fb2371909 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -177,7 +177,6 @@ export class FakeHumanExecution implements Execution { const enemies = enemiesWithTN .filter((o) => o.isPlayer()) - .map((o) => o as Player) .sort((a, b) => a.troops() - b.troops()); if (this.random.chance(20)) { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 9606e205b..85878ed77 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -447,12 +447,12 @@ export class PlayerImpl implements Player { this.mg.target(this, other); } - targets(): PlayerImpl[] { + targets(): Player[] { return this.targets_ .filter( (t) => this.mg.ticks() - t.tick < this.mg.config().targetDuration(), ) - .map((t) => t.target as PlayerImpl); + .map((t) => t.target); } transitiveTargets(): Player[] { @@ -759,7 +759,6 @@ export class PlayerImpl implements Player { nukeSpawn(tile: TileRef): TileRef | false { // only get missilesilos that are not on cooldown const spawns = this.units(UnitType.MissileSilo) - .map((u) => u as Unit) .filter((silo) => { return !silo.isCooldown(); }) From 8e9d6ce07e48adff4a5b4e4d0aea8fe513e018c3 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:45:18 -0400 Subject: [PATCH 08/11] refactor bot attacking --- src/core/execution/BotExecution.ts | 86 +++++++++++++++--------- src/core/execution/FakeHumanExecution.ts | 83 +++++++++++++---------- 2 files changed, 102 insertions(+), 67 deletions(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 90da61cd6..cc4b90f7a 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -13,16 +13,23 @@ import { AttackExecution } from "./AttackExecution"; export class BotExecution implements Execution { private active = true; private random: PseudoRandom; - private updateRate: number; - private updateTick: number; private mg: Game; private neighborsTerraNullius = true; + private enemy: Player; + private attackRate: number; + private attackTick: number; + private triggerRatio: number; + private reserveRatio: number; + constructor(private bot: Player) { this.random = new PseudoRandom(simpleHash(bot.id())); - this.updateRate = this.random.nextInt(10, 50); - this.updateTick = this.random.nextInt(0, this.updateRate); + this.attackRate = this.random.nextInt(10, 50); + this.attackTick = this.random.nextInt(0, this.attackRate); + this.triggerRatio = this.random.nextInt(20, 30); + this.reserveRatio = this.random.nextInt(10, 20); } + activeDuringSpawnPhase(): boolean { return false; } @@ -30,11 +37,10 @@ export class BotExecution implements Execution { init(mg: Game, ticks: number) { this.mg = mg; this.bot.setTargetTroopRatio(0.7); - // this.neighborsTerra = this.bot.neighbors().filter(n => n == this.gs.terraNullius()).length > 0 } tick(ticks: number) { - if (ticks % this.updateRate != this.updateTick) return; + if (ticks % this.attackRate != this.attackTick) return; if (!this.bot.isAlive()) { this.active = false; @@ -59,18 +65,6 @@ export class BotExecution implements Execution { } private maybeAttack() { - const traitors = this.bot - .neighbors() - .filter((n) => n.isPlayer() && n.isTraitor()) as Player[]; - if (traitors.length > 0) { - const toAttack = this.random.randElement(traitors); - const odds = this.bot.isFriendly(toAttack) ? 6 : 3; - if (this.random.chance(odds)) { - this.sendAttack(toAttack); - return; - } - } - if (this.neighborsTerraNullius) { for (const b of this.bot.borderTiles()) { for (const n of this.mg.neighbors(b)) { @@ -83,35 +77,65 @@ export class BotExecution implements Execution { this.neighborsTerraNullius = false; } - const border = Array.from(this.bot.borderTiles()) - .flatMap((t) => this.mg.neighbors(t)) - .filter((t) => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot); + if (this.enemy === null) { + // Save up troops until we reach the trigger ratio + const ratio = + this.bot.population() / this.mg.config().maxPopulation(this.bot); + if (ratio * 100 < this.triggerRatio) return; - if (border.length == 0) { - return; - } + // Select a new enemy + const traitors = this.bot + .neighbors() + .filter((n) => n.isPlayer() && n.isTraitor()) as Player[]; + if (traitors.length > 0) { + const toAttack = this.random.randElement(traitors); + const odds = this.bot.isFriendly(toAttack) ? 6 : 3; + if (this.random.chance(odds)) { + this.sendAttack(toAttack); + return; + } + } - const toAttack = border[this.random.nextInt(0, border.length)]; - const owner = this.mg.owner(toAttack); + const border = Array.from(this.bot.borderTiles()) + .flatMap((t) => this.mg.neighbors(t)) + .filter((t) => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot); - if (owner.isPlayer()) { - if (this.bot.isFriendly(owner)) { + if (border.length == 0) { return; } - if (owner.type() == PlayerType.FakeHuman) { + + const toAttack = border[this.random.nextInt(0, border.length)]; + const owner = this.mg.owner(toAttack); + if (!owner.isPlayer()) { + this.neighborsTerraNullius = true; + return; + } + this.enemy = owner; + } + + if (this.enemy) { + if (this.bot.isFriendly(this.enemy)) { + this.enemy = null; + return; + } + if (this.enemy.type() == PlayerType.FakeHuman) { if (!this.random.chance(2)) { return; } } + this.sendAttack(this.enemy); } - this.sendAttack(owner); } sendAttack(toAttack: Player | TerraNullius) { if (toAttack.isPlayer() && this.bot.isOnSameTeam(toAttack)) return; + const max = this.mg.config().maxPopulation(this.bot); + const target = (max * this.reserveRatio) / 100; + const troops = this.bot.population() - target; + if (troops < 1) return; this.mg.addExecution( new AttackExecution( - this.bot.troops() / 20, + troops, this.bot.id(), toAttack.isPlayer() ? toAttack.id() : null, ), diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index fb2371909..1561db5bf 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -38,15 +38,16 @@ export class FakeHumanExecution implements Execution { private player: Player = null; private enemy: Player | null = null; + private attackRate: number; + private attackTick: number; + private triggerRatio: number; + private reserveRatio: number; private lastEnemyUpdateTick: number = 0; private lastEmojiSent = new Map(); private lastNukeSent: [Tick, TileRef][] = []; private embargoMalusApplied = new Set(); - private attackRate: number; - private attackTick: number; - constructor( gameID: GameID, private playerInfo: PlayerInfo, @@ -56,6 +57,8 @@ export class FakeHumanExecution implements Execution { ); this.attackRate = this.random.nextInt(40, 80); this.attackTick = this.random.nextInt(0, this.attackRate); + this.triggerRatio = this.random.nextInt(45, 80); + this.reserveRatio = this.random.nextInt(20, 40); } init(mg: Game, ticks: number) { @@ -156,6 +159,14 @@ export class FakeHumanExecution implements Execution { (t) => this.mg.isLand(t) && this.mg.ownerID(t) != this.player.smallID(), ); + const enemiesWithTN = enemyborder.map((t) => + this.mg.playerBySmallID(this.mg.ownerID(t)), + ); + if (enemiesWithTN.filter((o) => !o.isPlayer()).length > 0) { + this.sendAttack(this.mg.terraNullius()); + return; + } + if (enemyborder.length == 0) { if (this.random.chance(5)) { this.sendBoat(); @@ -167,23 +178,17 @@ export class FakeHumanExecution implements Execution { return; } - const enemiesWithTN = enemyborder.map((t) => - this.mg.playerBySmallID(this.mg.ownerID(t)), - ); - if (enemiesWithTN.filter((o) => !o.isPlayer()).length > 0) { - this.sendAttack(this.mg.terraNullius()); - return; - } - const enemies = enemiesWithTN .filter((o) => o.isPlayer()) .sort((a, b) => a.troops() - b.troops()); + // 5% chance to send a random alliance request if (this.random.chance(20)) { - const toAlly = this.random.randElement(enemies); - if (this.player.canSendAllianceRequest(toAlly)) { - this.player.createAllianceRequest(toAlly); - return; + for (const toAlly of enemies) { + if (this.player.canSendAllianceRequest(toAlly)) { + this.player.createAllianceRequest(toAlly); + return; + } } } @@ -213,7 +218,7 @@ export class FakeHumanExecution implements Execution { } } - shouldDiscourageAttack(other: Player) { + private shouldDiscourageAttack(other: Player) { if (other.isTraitor()) { return false; } @@ -233,29 +238,31 @@ export class FakeHumanExecution implements Execution { this.enemy = null; } - const target = - this.player - .allies() - .filter((ally) => this.player.relation(ally) == Relation.Friendly) - .filter((ally) => ally.targets().length > 0) - .map((ally) => ({ ally: ally, t: ally.targets()[0] }))[0] ?? null; + outer: for (const ally of this.player.allies()) { + if (this.player.relation(ally) < Relation.Friendly) continue; + for (const target of ally.targets()) { + if (target === this.player) continue; + if (this.player.isAlliedWith(target)) continue; - if ( - target != null && - target.t != this.player && - !this.player.isAlliedWith(target.t) - ) { - this.player.updateRelation(target.ally, -20); - this.enemy = target.t; - this.lastEnemyUpdateTick = this.mg.ticks(); - if (target.ally.type() == PlayerType.Human) { - this.mg.addExecution( - new EmojiExecution(this.player.id(), target.ally.id(), "👍"), - ); + this.player.updateRelation(ally, -20); + this.enemy = target; + this.lastEnemyUpdateTick = this.mg.ticks(); + if (ally.type() == PlayerType.Human) { + this.mg.addExecution( + new EmojiExecution(this.player.id(), ally.id(), "👍"), + ); + } + break outer; } } - if (this.enemy == null) { + if (this.enemy === null) { + // Save up troops until we reach the trigger ratio + const ratio = + this.player.population() / this.mg.config().maxPopulation(this.player); + if (ratio * 100 < this.triggerRatio) return; + + // Choose a new enemy const mostHated = this.player.allRelationsSorted()[0] ?? null; if (mostHated != null && mostHated.relation == Relation.Hostile) { this.enemy = mostHated.player; @@ -652,9 +659,13 @@ export class FakeHumanExecution implements Execution { sendAttack(toAttack: Player | TerraNullius) { if (toAttack.isPlayer() && this.player.isOnSameTeam(toAttack)) return; + const max = this.mg.config().maxPopulation(this.player); + const target = (max * this.reserveRatio) / 100; + const troops = this.player.population() - target; + if (troops < 1) return; this.mg.addExecution( new AttackExecution( - this.player.troops() / 5, + troops, this.player.id(), toAttack.isPlayer() ? toAttack.id() : null, ), From b6dae6a70132eb5f6ba5bcc8e7de690778c28097 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Thu, 3 Apr 2025 20:37:51 -0400 Subject: [PATCH 09/11] fix build --- src/client/{gameStartingModal.ts => GameStartingModal.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/client/{gameStartingModal.ts => GameStartingModal.ts} (100%) diff --git a/src/client/gameStartingModal.ts b/src/client/GameStartingModal.ts similarity index 100% rename from src/client/gameStartingModal.ts rename to src/client/GameStartingModal.ts From 0a3397b8c6ebaf117882c0efaed15102c2610375 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Fri, 4 Apr 2025 00:06:41 -0400 Subject: [PATCH 10/11] wip --- src/core/execution/BotExecution.ts | 64 ++++++++++++++---------- src/core/execution/FakeHumanExecution.ts | 16 +++++- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index cc4b90f7a..b66f773ac 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -5,6 +5,7 @@ import { PlayerType, Relation, TerraNullius, + Tick, } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; import { simpleHash } from "../Util"; @@ -17,6 +18,7 @@ export class BotExecution implements Execution { private neighborsTerraNullius = true; private enemy: Player; + private lastEnemyUpdateTick: Tick; private attackRate: number; private attackTick: number; private triggerRatio: number; @@ -77,13 +79,42 @@ export class BotExecution implements Execution { this.neighborsTerraNullius = false; } + if (this.mg.ticks() - this.lastEnemyUpdateTick > 100) { + this.enemy = null; + } + + // Switch enemies if we're under attack + const incomingAttacks = this.bot.incomingAttacks(); + if (incomingAttacks.length > 0) { + this.enemy = incomingAttacks + .sort((a, b) => b.troops() - a.troops())[0] + .attacker(); + this.lastEnemyUpdateTick = this.mg.ticks(); + } + if (this.enemy === null) { // Save up troops until we reach the trigger ratio const ratio = this.bot.population() / this.mg.config().maxPopulation(this.bot); if (ratio * 100 < this.triggerRatio) return; - // Select a new enemy + // Choose a new enemy randomly + const border = Array.from(this.bot.borderTiles()) + .flatMap((t) => this.mg.neighbors(t)) + .filter((t) => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot); + if (border.length > 0) { + const toAttack = this.random.randElement(border); + const owner = this.mg.owner(toAttack); + if (!owner.isPlayer()) { + this.sendAttack(this.mg.terraNullius()); + this.neighborsTerraNullius = true; + return; + } + this.enemy = owner; + this.lastEnemyUpdateTick = this.mg.ticks(); + } + + // Select an allied traitor as an enemy const traitors = this.bot .neighbors() .filter((n) => n.isPlayer() && n.isTraitor()) as Player[]; @@ -91,26 +122,10 @@ export class BotExecution implements Execution { const toAttack = this.random.randElement(traitors); const odds = this.bot.isFriendly(toAttack) ? 6 : 3; if (this.random.chance(odds)) { - this.sendAttack(toAttack); - return; + this.enemy = toAttack; + this.lastEnemyUpdateTick = this.mg.ticks(); } } - - const border = Array.from(this.bot.borderTiles()) - .flatMap((t) => this.mg.neighbors(t)) - .filter((t) => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot); - - if (border.length == 0) { - return; - } - - const toAttack = border[this.random.nextInt(0, border.length)]; - const owner = this.mg.owner(toAttack); - if (!owner.isPlayer()) { - this.neighborsTerraNullius = true; - return; - } - this.enemy = owner; } if (this.enemy) { @@ -127,11 +142,12 @@ export class BotExecution implements Execution { } } - sendAttack(toAttack: Player | TerraNullius) { + private sendAttack(toAttack: Player | TerraNullius) { if (toAttack.isPlayer() && this.bot.isOnSameTeam(toAttack)) return; - const max = this.mg.config().maxPopulation(this.bot); + const max = + this.mg.config().maxPopulation(this.bot) * this.bot.targetTroopRatio(); const target = (max * this.reserveRatio) / 100; - const troops = this.bot.population() - target; + const troops = this.bot.troops() - target; if (troops < 1) return; this.mg.addExecution( new AttackExecution( @@ -142,10 +158,6 @@ export class BotExecution implements Execution { ); } - owner(): Player { - return this.bot; - } - isActive(): boolean { return this.active; } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 1561db5bf..df63c5d46 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -238,6 +238,16 @@ export class FakeHumanExecution implements Execution { this.enemy = null; } + // Switch enemies if we're under attack + const incomingAttacks = this.player.incomingAttacks(); + if (incomingAttacks.length > 0) { + this.enemy = incomingAttacks + .sort((a, b) => b.troops() - a.troops())[0] + .attacker(); + this.lastEnemyUpdateTick = this.mg.ticks(); + } + + // Attack allies' targets outer: for (const ally of this.player.allies()) { if (this.player.relation(ally) < Relation.Friendly) continue; for (const target of ally.targets()) { @@ -659,9 +669,11 @@ export class FakeHumanExecution implements Execution { sendAttack(toAttack: Player | TerraNullius) { if (toAttack.isPlayer() && this.player.isOnSameTeam(toAttack)) return; - const max = this.mg.config().maxPopulation(this.player); + const max = + this.mg.config().maxPopulation(this.player) * + this.player.targetTroopRatio(); const target = (max * this.reserveRatio) / 100; - const troops = this.player.population() - target; + const troops = this.player.troops() - target; if (troops < 1) return; this.mg.addExecution( new AttackExecution( From 11d7eaee990cc09a41b20bcc3a0b6c1a7bc6eb15 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Fri, 4 Apr 2025 20:40:36 -0400 Subject: [PATCH 11/11] wip --- src/core/execution/BotExecution.ts | 33 ++++++++++++++---------- src/core/execution/FakeHumanExecution.ts | 12 ++++----- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index b66f773ac..c518c5b1b 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -17,7 +17,7 @@ export class BotExecution implements Execution { private mg: Game; private neighborsTerraNullius = true; - private enemy: Player; + private enemy: Player | null = null; private lastEnemyUpdateTick: Tick; private attackRate: number; private attackTick: number; @@ -26,10 +26,10 @@ export class BotExecution implements Execution { constructor(private bot: Player) { this.random = new PseudoRandom(simpleHash(bot.id())); - this.attackRate = this.random.nextInt(10, 50); + this.attackRate = this.random.nextInt(40, 80); this.attackTick = this.random.nextInt(0, this.attackRate); - this.triggerRatio = this.random.nextInt(20, 30); - this.reserveRatio = this.random.nextInt(10, 20); + this.triggerRatio = this.random.nextInt(60, 90) / 100; + this.reserveRatio = this.random.nextInt(30, 60) / 100; } activeDuringSpawnPhase(): boolean { @@ -55,9 +55,12 @@ export class BotExecution implements Execution { private handleAllianceRequests() { this.bot.incomingAllianceRequests().forEach((ar) => { + const requestorIsMuchLarger = + ar.requestor().numTilesOwned() > this.bot.numTilesOwned() * 3; if ( ar.requestor().isTraitor() || - this.bot.relation(ar.requestor()) <= Relation.Distrustful + this.bot.relation(ar.requestor()) < Relation.Neutral || + (!requestorIsMuchLarger && ar.requestor().alliances().length >= 3) ) { ar.reject(); } else { @@ -94,14 +97,16 @@ export class BotExecution implements Execution { if (this.enemy === null) { // Save up troops until we reach the trigger ratio - const ratio = - this.bot.population() / this.mg.config().maxPopulation(this.bot); - if (ratio * 100 < this.triggerRatio) return; + const maxPop = this.mg.config().maxPopulation(this.bot); + const ratio = this.bot.population() / maxPop; + if (ratio < this.triggerRatio) return; // Choose a new enemy randomly const border = Array.from(this.bot.borderTiles()) .flatMap((t) => this.mg.neighbors(t)) - .filter((t) => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot); + .filter( + (t) => this.mg.isLand(t) && this.mg.ownerID(t) != this.bot.smallID(), + ); if (border.length > 0) { const toAttack = this.random.randElement(border); const owner = this.mg.owner(toAttack); @@ -114,7 +119,7 @@ export class BotExecution implements Execution { this.lastEnemyUpdateTick = this.mg.ticks(); } - // Select an allied traitor as an enemy + // Select a traitor as an enemy const traitors = this.bot .neighbors() .filter((n) => n.isPlayer() && n.isTraitor()) as Player[]; @@ -129,7 +134,7 @@ export class BotExecution implements Execution { } if (this.enemy) { - if (this.bot.isFriendly(this.enemy)) { + if (this.bot.isFriendly(this.enemy) && this.random.chance(50)) { this.enemy = null; return; } @@ -144,9 +149,9 @@ export class BotExecution implements Execution { private sendAttack(toAttack: Player | TerraNullius) { if (toAttack.isPlayer() && this.bot.isOnSameTeam(toAttack)) return; - const max = - this.mg.config().maxPopulation(this.bot) * this.bot.targetTroopRatio(); - const target = (max * this.reserveRatio) / 100; + const maxPop = this.mg.config().maxPopulation(this.bot); + const max = maxPop * this.bot.targetTroopRatio(); + const target = max * this.reserveRatio; const troops = this.bot.troops() - target; if (troops < 1) return; this.mg.addExecution( diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index df63c5d46..eed34f200 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -57,8 +57,8 @@ export class FakeHumanExecution implements Execution { ); this.attackRate = this.random.nextInt(40, 80); this.attackTick = this.random.nextInt(0, this.attackRate); - this.triggerRatio = this.random.nextInt(45, 80); - this.reserveRatio = this.random.nextInt(20, 40); + this.triggerRatio = this.random.nextInt(60, 90) / 100; + this.reserveRatio = this.random.nextInt(30, 60) / 100; } init(mg: Game, ticks: number) { @@ -268,9 +268,9 @@ export class FakeHumanExecution implements Execution { if (this.enemy === null) { // Save up troops until we reach the trigger ratio - const ratio = - this.player.population() / this.mg.config().maxPopulation(this.player); - if (ratio * 100 < this.triggerRatio) return; + const maxPop = this.mg.config().maxPopulation(this.player); + const ratio = this.player.population() / maxPop; + if (ratio < this.triggerRatio) return; // Choose a new enemy const mostHated = this.player.allRelationsSorted()[0] ?? null; @@ -672,7 +672,7 @@ export class FakeHumanExecution implements Execution { const max = this.mg.config().maxPopulation(this.player) * this.player.targetTroopRatio(); - const target = (max * this.reserveRatio) / 100; + const target = max * this.reserveRatio; const troops = this.player.troops() - target; if (troops < 1) return; this.mg.addExecution(