Merge branch 'main' into local-attack

This commit is contained in:
Aotumuri
2026-02-06 14:10:03 +09:00
committed by GitHub
42 changed files with 1526 additions and 1311 deletions
+11 -1
View File
@@ -1,7 +1,8 @@
import { Execution, Game, Player } from "../game/Game";
import { Execution, Game, isStructureType, Player } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
import { DeleteUnitExecution } from "./DeleteUnitExecution";
import { AiAttackBehavior } from "./utils/AiAttackBehavior";
export class BotExecution implements Execution {
@@ -58,6 +59,7 @@ export class BotExecution implements Execution {
}
this.acceptAllAllianceRequests();
this.deleteAllStructures();
this.maybeAttack();
}
@@ -80,6 +82,14 @@ export class BotExecution implements Execution {
}
}
private deleteAllStructures() {
for (const unit of this.bot.units()) {
if (isStructureType(unit.type()) && this.bot.canDeleteUnit()) {
this.mg.addExecution(new DeleteUnitExecution(this.bot, unit.id()));
}
}
}
private maybeAttack() {
if (this.attackBehavior === null) {
throw new Error("not initialized");
+29 -18
View File
@@ -1,5 +1,5 @@
import { Config } from "../configuration/Config";
import { Execution, Game, Player, UnitType } from "../game/Game";
import { Cell, Execution, Game, Player, UnitType } from "../game/Game";
import { TileRef } from "../game/GameMap";
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
@@ -139,11 +139,12 @@ export class PlayerExecution implements Execution {
const largestCluster = clusters[largestIndex];
if (largestCluster === undefined) throw new Error("No clusters");
this.player.largestClusterBoundingBox = calculateBoundingBox(
this.mg,
const largestClusterBox = calculateBoundingBox(this.mg, largestCluster);
this.player.largestClusterBoundingBox = largestClusterBox;
const surroundedBy = this.surroundedBySamePlayer(
largestCluster,
largestClusterBox,
);
const surroundedBy = this.surroundedBySamePlayer(largestCluster);
if (surroundedBy && !surroundedBy.isFriendly(this.player)) {
this.removeCluster(largestCluster);
}
@@ -158,7 +159,10 @@ export class PlayerExecution implements Execution {
}
}
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
private surroundedBySamePlayer(
cluster: Set<TileRef>,
clusterBox: { min: Cell; max: Cell },
): false | Player {
const enemies = new Set<number>();
for (const tile of cluster) {
let hasUnownedNeighbor = false;
@@ -187,7 +191,6 @@ export class PlayerExecution implements Execution {
}
const enemy = this.mg.playerBySmallID(Array.from(enemies)[0]) as Player;
const enemyBox = calculateBoundingBox(this.mg, enemy.borderTiles());
const clusterBox = calculateBoundingBox(this.mg, cluster);
if (inscribed(enemyBox, clusterBox)) {
return enemy;
}
@@ -195,7 +198,11 @@ export class PlayerExecution implements Execution {
}
private isSurrounded(cluster: Set<TileRef>): boolean {
const enemyTiles = new Set<TileRef>();
let hasEnemy = false;
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
for (const tr of cluster) {
if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) {
return false;
@@ -203,27 +210,31 @@ export class PlayerExecution implements Execution {
this.mg.forEachNeighbor(tr, (n) => {
const owner = this.mg.owner(n);
if (owner.isPlayer() && this.mg.ownerID(n) !== this.player.smallID()) {
enemyTiles.add(n);
hasEnemy = true;
const x = this.mg.x(n);
const y = this.mg.y(n);
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
});
}
if (enemyTiles.size === 0) {
if (!hasEnemy) {
return false;
}
const enemyBox = calculateBoundingBox(this.mg, enemyTiles);
const clusterBox = calculateBoundingBox(this.mg, cluster);
const enemyBox = { min: new Cell(minX, minY), max: new Cell(maxX, maxY) };
return inscribed(enemyBox, clusterBox);
}
private removeCluster(cluster: Set<TileRef>) {
if (
Array.from(cluster).some(
(t) => this.mg?.ownerID(t) !== this.player?.smallID(),
)
) {
// Other removeCluster operations could change tile owners,
// so double check.
return;
for (const t of cluster) {
if (this.mg?.ownerID(t) !== this.player?.smallID()) {
// Other removeCluster operations could change tile owners,
// so double check.
return;
}
}
const capturing = this.getCapturingPlayer(cluster);
+19 -17
View File
@@ -45,7 +45,9 @@ export class TrainStationExecution implements Execution {
this.active = false;
return;
}
this.spawnTrain(this.station, ticks);
if (this.spawnTrains) {
this.spawnTrain(this.station, ticks);
}
}
private shouldSpawnTrain(): boolean {
@@ -69,8 +71,8 @@ export class TrainStationExecution implements Execution {
if (cluster === null) {
return;
}
const availableForTrade = cluster.availableForTrade(this.unit.owner());
if (availableForTrade.size === 0) {
const owner = this.unit.owner();
if (!cluster.hasAnyTradeDestination(owner)) {
return;
}
if (!this.shouldSpawnTrain()) {
@@ -79,20 +81,20 @@ export class TrainStationExecution implements Execution {
// Pick a destination randomly.
// Could be improved to pick a lucrative trip
const destination: TrainStation =
this.random.randFromSet(availableForTrade);
if (destination !== station) {
this.mg.addExecution(
new TrainExecution(
this.mg.railNetwork(),
this.unit.owner(),
station,
destination,
this.numCars,
),
);
this.lastSpawnTick = currentTick;
}
const destination = cluster.randomTradeDestination(owner, this.random);
if (destination === null) return;
if (destination === station) return;
this.mg.addExecution(
new TrainExecution(
this.mg.railNetwork(),
owner,
station,
destination,
this.numCars,
),
);
this.lastSpawnTick = currentTick;
}
activeDuringSpawnPhase(): boolean {