Combine Bot and Nation behaviors in to a shared class (#434)

This is the first move in the effort to combine the redundant logic that
exists between BotExecution and FakeHumonExecution.

This commit:
1. Combines the alliance request handler, moving bots to use the same
logic as nations for acceptance.
2. Combines the sendAttack() functions, which may later be reworked.
3. Introduces selectEnemy() function to wrap enemy selection logic that
nations use.
4. Blocks nations from nuking bots.
5. Alters enemy selection to prefer neighboring bots if there are any.

## Description:

Fixes #467 

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

fake.neo

---------

Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
Scott Anderson
2025-04-17 19:37:16 -07:00
committed by GitHub
co-authored by Scott Anderson
parent ccadd7f6a8
commit 1b672420b3
4 changed files with 195 additions and 168 deletions
+21 -58
View File
@@ -1,13 +1,7 @@
import {
Execution,
Game,
Player,
PlayerType,
TerraNullius,
} from "../game/Game";
import { Execution, Game, Player } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import { AttackExecution } from "./AttackExecution";
import { BotBehavior } from "./utils/BotBehavior";
export class BotExecution implements Execution {
private active = true;
@@ -16,18 +10,20 @@ export class BotExecution implements Execution {
private mg: Game;
private neighborsTerraNullius = true;
private behavior: BotBehavior | null = null;
constructor(private bot: Player) {
this.random = new PseudoRandom(simpleHash(bot.id()));
this.attackRate = this.random.nextInt(10, 50);
}
activeDuringSpawnPhase(): boolean {
return false;
}
init(mg: Game, ticks: number) {
init(mg: Game) {
this.mg = mg;
this.bot.setTargetTroopRatio(0.7);
// this.neighborsTerra = this.bot.neighbors().filter(n => n == this.gs.terraNullius()).length > 0
}
tick(ticks: number) {
@@ -40,14 +36,15 @@ export class BotExecution implements Execution {
return;
}
this.bot.incomingAllianceRequests().forEach((ar) => {
if (ar.requestor().isTraitor()) {
ar.reject();
} else {
ar.accept();
}
});
if (this.behavior === null) {
this.behavior = new BotBehavior(this.random, this.mg, this.bot, 1 / 20);
}
this.behavior.handleAllianceRequests();
this.maybeAttack();
}
private maybeAttack() {
const traitors = this.bot
.neighbors()
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
@@ -55,56 +52,22 @@ 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);
this.behavior.sendAttack(toAttack);
return;
}
}
if (this.neighborsTerraNullius) {
for (const b of this.bot.borderTiles()) {
for (const n of this.mg.neighbors(b)) {
if (!this.mg.hasOwner(n) && this.mg.isLand(n)) {
this.sendAttack(this.mg.terraNullius());
return;
}
}
if (this.bot.sharesBorderWith(this.mg.terraNullius())) {
this.behavior.sendAttack(this.mg.terraNullius());
return;
}
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 (border.length == 0) {
return;
}
const toAttack = border[this.random.nextInt(0, border.length)];
const owner = this.mg.owner(toAttack);
if (owner.isPlayer()) {
if (this.bot.isFriendly(owner)) {
return;
}
if (owner.type() == PlayerType.FakeHuman) {
if (!this.random.chance(2)) {
return;
}
}
}
this.sendAttack(owner);
}
sendAttack(toAttack: Player | TerraNullius) {
if (toAttack.isPlayer() && this.bot.isOnSameTeam(toAttack)) return;
this.mg.addExecution(
new AttackExecution(
this.bot.troops() / 20,
this.bot.id(),
toAttack.isPlayer() ? toAttack.id() : null,
),
);
const enemy = this.behavior.selectRandomEnemy();
if (!enemy) return;
this.behavior.sendAttack(enemy);
}
owner(): Player {