ai builds defense posts.

This commit is contained in:
1brucben
2025-04-24 02:26:19 +02:00
parent 67cf20cd65
commit 5b60fc8498
3 changed files with 180 additions and 28 deletions
+11 -11
View File
@@ -473,16 +473,16 @@ export class DefaultConfig implements Config {
const defenderdensity = defenderTroops / defenderTiles;
const adjustedRatio = within(defenderTroops / attackTroops, 0.3, 20);
if (attacker.type() == PlayerType.Human) {
console.log(
"speed:",
4 *
within(defenderdensity, 3, 90) ** 0.6 *
adjustedRatio ** 0.7 *
speed,
);
console.log("density", defenderdensity);
}
// if (attacker.type() == PlayerType.Human) {
// console.log(
// "speed:",
// 4 *
// within(defenderdensity, 3, 90) ** 0.6 *
// adjustedRatio ** 0.7 *
// speed,
// );
// console.log("density", defenderdensity);
// }
return {
attackerTroopLoss:
mag * 10 +
@@ -551,7 +551,7 @@ export class DefaultConfig implements Config {
case Difficulty.Easy:
return 2_500 * (playerInfo?.nation?.strength ?? 1);
case Difficulty.Medium:
return 5_000 * (playerInfo?.nation?.strength ?? 1);
return 12_000 + 2000 * (playerInfo?.nation?.strength ?? 1);
case Difficulty.Hard:
return 20_000 * (playerInfo?.nation?.strength ?? 1);
case Difficulty.Impossible:
+151 -14
View File
@@ -44,6 +44,12 @@ export class FakeHumanExecution implements Execution {
private lastNukeSent: [Tick, TileRef][] = [];
private embargoMalusApplied = new Set<PlayerID>();
private portTargetRatio: number = 0.0003; // desired ports per tile
private cityTargetRatio: number = 0.0006; // desired cities per tile
private defensePostSpacing: number = 40; // minimum distance between defense posts
private defensePostTargetRatio: number = 0.001; // desired defense posts per tile
private lastDefensePostTick: number = -9999;
constructor(
gameID: GameID,
private playerInfo: PlayerInfo,
@@ -54,7 +60,7 @@ export class FakeHumanExecution implements Execution {
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(40, 60) / 100;
}
init(mg: Game) {
@@ -142,7 +148,7 @@ export class FakeHumanExecution implements Execution {
if (this.firstMove) {
this.firstMove = false;
this.behavior.sendAttack(this.mg.terraNullius());
this.behavior.sendAttack(this.mg.terraNullius(), true);
return;
}
@@ -400,23 +406,154 @@ export class FakeHumanExecution implements Execution {
}
private handleUnits() {
const ports = this.player.units(UnitType.Port);
if (ports.length == 0 && this.player.gold() > this.cost(UnitType.Port)) {
const oceanTiles = Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
);
if (oceanTiles.length > 0) {
const buildTile = this.random.randElement(oceanTiles);
this.mg.addExecution(
new ConstructionExecution(this.player.id(), buildTile, UnitType.Port),
);
const currentTick = this.mg.ticks();
const portsCount = this.player.units(UnitType.Port).length;
const citiesCount = this.player.units(UnitType.City).length;
const tilesCount = this.player.numTilesOwned();
const portRatio = portsCount / tilesCount;
const cityRatio = citiesCount / tilesCount;
const portDeficit = Math.max(
(this.portTargetRatio - portRatio) / this.portTargetRatio,
portsCount < 1 ? 1 : 0,
);
const cityDeficit = Math.max(
(this.cityTargetRatio - cityRatio) / this.cityTargetRatio,
citiesCount < 3 ? 1 : 0,
);
const canAffordPort = this.player.gold() >= this.cost(UnitType.Port);
const canAffordCity = this.player.gold() >= this.cost(UnitType.City);
const oceanTiles = Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
);
const canBuildPort = canAffordPort && oceanTiles.length > 0;
const canBuildCity =
canAffordCity && this.randTerritoryTile(this.player) !== null;
if (portDeficit > 0 || cityDeficit > 0) {
if (cityDeficit >= portDeficit && canBuildCity) {
const tile = this.randTerritoryTile(this.player);
if (tile) {
this.mg.addExecution(
new ConstructionExecution(this.player.id(), tile, UnitType.City),
);
return;
}
}
if (portDeficit > cityDeficit && canBuildPort) {
const tile = this.random.randElement(oceanTiles);
this.mg.addExecution(
new ConstructionExecution(this.player.id(), tile, UnitType.Port),
);
return;
}
// fallback: if port was preferred but unbuildable, try city
if (portDeficit > cityDeficit && !canBuildPort && canBuildCity) {
const tile = this.randTerritoryTile(this.player);
if (tile) {
this.mg.addExecution(
new ConstructionExecution(this.player.id(), tile, UnitType.City),
);
return;
}
}
}
if (currentTick - this.lastDefensePostTick >= 10) {
this.lastDefensePostTick = currentTick;
const defensePostsCount = this.player.units(UnitType.DefensePost).length;
const defensePostRatio = defensePostsCount / tilesCount;
const defensePostDeficit = this.defensePostTargetRatio - defensePostRatio;
const canAffordDefensePost =
this.player.gold() >= this.cost(UnitType.DefensePost);
// consolex.log(`[${this.playerInfo.name}] can afford: ${canAffordDefensePost}, deficit: ${defensePostDeficit}`);
if (defensePostDeficit > 0 && canAffordDefensePost) {
consolex.log("creating defense post");
const borderTiles = new Set(this.player.borderTiles());
const existingPosts = this.player
.units(UnitType.DefensePost)
.map((u) => u.tile());
const candidateTiles: TileRef[] = [];
const radius = 10;
const borderTileArray = Array.from(borderTiles);
this.random.shuffleArray(borderTileArray);
const sampledBorderTiles = borderTileArray.slice(0, 5); // <-- scan 3 border tiles only
for (const borderTile of sampledBorderTiles) {
const x0 = this.mg.x(borderTile);
const y0 = this.mg.y(borderTile);
tileScan: for (let dx = -radius; dx <= radius; dx++) {
for (let dy = -radius; dy <= radius; dy++) {
const x = x0 + dx;
const y = y0 + dy;
if (!this.mg.isValidCoord(x, y)) continue;
const tile = this.mg.ref(x, y);
if (this.mg.owner(tile) !== this.player) continue;
if (borderTiles.has(tile)) continue;
let tooCloseToBorder = false;
for (const borderTile of borderTiles) {
const distSq = this.mg.euclideanDistSquared(tile, borderTile);
if (distSq < 25) {
// must be at least 5 tiles away
tooCloseToBorder = true;
break;
}
}
if (tooCloseToBorder) continue;
let nearOtherPost = false;
for (const postTile of existingPosts) {
const distSq = this.mg.euclideanDistSquared(tile, postTile);
if (
distSq <
this.defensePostSpacing * this.defensePostSpacing
) {
nearOtherPost = true;
break;
}
}
if (nearOtherPost) continue;
if (this.player.canBuild(UnitType.DefensePost, tile)) {
candidateTiles.push(tile);
if (candidateTiles.length >= 5) break tileScan;
}
}
}
if (candidateTiles.length > 0) {
const tile = this.random.randElement(candidateTiles);
this.mg.addExecution(
new ConstructionExecution(
this.player.id(),
tile,
UnitType.DefensePost,
),
);
return;
} else {
consolex.log(
`[${this.playerInfo.name}] no valid tile found for Defense Post`,
);
}
}
}
return;
}
this.maybeSpawnStructure(UnitType.City, 2);
if (this.maybeSpawnWarship()) {
return;
}
if (!this.mg.config().disableNukes()) {
this.maybeSpawnStructure(UnitType.MissileSilo, 1);
}
+18 -3
View File
@@ -8,6 +8,7 @@ import {
Tick,
} from "../../game/Game";
import { PseudoRandom } from "../../PseudoRandom";
import { within } from "../../Util";
import { AttackExecution } from "../AttackExecution";
import { EmojiExecution } from "../EmojiExecution";
@@ -163,13 +164,27 @@ export class BotBehavior {
return this.enemy;
}
sendAttack(target: Player | TerraNullius) {
sendAttack(target: Player | TerraNullius, force: boolean = false) {
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;
const troops = this.player.troops() - targetTroops;
if (troops < 1) return;
let troops: number;
if (force) {
// send exactly 40% of current troops
troops = this.player.troops() * 0.4;
if (troops < 1) return;
} else {
troops = within(
this.player.troops() - targetTroops,
0.05 * this.player.troops(),
0.4 * this.player.troops(),
);
if (troops < 1) return;
}
this.game.addExecution(
new AttackExecution(
troops,