From a8a8d0b7ca8f0547e036d2926e2e70613cda1103 Mon Sep 17 00:00:00 2001 From: 1brucben <1benjbruce@gmail.com> Date: Wed, 21 May 2025 18:26:25 +0200 Subject: [PATCH] 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 --- src/core/execution/AttackExecution.ts | 81 +++++++++++---------- src/core/execution/utils/FlatBinaryHeap.ts | 84 ++++++++++++++++++++++ 2 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 src/core/execution/utils/FlatBinaryHeap.ts diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index d095d1a21..485db7ad9 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -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 = - new PriorityQueue((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, - ) {} -} diff --git a/src/core/execution/utils/FlatBinaryHeap.ts b/src/core/execution/utils/FlatBinaryHeap.ts new file mode 100644 index 000000000..627195613 --- /dev/null +++ b/src/core/execution/utils/FlatBinaryHeap.ts @@ -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(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; + } +}