mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 00:21:55 +00:00
meta3 with AI
This commit is contained in:
@@ -6,15 +6,21 @@ import { BotBehavior } from "./utils/BotBehavior";
|
||||
export class BotExecution implements Execution {
|
||||
private active = true;
|
||||
private random: PseudoRandom;
|
||||
private attackRate: number;
|
||||
private mg: Game;
|
||||
private neighborsTerraNullius = true;
|
||||
|
||||
private behavior: BotBehavior | null = null;
|
||||
private attackRate: number;
|
||||
private attackTick: number;
|
||||
private triggerRatio: number;
|
||||
private reserveRatio: number;
|
||||
|
||||
constructor(private bot: Player) {
|
||||
this.random = new PseudoRandom(simpleHash(bot.id()));
|
||||
this.attackRate = this.random.nextInt(10, 50);
|
||||
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;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
@@ -27,17 +33,21 @@ export class BotExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % this.attackRate != this.attackTick) return;
|
||||
|
||||
if (!this.bot.isAlive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticks % this.attackRate != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.behavior === null) {
|
||||
this.behavior = new BotBehavior(this.random, this.mg, this.bot, 1 / 20);
|
||||
this.behavior = new BotBehavior(
|
||||
this.random,
|
||||
this.mg,
|
||||
this.bot,
|
||||
this.triggerRatio,
|
||||
this.reserveRatio,
|
||||
);
|
||||
}
|
||||
|
||||
this.behavior.handleAllianceRequests();
|
||||
@@ -65,15 +75,14 @@ export class BotExecution implements Execution {
|
||||
this.neighborsTerraNullius = false;
|
||||
}
|
||||
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.checkIncomingAttacks();
|
||||
const enemy = this.behavior.selectRandomEnemy();
|
||||
if (!enemy) return;
|
||||
if (!this.bot.sharesBorderWith(enemy)) return;
|
||||
this.behavior.sendAttack(enemy);
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this.bot;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DefaultConfig } from "../configuration/DefaultConfig";
|
||||
import { consolex } from "../Consolex";
|
||||
import {
|
||||
Cell,
|
||||
@@ -36,6 +35,11 @@ export class FakeHumanExecution implements Execution {
|
||||
private mg: Game;
|
||||
private player: Player = null;
|
||||
|
||||
private attackRate: number;
|
||||
private attackTick: number;
|
||||
private triggerRatio: number;
|
||||
private reserveRatio: number;
|
||||
|
||||
private lastEmojiSent = new Map<Player, Tick>();
|
||||
private lastNukeSent: [Tick, TileRef][] = [];
|
||||
private embargoMalusApplied = new Set<PlayerID>();
|
||||
@@ -47,6 +51,10 @@ export class FakeHumanExecution implements Execution {
|
||||
this.random = new PseudoRandom(
|
||||
simpleHash(playerInfo.id) + simpleHash(gameID),
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
init(mg: Game) {
|
||||
@@ -97,17 +105,18 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % this.attackRate != this.attackTick) return;
|
||||
|
||||
if (this.mg.inSpawnPhase()) {
|
||||
if (ticks % this.random.nextInt(5, 30) == 0) {
|
||||
const rl = this.randomLand();
|
||||
if (rl == null) {
|
||||
consolex.warn(`cannot spawn ${this.playerInfo.name}`);
|
||||
return;
|
||||
}
|
||||
this.mg.addExecution(new SpawnExecution(this.playerInfo, rl));
|
||||
const rl = this.randomLand();
|
||||
if (rl == null) {
|
||||
consolex.warn(`cannot spawn ${this.playerInfo.name}`);
|
||||
return;
|
||||
}
|
||||
this.mg.addExecution(new SpawnExecution(this.playerInfo, rl));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player == null) {
|
||||
this.player = this.mg.players().find((p) => p.id() == this.playerInfo.id);
|
||||
if (this.player == null) {
|
||||
@@ -122,7 +131,13 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
if (this.behavior === null) {
|
||||
// Player is unavailable during init()
|
||||
this.behavior = new BotBehavior(this.random, this.mg, this.player, 1 / 5);
|
||||
this.behavior = new BotBehavior(
|
||||
this.random,
|
||||
this.mg,
|
||||
this.player,
|
||||
this.triggerRatio,
|
||||
this.reserveRatio,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.firstMove) {
|
||||
@@ -131,10 +146,6 @@ export class FakeHumanExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticks % this.random.nextInt(40, 80) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.player.troops() > 100_000 &&
|
||||
this.player.targetTroopRatio() > 0.7
|
||||
@@ -147,7 +158,10 @@ export class FakeHumanExecution implements Execution {
|
||||
this.handleEnemies();
|
||||
this.handleUnits();
|
||||
this.handleEmbargoesToHostileNations();
|
||||
this.maybeAttack();
|
||||
}
|
||||
|
||||
private maybeAttack() {
|
||||
const enemyborder = Array.from(this.player.borderTiles())
|
||||
.flatMap((t) => this.mg.neighbors(t))
|
||||
.filter(
|
||||
@@ -175,9 +189,9 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
const enemies = enemiesWithTN
|
||||
.filter((o) => o.isPlayer())
|
||||
.map((o) => o as Player)
|
||||
.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)) {
|
||||
@@ -195,43 +209,24 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private chanceScaled(n: number): boolean {
|
||||
const gameConfig = this.mg.config() as DefaultConfig;
|
||||
const maxPop = gameConfig.maxPopulation(this.player);
|
||||
const threshold = (this.player.targetTroopRatio() * maxPop) / 2;
|
||||
const troops = this.player.troops();
|
||||
|
||||
let scaledN = n;
|
||||
|
||||
if (troops < 0.25 * threshold) {
|
||||
return false; // no chance
|
||||
} else if (troops < 0.5 * threshold) {
|
||||
// scale smoothly from 0 to 1 as ratio goes from 0.25 to 0.5
|
||||
const ratio = (troops - 0.25 * threshold) / (0.25 * threshold); // in [0, 1]
|
||||
scaledN = Math.max(1, Math.round(n / ratio));
|
||||
}
|
||||
|
||||
return this.random.chance(scaledN);
|
||||
}
|
||||
|
||||
private shouldAttack(other: Player): boolean {
|
||||
if (this.player.isOnSameTeam(other)) {
|
||||
return false;
|
||||
}
|
||||
if (this.player.isFriendly(other)) {
|
||||
if (this.shouldDiscourageAttack(other)) {
|
||||
return this.chanceScaled(200);
|
||||
return this.random.chance(200);
|
||||
}
|
||||
return this.chanceScaled(50);
|
||||
return this.random.chance(50);
|
||||
} else {
|
||||
if (this.shouldDiscourageAttack(other)) {
|
||||
return this.chanceScaled(4);
|
||||
return this.random.chance(4);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
shouldDiscourageAttack(other: Player) {
|
||||
private shouldDiscourageAttack(other: Player) {
|
||||
if (other.isTraitor()) {
|
||||
return false;
|
||||
}
|
||||
@@ -247,6 +242,8 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
handleEnemies() {
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.checkIncomingAttacks();
|
||||
this.behavior.assistAllies();
|
||||
const enemy = this.behavior.selectEnemy();
|
||||
if (!enemy) return;
|
||||
@@ -277,8 +274,7 @@ export class FakeHumanExecution implements Execution {
|
||||
const silos = this.player.units(UnitType.MissileSilo);
|
||||
if (
|
||||
silos.length == 0 ||
|
||||
this.player.gold() <
|
||||
this.mg.config().unitInfo(UnitType.AtomBomb).cost(this.player) ||
|
||||
this.player.gold() < this.cost(UnitType.AtomBomb) ||
|
||||
other.type() == PlayerType.Bot ||
|
||||
this.player.isOnSameTeam(other)
|
||||
) {
|
||||
@@ -417,36 +413,21 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.maybeSpawnStructure(
|
||||
UnitType.City,
|
||||
2,
|
||||
(t) => new ConstructionExecution(this.player.id(), t, UnitType.City),
|
||||
);
|
||||
this.maybeSpawnStructure(UnitType.City, 2);
|
||||
if (this.maybeSpawnWarship()) {
|
||||
return;
|
||||
}
|
||||
if (!this.mg.config().disableNukes()) {
|
||||
this.maybeSpawnStructure(
|
||||
UnitType.MissileSilo,
|
||||
1,
|
||||
(t) =>
|
||||
new ConstructionExecution(this.player.id(), t, UnitType.MissileSilo),
|
||||
);
|
||||
this.maybeSpawnStructure(UnitType.MissileSilo, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private maybeSpawnStructure(
|
||||
type: UnitType,
|
||||
maxNum: number,
|
||||
build: (tile: TileRef) => Execution,
|
||||
) {
|
||||
private maybeSpawnStructure(type: UnitType, maxNum: number) {
|
||||
const units = this.player.units(type);
|
||||
if (units.length >= maxNum) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.player.gold() < this.mg.config().unitInfo(type).cost(this.player)
|
||||
) {
|
||||
if (this.player.gold() < this.cost(type)) {
|
||||
return;
|
||||
}
|
||||
const tile = this.randTerritoryTile(this.player);
|
||||
@@ -457,7 +438,9 @@ export class FakeHumanExecution implements Execution {
|
||||
if (canBuild == false) {
|
||||
return;
|
||||
}
|
||||
this.mg.addExecution(build(tile));
|
||||
this.mg.addExecution(
|
||||
new ConstructionExecution(this.player.id(), tile, type),
|
||||
);
|
||||
}
|
||||
|
||||
private maybeSpawnWarship(): boolean {
|
||||
|
||||
@@ -19,7 +19,8 @@ export class BotBehavior {
|
||||
private random: PseudoRandom,
|
||||
private game: Game,
|
||||
private player: Player,
|
||||
private attackRatio: number,
|
||||
private triggerRatio: number,
|
||||
private reserveRatio: number,
|
||||
) {}
|
||||
|
||||
handleAllianceRequests() {
|
||||
@@ -39,6 +40,24 @@ export class BotBehavior {
|
||||
);
|
||||
}
|
||||
|
||||
forgetOldEnemies() {
|
||||
// Forget old enemies
|
||||
if (this.game.ticks() - this.enemyUpdated > 100) {
|
||||
this.enemy = null;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
assistAllies() {
|
||||
outer: for (const ally of this.player.allies()) {
|
||||
if (ally.targets().length === 0) continue;
|
||||
@@ -66,9 +85,11 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
selectEnemy(): Player | null {
|
||||
// Forget old enemies
|
||||
if (this.game.ticks() - this.enemyUpdated > 100) {
|
||||
this.enemy = 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;
|
||||
}
|
||||
|
||||
// Prefer neighboring bots
|
||||
@@ -100,24 +121,54 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
selectRandomEnemy(): Player | TerraNullius | null {
|
||||
const neighbors = this.player.neighbors();
|
||||
for (const neighbor of this.random.shuffleArray(neighbors)) {
|
||||
if (neighbor.isPlayer()) {
|
||||
if (this.player.isFriendly(neighbor)) continue;
|
||||
if (neighbor.type() == PlayerType.FakeHuman) {
|
||||
if (this.random.chance(2)) {
|
||||
continue;
|
||||
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;
|
||||
|
||||
// Choose a new enemy randomly
|
||||
const neighbors = this.player.neighbors();
|
||||
for (const neighbor of this.random.shuffleArray(neighbors)) {
|
||||
if (neighbor.isPlayer()) {
|
||||
if (this.player.isFriendly(neighbor)) continue;
|
||||
if (neighbor.type() == PlayerType.FakeHuman) {
|
||||
if (this.random.chance(2)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.enemy = neighbor;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
}
|
||||
|
||||
// Select a traitor as an enemy
|
||||
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();
|
||||
}
|
||||
}
|
||||
return neighbor;
|
||||
}
|
||||
return null;
|
||||
|
||||
// Sanity check, don't attack our allies or teammates
|
||||
if (this.enemy && this.player.isFriendly(this.enemy)) {
|
||||
this.enemy = null;
|
||||
}
|
||||
return this.enemy;
|
||||
}
|
||||
|
||||
sendAttack(target: Player | TerraNullius) {
|
||||
if (target.isPlayer() && this.player.isOnSameTeam(target)) return;
|
||||
const troops = this.player.troops() * this.attackRatio;
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const maxTroops = maxPop * this.player.targetTroopRatio();
|
||||
const targetTroops = maxTroops * this.reserveRatio;
|
||||
const troops = this.player.troops() - targetTroops;
|
||||
if (troops < 1) return;
|
||||
this.game.addExecution(
|
||||
new AttackExecution(
|
||||
|
||||
@@ -469,12 +469,12 @@ export class PlayerImpl implements Player {
|
||||
this.mg.target(this, other);
|
||||
}
|
||||
|
||||
targets(): PlayerImpl[] {
|
||||
targets(): Player[] {
|
||||
return this.targets_
|
||||
.filter(
|
||||
(t) => this.mg.ticks() - t.tick < this.mg.config().targetDuration(),
|
||||
)
|
||||
.map((t) => t.target as PlayerImpl);
|
||||
.map((t) => t.target);
|
||||
}
|
||||
|
||||
transitiveTargets(): Player[] {
|
||||
@@ -809,7 +809,6 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
// only get missilesilos that are not on cooldown
|
||||
const spawns = this.units(UnitType.MissileSilo)
|
||||
.map((u) => u as Unit)
|
||||
.filter((silo) => {
|
||||
return !silo.isCooldown();
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user