mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 02:05:40 +00:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user