mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:20:43 +00:00
Optimizations for botbehaviour (#1114)
## Description: Some optimizations for bot & fakehuman execution. Better performance measured using profiling, at the start of a game or in singple player, but since the circumstances differ it is not a very reliable comparison. --- In BotExecution: Added getNeighborTraitorToAttack in Botbehavior to use in maybeAttack. No caching for playerNeighbors array, because although it could be re-used up to two times in selectRandomEnemy, maybeAttack runs every tick and selectEnemy only every 10 seconds so the cache overhead would not be worth it. --- In Botbehavior: Put repeated code in dedicated functions for readability, maintainance ease, and reduce the chances of forgetting to update the timestamp for enemyUpdated. Can be seen as a follow-up to PRs #434 and #946. -- In both selectEnemy and selectRandomEnemy: Put the first if this.enemy === null statement parenthesis, around the two checks below. Because if there's already an enemy (if forgetEnemies hasn't nullified it, enemy === null is false at the first if statement), then enemy === null will still be false for the two checks below the first. No need to check it thrice then. Once inside the first if statement (when enemy === null is true), then we need to check it two times more in case the code inside already set an enemy. -- In selectEnemy under 'Prefer neighboring bots': Removed unneccessary check for enemy === null. Replaced sort with more performant for loop. -- In selectRandomEnemy: Under 'Select a traitor as an enemy', if a traitor is a friendly player, they got less odds to be chosen as enemy over an unfriendly player, but they weren't ruled out. While below in the Sanity Check, we specifically rule out friendly players as enemy and set enemy=null. So excluded friendly players under 'Select a traitor as an enemy' instead of only giving them lower odds. Use the new shared method getNeighborTraitorToAttack. -- In checkIncomingAttacks, since we're only interested in the max value, replaced sort with loop that makes a single pass through the attacks to find the largest one. -- For shouldAcceptAllianceRequest, #1049 added tests and improved readability of Alliance requests. It may have also deoptimized it a bit. Const notTooManyAlliances would, in the past, not be computed if requestorIsMuchLarger was already found to be true. Since the readability changes, tooManyAlliances was always computed regardless of the value of requestorIsMuchLarger. This PR attempts to address this too, while still keeping it as readable hopefully. Also choose for early returns to optimize a bit more. So if isTraitor is true, don't compute any further and just return false immediately etc. Switched the order of testing for noMalice and isTraitor. Traitor status used to be throughout the game, now it's only 30 seconds or will be maybe 45 seconds to 1 minute. So the chances of someone asking for alliance and being a traitor at the same time have decreased. isMalice chances could be bigger. Removed the named constants and choose to put them in comments, so readability should be about the same as #1049 intended. Kept the braces for the if statements because otherwise Prettier would misalign the comments.) ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33
This commit is contained in:
@@ -58,11 +58,8 @@ export class BotExecution implements Execution {
|
||||
if (this.behavior === null) {
|
||||
throw new Error("not initialized");
|
||||
}
|
||||
const traitors = this.bot
|
||||
.neighbors()
|
||||
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
|
||||
if (traitors.length > 0) {
|
||||
const toAttack = this.random.randElement(traitors);
|
||||
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);
|
||||
|
||||
@@ -43,22 +43,48 @@ export class BotBehavior {
|
||||
this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji));
|
||||
}
|
||||
|
||||
private setNewEnemy(newEnemy: Player | null) {
|
||||
this.enemy = newEnemy;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
}
|
||||
|
||||
private clearEnemy() {
|
||||
this.enemy = null;
|
||||
}
|
||||
|
||||
forgetOldEnemies() {
|
||||
// Forget old enemies
|
||||
if (this.game.ticks() - this.enemyUpdated > 100) {
|
||||
this.enemy = null;
|
||||
this.clearEnemy();
|
||||
}
|
||||
}
|
||||
|
||||
private hasSufficientTroops(): boolean {
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const ratio = this.player.population() / maxPop;
|
||||
return ratio >= this.triggerRatio;
|
||||
}
|
||||
|
||||
private checkIncomingAttacks() {
|
||||
// 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.enemyUpdated = this.game.ticks();
|
||||
let largestAttack = 0;
|
||||
let largestAttacker: Player | undefined;
|
||||
for (const attack of incomingAttacks) {
|
||||
if (attack.troops() <= largestAttack) continue;
|
||||
largestAttack = attack.troops();
|
||||
largestAttacker = attack.attacker();
|
||||
}
|
||||
if (largestAttacker !== undefined) {
|
||||
this.setNewEnemy(largestAttacker);
|
||||
}
|
||||
}
|
||||
|
||||
getNeighborTraitorToAttack(): Player | null {
|
||||
const traitors = this.player
|
||||
.neighbors()
|
||||
.filter((n): n is Player => n.isPlayer() && n.isTraitor());
|
||||
return traitors.length > 0 ? this.random.randElement(traitors) : null;
|
||||
}
|
||||
|
||||
assistAllies() {
|
||||
@@ -79,8 +105,7 @@ export class BotBehavior {
|
||||
}
|
||||
// All checks passed, assist them
|
||||
this.player.updateRelation(ally, -20);
|
||||
this.enemy = target;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
this.setNewEnemy(target);
|
||||
this.emoji(ally, this.assistAcceptEmoji);
|
||||
break outer;
|
||||
}
|
||||
@@ -90,50 +115,55 @@ export class BotBehavior {
|
||||
selectEnemy(): Player | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const ratio = this.player.population() / maxPop;
|
||||
if (ratio < this.triggerRatio) return null;
|
||||
}
|
||||
if (!this.hasSufficientTroops()) return null;
|
||||
|
||||
// Prefer neighboring bots
|
||||
if (this.enemy === null) {
|
||||
// Prefer neighboring bots
|
||||
const bots = this.player
|
||||
.neighbors()
|
||||
.filter((n) => n.isPlayer() && n.type() === PlayerType.Bot) as Player[];
|
||||
if (bots.length > 0) {
|
||||
const density = (p: Player) => p.troops() / p.numTilesOwned();
|
||||
this.enemy = bots.sort((a, b) => density(a) - density(b))[0];
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
let lowestDensityBot: Player | undefined;
|
||||
let lowestDensity = Infinity;
|
||||
|
||||
for (const bot of bots) {
|
||||
const currentDensity = density(bot);
|
||||
if (currentDensity < lowestDensity) {
|
||||
lowestDensity = currentDensity;
|
||||
lowestDensityBot = bot;
|
||||
}
|
||||
}
|
||||
|
||||
if (lowestDensityBot !== undefined) {
|
||||
this.setNewEnemy(lowestDensityBot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
|
||||
// Select the most hated player
|
||||
if (this.enemy === null) {
|
||||
const mostHated = this.player.allRelationsSorted()[0];
|
||||
if (mostHated !== undefined && mostHated.relation === Relation.Hostile) {
|
||||
this.enemy = mostHated.player;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
// Select the most hated player
|
||||
if (this.enemy === null) {
|
||||
const mostHated = this.player.allRelationsSorted()[0];
|
||||
if (
|
||||
mostHated !== undefined &&
|
||||
mostHated.relation === Relation.Hostile
|
||||
) {
|
||||
this.setNewEnemy(mostHated.player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check, don't attack our allies or teammates
|
||||
if (this.enemy && this.player.isFriendly(this.enemy)) {
|
||||
this.enemy = null;
|
||||
}
|
||||
return this.enemy;
|
||||
return this.enemySanityCheck();
|
||||
}
|
||||
|
||||
selectRandomEnemy(): Player | TerraNullius | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const ratio = this.player.population() / maxPop;
|
||||
if (ratio < this.triggerRatio) return null;
|
||||
if (!this.hasSufficientTroops()) return null;
|
||||
|
||||
// Choose a new enemy randomly
|
||||
const neighbors = this.player.neighbors();
|
||||
@@ -145,34 +175,32 @@ export class BotBehavior {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.enemy = neighbor;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
this.setNewEnemy(neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
|
||||
// Select a traitor as an enemy
|
||||
if (this.enemy === null) {
|
||||
const traitors = this.player
|
||||
.neighbors()
|
||||
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
|
||||
if (traitors.length > 0) {
|
||||
const toAttack = this.random.randElement(traitors);
|
||||
const odds = this.player.isFriendly(toAttack) ? 6 : 3;
|
||||
if (this.random.chance(odds)) {
|
||||
this.enemy = toAttack;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
// Select a traitor as an enemy
|
||||
if (this.enemy === null) {
|
||||
const toAttack = this.getNeighborTraitorToAttack();
|
||||
if (toAttack !== null) {
|
||||
if (!this.player.isFriendly(toAttack) && this.random.chance(3)) {
|
||||
this.setNewEnemy(toAttack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check, don't attack our allies or teammates
|
||||
return this.enemySanityCheck();
|
||||
}
|
||||
|
||||
private enemySanityCheck(): Player | null {
|
||||
if (this.enemy && this.player.isFriendly(this.enemy)) {
|
||||
this.enemy = null;
|
||||
this.clearEnemy();
|
||||
}
|
||||
return this.enemy;
|
||||
}
|
||||
@@ -200,12 +228,17 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
function shouldAcceptAllianceRequest(player: Player, request: AllianceRequest) {
|
||||
const isTraitor = request.requestor().isTraitor();
|
||||
const hasMalice = player.relation(request.requestor()) < Relation.Neutral;
|
||||
const requestorIsMuchLarger =
|
||||
request.requestor().numTilesOwned() > player.numTilesOwned() * 3;
|
||||
const tooManyAlliances = request.requestor().alliances().length >= 3;
|
||||
return (
|
||||
!isTraitor && !hasMalice && (requestorIsMuchLarger || !tooManyAlliances)
|
||||
);
|
||||
if (player.relation(request.requestor()) < Relation.Neutral) {
|
||||
return false; // Reject if hasMalice
|
||||
}
|
||||
if (request.requestor().isTraitor()) {
|
||||
return false; // Reject if isTraitor
|
||||
}
|
||||
if (request.requestor().numTilesOwned() > player.numTilesOwned() * 3) {
|
||||
return true; // Accept if requestorIsMuchLarger
|
||||
}
|
||||
if (request.requestor().alliances().length >= 3) {
|
||||
return false; // Reject if tooManyAlliances
|
||||
}
|
||||
return true; // Accept otherwise
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user