mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:30:43 +00:00
Update MIRV target selection algorithm (#2765)
## Description:
`MIRVExecution.separate` is consuming more resources than it needs to.
This PR introduces a series of minor performance changes which do not
alter the behavior of the selection algorithm. Assessing the code
initially, I was convinced there are multiple easy wins - starting with
the proximity check. However, after many hours of mostly math, no
alternative solution came close to the speed of current implementation.
Therefore this PR consists of only a few minor tweaks:
#### Removed `console.log`
This is by far the worst offender, in my test removing the three lines
of console log improved execution time from ~30ms down to ~10ms. The
logs are not very useful. I do not see a clear pattern in the logs
produced by the application therefore they were completely removed for
now. If there is a need for the log in production build, I suggest
adding single line with the number of destinations selected.
```diff
- console.log(`dsts: ${dsts.length}`);
- console.log(`got ${dsts.length} dsts!!`);
- console.log("couldn't find place, giving up");
```
#### Replaced multiple calls to `random.nextInt` with single call to
`random.next`
The flamechart shows calling pseudo random number generator is
expensive. Therefore instead of calling it twice, the code now generates
a single random number and derives further calculations from it. It
remains 100% deterministic and there should not be any noticeable change
to the enthrophy. This saves about ~1.5ms in my tests.
```diff
- const x = this.random.nextInt(
- this.mg.x(ref) - this.mirvRange,
- this.mg.x(ref) + this.mirvRange,
- );
- const y = this.random.nextInt(
- this.mg.y(ref) - this.mirvRange,
- this.mg.y(ref) + this.mirvRange,
- );
+ const r1 = this.random.next();
+ const r2 = (r1 * 15485863) % 1;
+ const x = Math.round(r1 * this.range * 2 - this.range + this.baseX);
+ const y = Math.round(r2 * this.range * 2 - this.range + this.baseY);
```
#### Caching of destination coordinates
Since the target tile coordinates are used a lot, instead of retrieving
them every time with `this.mg.x` and `this.mg.y`, they get cached as
`baseX` and `baseY`. To reduce usage further, I also exposed `x` and `y`
to `isOverlapping` / `proximityCheck` directly instead of passing the
tile. Since available methods operate on `TileRef`, this change requires
the calculations - manhattan and euclidean distance - to be inlined. I
do not think this is a big issue, considering this code is responsible
for very specific task. This saves another ~1.5ms in my tests.
```diff
- if (this.mg.euclideanDistSquared(tile, ref) > mirvRange2) {
+ if ((x - this.baseX) ** 2 + (y - this.baseY) ** 2 > this.rangeSquared) {
```
## Benchmark:
**Before**
```
=== MIRV Performance Benchmark Results ===
MIRV target selection - sparse territory x 53.53 ops/sec ±0.48% (71 runs sampled)
MIRV target selection - dense territory x 53.39 ops/sec ±0.57% (70 runs sampled)
MIRV target selection - giant world map (350 targets) x 1,129 ops/sec ±0.98% (90 runs sampled)
```
**After**
```
=== MIRV Performance Benchmark Results ===
MIRV target selection - sparse territory x 198 ops/sec ±0.39% (85 runs sampled)
MIRV target selection - dense territory x 200 ops/sec ±0.28% (86 runs sampled)
MIRV target selection - giant world map (350 targets) x 1,409 ops/sec ±0.89% (92 runs sampled)
```
## Please complete the following:
- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory*
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
\* Tests in separate PR, implemented against master:
https://github.com/openfrontio/OpenFrontIO/pull/2767
## Please put your Discord username so you can be contacted if a bug or
regression is found:
moleole
This commit is contained in:
committed by
GitHub
parent
6887ae598f
commit
4877e202f6
@@ -20,9 +20,14 @@ export class MirvExecution implements Execution {
|
||||
|
||||
private nuke: Unit | null = null;
|
||||
|
||||
private mirvRange = 1500;
|
||||
private range = 1500;
|
||||
private rangeSquared = this.range * this.range;
|
||||
private minimumSpread = 55;
|
||||
private warheadCount = 350;
|
||||
|
||||
private baseX: number;
|
||||
private baseY: number;
|
||||
|
||||
private random: PseudoRandom;
|
||||
|
||||
private pathFinder: ParabolaPathFinder;
|
||||
@@ -98,25 +103,15 @@ export class MirvExecution implements Execution {
|
||||
}
|
||||
|
||||
private separate() {
|
||||
if (this.nuke === null) throw new Error("uninitialized");
|
||||
const dsts: TileRef[] = [this.dst];
|
||||
let attempts = 1000;
|
||||
while (attempts > 0 && dsts.length < this.warheadCount) {
|
||||
attempts--;
|
||||
const potential = this.randomLand(this.dst, dsts);
|
||||
if (potential === null) {
|
||||
continue;
|
||||
}
|
||||
dsts.push(potential);
|
||||
if (this.nuke === null) {
|
||||
throw new Error("uninitialized");
|
||||
}
|
||||
console.log(`dsts: ${dsts.length}`);
|
||||
dsts.sort(
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(b, this.dst) - this.mg.manhattanDist(a, this.dst),
|
||||
);
|
||||
console.log(`got ${dsts.length} dsts!!`);
|
||||
|
||||
for (const [i, dst] of dsts.entries()) {
|
||||
this.baseX = this.mg.x(this.dst);
|
||||
this.baseY = this.mg.y(this.dst);
|
||||
|
||||
const destinations = this.selectDestinations();
|
||||
for (const [i, dst] of destinations.entries()) {
|
||||
this.mg.addExecution(
|
||||
new NukeExecution(
|
||||
UnitType.MIRVWarhead,
|
||||
@@ -132,47 +127,67 @@ export class MirvExecution implements Execution {
|
||||
this.nuke.delete(false);
|
||||
}
|
||||
|
||||
randomLand(ref: TileRef, taken: TileRef[]): TileRef | null {
|
||||
let tries = 0;
|
||||
const mirvRange2 = this.mirvRange * this.mirvRange;
|
||||
while (tries < 100) {
|
||||
tries++;
|
||||
const x = this.random.nextInt(
|
||||
this.mg.x(ref) - this.mirvRange,
|
||||
this.mg.x(ref) + this.mirvRange,
|
||||
);
|
||||
const y = this.random.nextInt(
|
||||
this.mg.y(ref) - this.mirvRange,
|
||||
this.mg.y(ref) + this.mirvRange,
|
||||
);
|
||||
private selectDestinations(): TileRef[] {
|
||||
const targets: TileRef[] = [this.dst];
|
||||
|
||||
for (let attempt = 0; attempt < 1000; attempt++) {
|
||||
const target = this.tryGenerateTarget(targets);
|
||||
if (target) targets.push(target);
|
||||
if (targets.length >= this.warheadCount) break;
|
||||
}
|
||||
|
||||
return targets.sort(
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(b, this.dst) - this.mg.manhattanDist(a, this.dst),
|
||||
);
|
||||
}
|
||||
|
||||
private tryGenerateTarget(taken: TileRef[]): TileRef | undefined {
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
const r1 = this.random.next();
|
||||
const r2 = (r1 * 15485863) % 1;
|
||||
|
||||
const x = Math.round(r1 * this.range * 2 - this.range + this.baseX);
|
||||
const y = Math.round(r2 * this.range * 2 - this.range + this.baseY);
|
||||
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tile = this.mg.ref(x, y);
|
||||
|
||||
if (!this.mg.isLand(tile)) {
|
||||
continue;
|
||||
}
|
||||
if (this.mg.euclideanDistSquared(tile, ref) > mirvRange2) {
|
||||
|
||||
if ((x - this.baseX) ** 2 + (y - this.baseY) ** 2 > this.rangeSquared) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.mg.owner(tile) !== this.targetPlayer) {
|
||||
continue;
|
||||
}
|
||||
if (this.proximityCheck(tile, taken)) {
|
||||
|
||||
if (this.isOverlapping(x, y, taken)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return tile;
|
||||
}
|
||||
console.log("couldn't find place, giving up");
|
||||
return null;
|
||||
}
|
||||
|
||||
private proximityCheck(tile: TileRef, taken: TileRef[]): boolean {
|
||||
for (const t of taken) {
|
||||
if (this.mg.manhattanDist(tile, t) < 55) {
|
||||
private isOverlapping(x: number, y: number, taken: TileRef[]): boolean {
|
||||
for (const existingTile of taken) {
|
||||
const existingTileX = this.mg.x(existingTile);
|
||||
const existingTileY = this.mg.y(existingTile);
|
||||
const manhattanDistance =
|
||||
Math.abs(x - existingTileX) + Math.abs(y - existingTileY);
|
||||
|
||||
if (manhattanDistance < this.minimumSpread) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user