Performance Enhancement for AttackExecution (#820)

## Description:
The branch includes some improvements in AttackExecution including
caching and improved queueing of tiles.
## Please complete the following:

- [x] I have added screenshots for all UI updates
- [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:

1brucben
This commit is contained in:
1brucben
2025-05-21 18:26:25 +02:00
committed by GitHub
parent 772b72dfef
commit a8a8d0b7ca
2 changed files with 126 additions and 39 deletions
+42 -39
View File
@@ -1,4 +1,3 @@
import { PriorityQueue } from "@datastructures-js/priority-queue";
import { renderNumber, renderTroops } from "../../client/Utils";
import {
Attack,
@@ -13,16 +12,14 @@ import {
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { PseudoRandom } from "../PseudoRandom";
import { FlatBinaryHeap } from "./utils/FlatBinaryHeap"; // adjust path if needed
const malusForRetreat = 25;
export class AttackExecution implements Execution {
private breakAlliance = false;
private active: boolean = true;
private toConquer: PriorityQueue<TileContainer> =
new PriorityQueue<TileContainer>((a: TileContainer, b: TileContainer) => {
return a.priority - b.priority;
});
private toConquer = new FlatBinaryHeap();
private random = new PseudoRandom(123);
private _owner: Player;
@@ -196,9 +193,12 @@ export class AttackExecution implements Execution {
if (this.attack === null) {
throw new Error("Attack not initialized");
}
let troopCount = this.attack.troops(); // cache troop count
const targetIsPlayer = this.target.isPlayer(); // cache target type
const targetPlayer = targetIsPlayer ? (this.target as Player) : null; // cache target player
if (this.attack.retreated()) {
if (this.attack.target().isPlayer()) {
if (targetIsPlayer) {
this.retreat(malusForRetreat);
} else {
this.retreat();
@@ -216,12 +216,14 @@ export class AttackExecution implements Execution {
return;
}
const alliance = this._owner.allianceWith(this.target as Player);
const alliance = targetPlayer
? this._owner.allianceWith(targetPlayer)
: null;
if (this.breakAlliance && alliance !== null) {
this.breakAlliance = false;
this._owner.breakAlliance(alliance);
}
if (this.target.isPlayer() && this._owner.isAlliedWith(this.target)) {
if (targetPlayer && this._owner.isAlliedWith(targetPlayer)) {
// In this case a new alliance was created AFTER the attack started.
this.retreat();
return;
@@ -230,14 +232,14 @@ export class AttackExecution implements Execution {
let numTilesPerTick = this.mg
.config()
.attackTilesPerTick(
this.attack.troops(),
troopCount,
this._owner,
this.target,
this.border.size + this.random.nextInt(0, 5),
);
while (numTilesPerTick > 0) {
if (this.attack.troops() < 1) {
if (troopCount < 1) {
this.attack.delete();
this.active = false;
return;
@@ -249,13 +251,16 @@ export class AttackExecution implements Execution {
return;
}
const tileToConquer = this.toConquer.dequeue().tile;
const [tileToConquer] = this.toConquer.dequeue();
this.border.delete(tileToConquer);
const onBorder =
this.mg
.neighbors(tileToConquer)
.filter((t) => this.mg.owner(t) === this._owner).length > 0;
let onBorder = false;
for (const n of this.mg.neighbors(tileToConquer)) {
if (this.mg.owner(n) === this._owner) {
onBorder = true;
break;
}
}
if (this.mg.owner(tileToConquer) !== this.target || !onBorder) {
continue;
}
@@ -264,15 +269,16 @@ export class AttackExecution implements Execution {
.config()
.attackLogic(
this.mg,
this.attack.troops(),
troopCount,
this._owner,
this.target,
tileToConquer,
);
numTilesPerTick -= tilesPerTickUsed;
this.attack.setTroops(this.attack.troops() - attackerTroopLoss);
if (this.target.isPlayer()) {
this.target.removeTroops(defenderTroopLoss);
troopCount -= attackerTroopLoss;
this.attack.setTroops(troopCount);
if (targetPlayer) {
targetPlayer.removeTroops(defenderTroopLoss);
}
this._owner.conquer(tileToConquer);
this.handleDeadDefender();
@@ -280,6 +286,8 @@ export class AttackExecution implements Execution {
}
private addNeighbors(tile: TileRef) {
const tickNow = this.mg.ticks(); // cache tick
for (const neighbor of this.mg.neighbors(tile)) {
if (
this.mg.isWater(neighbor) ||
@@ -288,11 +296,15 @@ export class AttackExecution implements Execution {
continue;
}
this.border.add(neighbor);
const numOwnedByMe = this.mg
.neighbors(neighbor)
.filter((t) => this.mg.owner(t) === this._owner).length;
let numOwnedByMe = 0;
for (const n of this.mg.neighbors(neighbor)) {
if (this.mg.owner(n) === this._owner) {
numOwnedByMe++;
}
}
let mag = 0;
switch (this.mg.terrainType(tile)) {
switch (this.mg.terrainType(neighbor)) {
case TerrainType.Plains:
mag = 1;
break;
@@ -303,14 +315,12 @@ export class AttackExecution implements Execution {
mag = 2;
break;
}
this.toConquer.enqueue(
new TileContainer(
neighbor,
(this.random.nextInt(0, 7) + 10) *
(1 - numOwnedByMe * 0.5 + mag / 2) +
this.mg.ticks(),
),
);
const priority =
(this.random.nextInt(0, 7) + 10) * (1 - numOwnedByMe * 0.5 + mag / 2) +
tickNow;
this.toConquer.enqueue(neighbor, priority);
}
}
@@ -356,10 +366,3 @@ export class AttackExecution implements Execution {
return this.active;
}
}
class TileContainer {
constructor(
public readonly tile: TileRef,
public readonly priority: number,
) {}
}
@@ -0,0 +1,84 @@
import { TileRef } from "../../game/GameMap";
/**
* Lightweight min-heap specialised for (priority:number, tile:TileRef) pairs.
* - priorities stored in a contiguous Float32Array
* - tiles stored in a parallel object array
*/
export class FlatBinaryHeap {
/** parallel arrays: pri[ i ] is the priority of tiles[ i ] */
private pri: Float32Array;
private tiles: TileRef[];
private len = 0; // current number of elements
constructor(capacity = 1024) {
this.pri = new Float32Array(capacity);
this.tiles = new Array<TileRef>(capacity);
}
/** remove every element without reallocating */
clear(): void {
this.len = 0;
}
/** current heap size */
size(): number {
return this.len;
}
//insert tiles
enqueue(tile: TileRef, priority: number): void {
if (this.len === this.pri.length) this.grow(); // ensure space
let i = this.len++;
/* sift-up */
while (i > 0) {
const parent = (i - 1) >> 1;
if (priority >= this.pri[parent]) break;
this.pri[i] = this.pri[parent];
this.tiles[i] = this.tiles[parent];
i = parent;
}
this.pri[i] = priority;
this.tiles[i] = tile;
}
//remove tiles
dequeue(): [TileRef, number] {
if (this.len === 0) throw new Error("heap empty");
const topTile = this.tiles[0];
const topPri = this.pri[0];
const lastPri = this.pri[--this.len];
const lastTile = this.tiles[this.len];
/* sift-down */
let i = 0;
while (true) {
const left = (i << 1) + 1;
if (left >= this.len) break;
const right = left + 1;
const child =
right < this.len && this.pri[right] < this.pri[left] ? right : left;
if (lastPri <= this.pri[child]) break;
this.pri[i] = this.pri[child];
this.tiles[i] = this.tiles[child];
i = child;
}
this.pri[i] = lastPri;
this.tiles[i] = lastTile;
return [topTile, topPri];
}
/** double the underlying storage */
private grow(): void {
const newCap = this.pri.length << 1;
const newPri = new Float32Array(newCap);
newPri.set(this.pri);
this.pri = newPri;
this.tiles.length = newCap;
}
}