Files
OpenFrontIO/src/core/execution/BotExecution.ts
T
Scott Anderson 2e442c9c29 Add expand ratio to bot behavior class (#1376)
## Description:

- Create a new expand ratio that allows AI players to expand with a much
lower reserve ratio than the normal attack reserve ratio.
- Unify the implementation of the first attack between bots and nations.
- Bugfix: Multiple attacks per tick could cause nations to full-send.
- Improve the chance of finding a place to boat to by allowing nations
to target non-shore land tiles.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [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
2025-07-08 18:39:11 -07:00

96 lines
2.5 KiB
TypeScript

import { Execution, Game, Player } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import { BotBehavior } from "./utils/BotBehavior";
export class BotExecution implements Execution {
private active = true;
private random: PseudoRandom;
private mg: Game;
private neighborsTerraNullius = true;
private behavior: BotBehavior | null = null;
private attackRate: number;
private attackTick: number;
private triggerRatio: number;
private reserveRatio: number;
private expandRatio: number;
constructor(private bot: Player) {
this.random = new PseudoRandom(simpleHash(bot.id()));
this.attackRate = this.random.nextInt(40, 80);
this.attackTick = this.random.nextInt(0, this.attackRate);
this.triggerRatio = this.random.nextInt(60, 90) / 100;
this.reserveRatio = this.random.nextInt(20, 30) / 100;
this.expandRatio = this.random.nextInt(10, 20) / 100;
}
activeDuringSpawnPhase(): boolean {
return false;
}
init(mg: Game) {
this.mg = mg;
this.bot.setTargetTroopRatio(0.7);
}
tick(ticks: number) {
if (ticks % this.attackRate !== this.attackTick) return;
if (!this.bot.isAlive()) {
this.active = false;
return;
}
if (this.behavior === null) {
this.behavior = new BotBehavior(
this.random,
this.mg,
this.bot,
this.triggerRatio,
this.reserveRatio,
this.expandRatio,
);
// Send an attack on the first tick
this.behavior.sendAttack(this.mg.terraNullius());
return;
}
this.behavior.handleAllianceRequests();
this.maybeAttack();
}
private maybeAttack() {
if (this.behavior === null) {
throw new Error("not initialized");
}
const toAttack = this.behavior.getNeighborTraitorToAttack();
if (toAttack !== null) {
const odds = this.bot.isFriendly(toAttack) ? 6 : 3;
if (this.random.chance(odds)) {
this.behavior.sendAttack(toAttack);
return;
}
}
if (this.neighborsTerraNullius) {
if (this.bot.sharesBorderWith(this.mg.terraNullius())) {
this.behavior.sendAttack(this.mg.terraNullius());
return;
}
this.neighborsTerraNullius = false;
}
this.behavior.forgetOldEnemies();
const enemy = this.behavior.selectRandomEnemy();
if (!enemy) return;
if (!this.bot.sharesBorderWith(enemy)) return;
this.behavior.sendAttack(enemy);
}
isActive(): boolean {
return this.active;
}
}