meta3 with AI

This commit is contained in:
1brucben
2025-04-23 23:46:01 +02:00
4 changed files with 129 additions and 87 deletions
+20 -11
View File
@@ -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;
}
+42 -59
View File
@@ -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 {
+65 -14
View File
@@ -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(
+2 -3
View File
@@ -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();
})