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, ),