mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 18:33:07 +00:00
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
This commit is contained in:
@@ -14,13 +14,15 @@ export class BotExecution implements Execution {
|
||||
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(30, 60) / 100;
|
||||
this.reserveRatio = this.random.nextInt(20, 30) / 100;
|
||||
this.expandRatio = this.random.nextInt(10, 20) / 100;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
@@ -47,7 +49,12 @@ export class BotExecution implements Execution {
|
||||
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();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Cell,
|
||||
Difficulty,
|
||||
Execution,
|
||||
Game,
|
||||
Gold,
|
||||
@@ -27,8 +26,6 @@ import { closestTwoTiles } from "./Util";
|
||||
import { BotBehavior } from "./utils/BotBehavior";
|
||||
|
||||
export class FakeHumanExecution implements Execution {
|
||||
private firstMove = true;
|
||||
|
||||
private active = true;
|
||||
private random: PseudoRandom;
|
||||
private behavior: BotBehavior | null = null;
|
||||
@@ -39,6 +36,7 @@ export class FakeHumanExecution implements Execution {
|
||||
private attackTick: number;
|
||||
private triggerRatio: number;
|
||||
private reserveRatio: number;
|
||||
private expandRatio: number;
|
||||
|
||||
private lastEmojiSent = new Map<Player, Tick>();
|
||||
private lastNukeSent: [Tick, TileRef][] = [];
|
||||
@@ -56,6 +54,7 @@ export class FakeHumanExecution implements Execution {
|
||||
this.attackTick = this.random.nextInt(0, this.attackRate);
|
||||
this.triggerRatio = this.random.nextInt(60, 90) / 100;
|
||||
this.reserveRatio = this.random.nextInt(30, 60) / 100;
|
||||
this.expandRatio = this.random.nextInt(15, 25) / 100;
|
||||
this.heckleEmoji = ["🤡", "😡"].map((e) => flattenedEmojiTable.indexOf(e));
|
||||
}
|
||||
|
||||
@@ -145,11 +144,10 @@ export class FakeHumanExecution implements Execution {
|
||||
this.player,
|
||||
this.triggerRatio,
|
||||
this.reserveRatio,
|
||||
this.expandRatio,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.firstMove) {
|
||||
this.firstMove = false;
|
||||
// Send an attack on the first tick
|
||||
this.behavior.sendAttack(this.mg.terraNullius());
|
||||
return;
|
||||
}
|
||||
@@ -163,7 +161,6 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
this.updateRelationsFromEmbargos();
|
||||
this.behavior.handleAllianceRequests();
|
||||
this.handleEnemies();
|
||||
this.handleUnits();
|
||||
this.handleEmbargoesToHostileNations();
|
||||
this.maybeAttack();
|
||||
@@ -191,80 +188,30 @@ export class FakeHumanExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
const enemiesWithTN = enemyborder.map((t) =>
|
||||
const borderPlayers = enemyborder.map((t) =>
|
||||
this.mg.playerBySmallID(this.mg.ownerID(t)),
|
||||
);
|
||||
if (enemiesWithTN.filter((o) => !o.isPlayer()).length > 0) {
|
||||
if (borderPlayers.some((o) => !o.isPlayer())) {
|
||||
this.behavior.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;
|
||||
}
|
||||
}
|
||||
|
||||
// 50-50 attack weakest player vs random player
|
||||
const toAttack = this.random.chance(2)
|
||||
? enemies[0]
|
||||
: this.random.randElement(enemies);
|
||||
if (this.shouldAttack(toAttack)) {
|
||||
this.behavior.sendAttack(toAttack);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldAttack(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (this.player.isOnSameTeam(other)) {
|
||||
return false;
|
||||
}
|
||||
if (this.player.isFriendly(other)) {
|
||||
if (this.shouldDiscourageAttack(other)) {
|
||||
return this.random.chance(200);
|
||||
}
|
||||
return this.random.chance(50);
|
||||
} else {
|
||||
if (this.shouldDiscourageAttack(other)) {
|
||||
return this.random.chance(4);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldDiscourageAttack(other: Player) {
|
||||
if (other.isTraitor()) {
|
||||
return false;
|
||||
}
|
||||
const difficulty = this.mg.config().gameConfig().difficulty;
|
||||
if (
|
||||
difficulty === Difficulty.Hard ||
|
||||
difficulty === Difficulty.Impossible
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (other.type() !== PlayerType.Human) {
|
||||
return false;
|
||||
}
|
||||
// Only discourage attacks on Humans who are not traitors on easy or medium difficulty.
|
||||
return true;
|
||||
}
|
||||
|
||||
handleEnemies() {
|
||||
if (this.player === null || this.behavior === null) {
|
||||
throw new Error("not initialized");
|
||||
}
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.assistAllies();
|
||||
const enemy = this.behavior.selectEnemy();
|
||||
if (!enemy) return;
|
||||
if (!enemy) {
|
||||
// 5% chance to send a random alliance request
|
||||
if (this.random.chance(20)) {
|
||||
const enemies = borderPlayers
|
||||
.filter((o) => o.isPlayer())
|
||||
.sort((a, b) => a.troops() - b.troops());
|
||||
const toAlly = this.random.randElement(enemies);
|
||||
if (this.player.canSendAllianceRequest(toAlly)) {
|
||||
this.player.createAllianceRequest(toAlly);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.maybeSendEmoji(enemy);
|
||||
this.maybeSendNuke(enemy);
|
||||
if (this.player.sharesBorderWith(enemy)) {
|
||||
@@ -561,7 +508,7 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
const src = this.random.randElement(oceanShore);
|
||||
|
||||
const dst = this.randOceanShoreTile(src, 150);
|
||||
const dst = this.randomBoatTarget(src, 150);
|
||||
if (dst === null) {
|
||||
return;
|
||||
}
|
||||
@@ -603,7 +550,7 @@ export class FakeHumanExecution implements Execution {
|
||||
return null;
|
||||
}
|
||||
|
||||
private randOceanShoreTile(tile: TileRef, dist: number): TileRef | null {
|
||||
private randomBoatTarget(tile: TileRef, dist: number): TileRef | null {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const x = this.mg.x(tile);
|
||||
const y = this.mg.y(tile);
|
||||
@@ -614,7 +561,7 @@ export class FakeHumanExecution implements Execution {
|
||||
continue;
|
||||
}
|
||||
const randTile = this.mg.ref(randX, randY);
|
||||
if (!this.mg.isOceanShore(randTile)) {
|
||||
if (!this.mg.isLand(randTile)) {
|
||||
continue;
|
||||
}
|
||||
const owner = this.mg.owner(randTile);
|
||||
|
||||
@@ -18,14 +18,13 @@ export class BotBehavior {
|
||||
|
||||
private assistAcceptEmoji = flattenedEmojiTable.indexOf("👍");
|
||||
|
||||
private firstAttackSent = false;
|
||||
|
||||
constructor(
|
||||
private random: PseudoRandom,
|
||||
private game: Game,
|
||||
private player: Player,
|
||||
private triggerRatio: number,
|
||||
private reserveRatio: number,
|
||||
private expandRatio: number,
|
||||
) {}
|
||||
|
||||
handleAllianceRequests() {
|
||||
@@ -211,14 +210,12 @@ export class BotBehavior {
|
||||
if (target.isPlayer() && this.player.isOnSameTeam(target)) return;
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const maxTroops = maxPop * this.player.targetTroopRatio();
|
||||
const targetTroops = maxTroops * this.reserveRatio;
|
||||
// Don't wait until it has sufficient reserves to send the first attack
|
||||
// to prevent the bot from waiting too long at the start of the game.
|
||||
const troops = this.firstAttackSent
|
||||
? this.player.troops() - targetTroops
|
||||
: this.player.troops() / 5;
|
||||
const reserveRatio = target.isPlayer()
|
||||
? this.reserveRatio
|
||||
: this.expandRatio;
|
||||
const targetTroops = maxTroops * reserveRatio;
|
||||
const troops = this.player.troops() - targetTroops;
|
||||
if (troops < 1) return;
|
||||
this.firstAttackSent = true;
|
||||
this.game.addExecution(
|
||||
new AttackExecution(
|
||||
troops,
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("BotBehavior.handleAllianceRequests", () => {
|
||||
|
||||
const random = new PseudoRandom(42);
|
||||
|
||||
botBehavior = new BotBehavior(random, game, player, 0.5, 0.5);
|
||||
botBehavior = new BotBehavior(random, game, player, 0.5, 0.5, 0.2);
|
||||
});
|
||||
|
||||
function setupAllianceRequest({
|
||||
|
||||
Reference in New Issue
Block a user