From ec09e1849de4a3a6de74ebfc73978578824de71a Mon Sep 17 00:00:00 2001 From: rortan134 Date: Sun, 7 Jul 2024 22:38:51 +0200 Subject: [PATCH 1/6] improve perf --- athena/MapData.tsx | 9 ++- athena/Radius.tsx | 136 +++++++++++++++++++++++------------------- athena/info/Tile.tsx | 2 +- athena/map/Vector.tsx | 5 +- 4 files changed, 84 insertions(+), 68 deletions(-) diff --git a/athena/MapData.tsx b/athena/MapData.tsx index 900eb696..7a11dce7 100644 --- a/athena/MapData.tsx +++ b/athena/MapData.tsx @@ -271,19 +271,18 @@ export default class MapData { : null; } - getTileInfo(vector: Vector, layer?: TileLayer) { + getTileInfo(vector: Vector, layer?: TileLayer, index?: number) { if (!this.contains(vector)) { throw new Error( `getTileInfo: Vector '${vector.x},${vector.y}' is not within the map limits of width '${this.size.width}' and height '${this.size.height}'.`, ); } - - return getTileInfo(this.map[this.getTileIndex(vector)], layer); + return getTileInfo(this.map[index ?? this.getTileIndex(vector)], layer); } - maybeGetTileInfo(vector: Vector, layer?: TileLayer) { + maybeGetTileInfo(vector: Vector, layer?: TileLayer, index?: number) { if (this.contains(vector)) { - return getTileInfo(this.map[this.getTileIndex(vector)], layer); + return getTileInfo(this.map[index ?? this.getTileIndex(vector)], layer); } } diff --git a/athena/Radius.tsx b/athena/Radius.tsx index 38b08ed5..7c58edfe 100644 --- a/athena/Radius.tsx +++ b/athena/Radius.tsx @@ -11,7 +11,7 @@ import Vector from './map/Vector.tsx'; import MapData from './MapData.tsx'; type RadiusConfiguration = { - getCost(map: MapData, unit: Unit, vector: Vector): number; + getCost(map: MapData, unit: Unit, vector: Vector, index?: number): number; getResourceValue(unit: Unit): number; getTransitionCost( info: UnitInfo, @@ -37,38 +37,54 @@ export const RadiusItem = ( vector, }); -function isAccessibleBase(map: MapData, unit: Unit, vector: Vector) { - if (!map.contains(vector)) { - return false; +const cacheMap = new Map(); + +function getCostBase(map: MapData, unit: Unit, vector: Vector, index?: number) { + const tileInfo = map.maybeGetTileInfo(vector, undefined, index); + return tileInfo ? tileInfo.getMovementCost(unit.info) : -1; +} + +function getTransitionCostBase( + info: UnitInfo, + current: TileInfo, + parent: TileInfo, +) { + if (parent.group !== current.group) { + return parent.getTransitionCost(info) + current.getTransitionCost(info); } + return 0; +} +function isAccessibleBase(map: MapData, unit: Unit, vector: Vector) { const unitB = map.units.get(vector); if (unitB && map.isOpponent(unitB, unit)) { return false; } - const building = map.buildings.get(vector); - if (building && !building.info.isAccessibleBy(unit.info)) { - return false; - } + return !(building && !building.info.isAccessibleBy(unit.info)); +} - return true; +function isAccessible(map: MapData, unit: Unit, vector: Vector) { + const key = vector.valueOf(); + let accessible = cacheMap.get(key); + if (accessible != null) { + return accessible; + } + accessible = isAccessibleBase(map, unit, vector); + cacheMap.set(key, accessible); + return accessible; } export const MoveConfiguration = { - getCost: (map: MapData, unit: Unit, vector: Vector) => - map.maybeGetTileInfo(vector)?.getMovementCost(unit.info) || -1, + getCost: getCostBase, getResourceValue: (unit: Unit) => unit.fuel, - getTransitionCost: (info: UnitInfo, current: TileInfo, parent: TileInfo) => - (current.group !== parent.group && - parent.getTransitionCost(info) + current.getTransitionCost(info)) || - 0, - isAccessible: isAccessibleBase, + getTransitionCost: getTransitionCostBase, + isAccessible, } as const; const VisionConfiguration = { - getCost: (map: MapData, unit: Unit, vector: Vector) => - map.maybeGetTileInfo(vector)?.configuration.vision || -1, + getCost: (map: MapData, unit: Unit, vector: Vector, index?: number) => + map.maybeGetTileInfo(vector, undefined, index)?.configuration.vision || -1, getResourceValue: () => Number.POSITIVE_INFINITY, getTransitionCost: () => 0, isAccessible: (map: MapData, unit: Unit, vector: Vector) => @@ -81,29 +97,29 @@ function calculateRadius( start: Vector, radius: number, { - getCost, getResourceValue, getTransitionCost, isAccessible, }: RadiusConfiguration = MoveConfiguration, ): Map { - const { info } = unit; - const closed = new Array(map.size.width * map.size.height); + const closed: { [key: number]: 1 } = {}; const paths = new Map(); const queue = new FastPriorityQueue((a, b) => a.cost < b.cost); + const minRadius = Math.min(radius, getResourceValue(unit)); queue.add(RadiusItem(start)); - while (!queue.isEmpty()) { + let index: number = map.getTileIndex(start); + do { const { cost: parentCost, vector } = queue.poll()!; - const index = map.getTileIndex(vector); + index = map.getTileIndex(vector); if (closed[index]) { continue; } - closed[index] = true; + closed[index] = 1; const vectors = vector.adjacent(); - for (let i = 0; i < vectors.length; i++) { - const currentVector = vectors[i]; + const parentTileInfo = map.getTileInfo(vector, undefined, index); + for (const currentVector of vectors) { if (!map.contains(currentVector)) { continue; } @@ -111,37 +127,33 @@ function calculateRadius( if (closed[currentIndex]) { continue; } - const cost = getCost(map, unit, currentVector); + const currentTileInfo = map.getTileInfo( + currentVector, + undefined, + currentIndex, + ); + const cost = currentTileInfo.getMovementCost(unit.info); if (cost < 0 || !isAccessible(map, unit, currentVector)) { - closed[currentIndex] = true; + closed[currentIndex] = 1; continue; } const nextCost = parentCost + cost + - getTransitionCost( - info, - map.getTileInfo(vector), - map.getTileInfo(currentVector), - ); + getTransitionCost(unit.info, parentTileInfo, currentTileInfo); + if (nextCost > minRadius) { + continue; + } const previousPath = paths.get(currentVector); - if ( - nextCost <= radius && - (!previousPath || nextCost < previousPath.cost) && - nextCost <= getResourceValue(unit) - ) { - const item = { - cost: nextCost, - parent: vector, - vector: currentVector, - }; + if (!previousPath || nextCost < previousPath.cost) { + const item = RadiusItem(currentVector, nextCost, vector); paths.set(currentVector, item); if (nextCost < radius) { queue.add(item); } } } - } + } while (!queue.isEmpty()); return paths; } @@ -177,6 +189,7 @@ export function getPathCost( const seen = new Set([start]); let previousVector = start; let totalCost = 0; + const previousVectorTileInfo = map.getTileInfo(previousVector); for (const vector of path) { if (seen.has(vector) || !map.contains(vector)) { @@ -195,11 +208,7 @@ export function getPathCost( totalCost += cost + - getTransitionCost( - info, - map.getTileInfo(vector), - map.getTileInfo(previousVector), - ); + getTransitionCost(info, map.getTileInfo(vector), previousVectorTileInfo); if (totalCost > radius || totalCost > getResourceValue(unit)) { return -1; @@ -212,21 +221,28 @@ export function getPathCost( return !unitB || canLoad(map, unitB, unit, previousVector) ? totalCost : -1; } +function getVisionRange( + map: MapData, + unit: Unit, + start: Vector, + radius: number, +) { + const range = unit.isUnfolded() + ? 2 + : unit.info.type === EntityType.Infantry && + map.getTileInfo(start).type & TileTypes.Mountain + ? 1 + : 0; + return radius + range; +} + export function visible( map: MapData, unit: Unit, start: Vector, radius: number = unit.info.configuration.vision, ): ReadonlyMap { - const vision = - radius + - (unit.isUnfolded() - ? 2 - : unit.info.type === EntityType.Infantry && - map.getTileInfo(start).type & TileTypes.Mountain - ? 1 - : 0); - + const vision = getVisionRange(map, unit, start, radius); const visible = calculateRadius( map, unit, @@ -234,7 +250,6 @@ export function visible( vision, VisionConfiguration, ); - const player = map.getPlayer(unit); const canSeeHiddenFields = player.activeSkills.size && @@ -339,7 +354,8 @@ export function attackable( } const vectors = parent.vector.adjacent(); - for (let i = 0; i < vectors.length; i++) { + const len = vectors.length; + for (let i = 0; i < len; i++) { const vector = vectors[i]; if (map.contains(vector)) { const itemB = attackable.get(vector); diff --git a/athena/info/Tile.tsx b/athena/info/Tile.tsx index cb77d761..e6f29304 100644 --- a/athena/info/Tile.tsx +++ b/athena/info/Tile.tsx @@ -198,7 +198,7 @@ export class TileInfo { } getTransitionCost({ movementType }: { movementType: MovementType }): number { - return this.configuration.transitionCost?.get(movementType) || 0; + return this.configuration.transitionCost?.get(movementType) ?? 0; } isInaccessible() { diff --git a/athena/map/Vector.tsx b/athena/map/Vector.tsx index 80667b2b..2416dde5 100644 --- a/athena/map/Vector.tsx +++ b/athena/map/Vector.tsx @@ -40,7 +40,7 @@ export default abstract class Vector { adjacent() { return ( - this.vectors || + this.vectors ?? (this.vectors = [ this.up(), this.right(), @@ -130,7 +130,8 @@ export function decodeVectorArray( array: ReadonlyArray, ): ReadonlyArray { const result = []; - for (let i = 0; i < array.length; i += 2) { + const len = array.length; + for (let i = 0; i < len; i += 2) { result.push(vec(array[i], array[i + 1])); } return result; From eeca5a95e5c32f8fa1fea951cca93ba2dd73cd3c Mon Sep 17 00:00:00 2001 From: rortan134 Date: Sun, 7 Jul 2024 23:06:55 +0200 Subject: [PATCH 2/6] fix: use vector coordinates as cache id --- athena/MapData.tsx | 9 ++++++--- athena/Radius.tsx | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/athena/MapData.tsx b/athena/MapData.tsx index 7a11dce7..23eb32e8 100644 --- a/athena/MapData.tsx +++ b/athena/MapData.tsx @@ -416,7 +416,8 @@ export default class MapData { value: T, ): T { const { map, size } = this; - for (let i = 0; i < map.length; i++) { + const len = map.length; + for (let i = 0; i < len; i++) { value = fn.call(this, value, indexToVector(i, size.width), i); } return value; @@ -450,7 +451,8 @@ export default class MapData { value: T, ): T { const { map, modifiers, size } = this; - for (let i = 0; i < map.length; i++) { + const len = map.length; + for (let i = 0; i < len; i++) { const field = map[i]; if (typeof field === 'number') { value = fn.call( @@ -502,7 +504,8 @@ export default class MapData { value: T, ): T { const { decorators, size } = this; - for (let i = 0; i < decorators.length; i++) { + const len = decorators.length; + for (let i = 0; i < len; i++) { const decorator = getDecorator(decorators[i]); if (decorator) { value = fn.call( diff --git a/athena/Radius.tsx b/athena/Radius.tsx index 7c58edfe..c3b04174 100644 --- a/athena/Radius.tsx +++ b/athena/Radius.tsx @@ -65,7 +65,7 @@ function isAccessibleBase(map: MapData, unit: Unit, vector: Vector) { } function isAccessible(map: MapData, unit: Unit, vector: Vector) { - const key = vector.valueOf(); + const key = vector.toJSON(); let accessible = cacheMap.get(key); if (accessible != null) { return accessible; From 7a236dee5272d103348a4aa042022ffafcd153c7 Mon Sep 17 00:00:00 2001 From: rortan134 Date: Mon, 8 Jul 2024 04:28:55 +0200 Subject: [PATCH 3/6] rename length variables --- athena/MapData.tsx | 12 ++++++------ athena/Radius.tsx | 4 ++-- athena/map/Vector.tsx | 4 ++-- hephaestus/jenkinsHash.tsx | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/athena/MapData.tsx b/athena/MapData.tsx index 23eb32e8..b466ab5d 100644 --- a/athena/MapData.tsx +++ b/athena/MapData.tsx @@ -416,8 +416,8 @@ export default class MapData { value: T, ): T { const { map, size } = this; - const len = map.length; - for (let i = 0; i < len; i++) { + const length = map.length; + for (let i = 0; i < length; i++) { value = fn.call(this, value, indexToVector(i, size.width), i); } return value; @@ -451,8 +451,8 @@ export default class MapData { value: T, ): T { const { map, modifiers, size } = this; - const len = map.length; - for (let i = 0; i < len; i++) { + const length = map.length; + for (let i = 0; i < length; i++) { const field = map[i]; if (typeof field === 'number') { value = fn.call( @@ -504,8 +504,8 @@ export default class MapData { value: T, ): T { const { decorators, size } = this; - const len = decorators.length; - for (let i = 0; i < len; i++) { + const length = decorators.length; + for (let i = 0; i < length; i++) { const decorator = getDecorator(decorators[i]); if (decorator) { value = fn.call( diff --git a/athena/Radius.tsx b/athena/Radius.tsx index c3b04174..d495796c 100644 --- a/athena/Radius.tsx +++ b/athena/Radius.tsx @@ -354,8 +354,8 @@ export function attackable( } const vectors = parent.vector.adjacent(); - const len = vectors.length; - for (let i = 0; i < len; i++) { + const length = vectors.length; + for (let i = 0; i < length; i++) { const vector = vectors[i]; if (map.contains(vector)) { const itemB = attackable.get(vector); diff --git a/athena/map/Vector.tsx b/athena/map/Vector.tsx index 2416dde5..fe52a06c 100644 --- a/athena/map/Vector.tsx +++ b/athena/map/Vector.tsx @@ -130,8 +130,8 @@ export function decodeVectorArray( array: ReadonlyArray, ): ReadonlyArray { const result = []; - const len = array.length; - for (let i = 0; i < len; i += 2) { + const length = array.length; + for (let i = 0; i < length; i += 2) { result.push(vec(array[i], array[i + 1])); } return result; diff --git a/hephaestus/jenkinsHash.tsx b/hephaestus/jenkinsHash.tsx index 2312e9ab..c1502091 100644 --- a/hephaestus/jenkinsHash.tsx +++ b/hephaestus/jenkinsHash.tsx @@ -1,7 +1,7 @@ const toUtf8 = (str: string) => { const result = []; - const len = str.length; - for (let i = 0; i < len; i++) { + const length = str.length; + for (let i = 0; i < length; i++) { let charcode = str.charCodeAt(i); if (charcode < 0x80) { result.push(charcode); @@ -36,8 +36,8 @@ const _jenkinsHash = (str: string): number => { const utf8 = toUtf8(str); let hash = 0; - const len = utf8.length; - for (let i = 0; i < len; i++) { + const length = utf8.length; + for (let i = 0; i < length; i++) { hash += utf8[i]; hash = (hash + (hash << 10)) >>> 0; hash ^= hash >>> 6; From ddae0b70bcf44606760a3ff25b442be1c49127c7 Mon Sep 17 00:00:00 2001 From: rortan134 Date: Mon, 8 Jul 2024 04:46:37 +0200 Subject: [PATCH 4/6] rename to previousTileInfo --- athena/Radius.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/athena/Radius.tsx b/athena/Radius.tsx index d495796c..b261e949 100644 --- a/athena/Radius.tsx +++ b/athena/Radius.tsx @@ -189,7 +189,7 @@ export function getPathCost( const seen = new Set([start]); let previousVector = start; let totalCost = 0; - const previousVectorTileInfo = map.getTileInfo(previousVector); + const previousTileInfo = map.getTileInfo(previousVector); for (const vector of path) { if (seen.has(vector) || !map.contains(vector)) { @@ -208,7 +208,7 @@ export function getPathCost( totalCost += cost + - getTransitionCost(info, map.getTileInfo(vector), previousVectorTileInfo); + getTransitionCost(info, map.getTileInfo(vector), previousTileInfo); if (totalCost > radius || totalCost > getResourceValue(unit)) { return -1; From e929a36f86afa472a281aa0bfec369576f32e2f7 Mon Sep 17 00:00:00 2001 From: rortan134 Date: Mon, 8 Jul 2024 15:29:22 +0200 Subject: [PATCH 5/6] update cache impl --- athena/MapData.tsx | 6 ++++++ athena/Radius.tsx | 21 +++++++++++++++++---- athena/package.json | 1 + 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/athena/MapData.tsx b/athena/MapData.tsx index b466ab5d..33e9360a 100644 --- a/athena/MapData.tsx +++ b/athena/MapData.tsx @@ -173,6 +173,7 @@ const toPlayer = (object: AnyEntity): PlayerID => : object.id; export default class MapData { + protected id: string; private players: Map; private playerToTeam: Map; private _firstPlayer: PlayerID = 0; @@ -193,6 +194,7 @@ export default class MapData { public readonly buildings: ImmutableMap, public readonly units: ImmutableMap, ) { + this.id = JSON.stringify(this.buildings.toJSON()); this.players = new Map( teams.flatMap((team) => team.players).sortBy(({ id }) => id), ); @@ -208,6 +210,10 @@ export default class MapData { this.playerToTeam.set(0, -1); } + getId() { + return this.id; + } + contains(size: { x: number; y: number }) { return this.size.contains(size); } diff --git a/athena/Radius.tsx b/athena/Radius.tsx index b261e949..97f04d26 100644 --- a/athena/Radius.tsx +++ b/athena/Radius.tsx @@ -1,4 +1,5 @@ import FastPriorityQueue from 'fastpriorityqueue'; +import LRUCache from 'mnemonist/lru-cache'; import { Skill } from './info/Skill.tsx'; import { TileInfo, TileTypes } from './info/Tile.tsx'; import { UnitInfo } from './info/Unit.tsx'; @@ -37,7 +38,18 @@ export const RadiusItem = ( vector, }); -const cacheMap = new Map(); +let lruCacheMap: LRUCache; +let currentMapId: string; + +function getCache(map: MapData) { + // Reinstantiate the cache if the map changes + if (currentMapId !== map.getId()) { + const cacheCapacity = (map.size.width * map.size.height) / 2; // No need to cache the entire map + lruCacheMap = new LRUCache(cacheCapacity); + currentMapId = map.getId(); + } + return lruCacheMap; +} function getCostBase(map: MapData, unit: Unit, vector: Vector, index?: number) { const tileInfo = map.maybeGetTileInfo(vector, undefined, index); @@ -65,13 +77,14 @@ function isAccessibleBase(map: MapData, unit: Unit, vector: Vector) { } function isAccessible(map: MapData, unit: Unit, vector: Vector) { - const key = vector.toJSON(); - let accessible = cacheMap.get(key); + const cache = getCache(map); + const key = vector.hashCode(); + let accessible = cache.get(key); if (accessible != null) { return accessible; } accessible = isAccessibleBase(map, unit, vector); - cacheMap.set(key, accessible); + cache.set(key, accessible); return accessible; } diff --git a/athena/package.json b/athena/package.json index d91d7f76..ccb49d2c 100644 --- a/athena/package.json +++ b/athena/package.json @@ -14,6 +14,7 @@ "@nkzw/immutable-map": "^1.2.2", "array-shuffle": "^3.0.0", "fastpriorityqueue": "^0.7.5", + "mnemonist": "^0.39.8", "skmeans": "^0.11.3" }, "devDependencies": { From c24ec81368f6eff96500a6b9156e55d15b623d6f Mon Sep 17 00:00:00 2001 From: rortan134 Date: Tue, 9 Jul 2024 13:12:55 +0200 Subject: [PATCH 6/6] update cache impl --- athena/MapData.tsx | 8 ++++---- athena/Radius.tsx | 19 ++++--------------- athena/package.json | 1 - 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/athena/MapData.tsx b/athena/MapData.tsx index 33e9360a..937aec3a 100644 --- a/athena/MapData.tsx +++ b/athena/MapData.tsx @@ -173,13 +173,13 @@ const toPlayer = (object: AnyEntity): PlayerID => : object.id; export default class MapData { - protected id: string; private players: Map; private playerToTeam: Map; private _firstPlayer: PlayerID = 0; private _hasNeutralUnits: boolean | null = null; private _activeUnitTypes: ReadonlyMap | null = null; + protected unitAccessibilityCache: Map; constructor( public readonly map: TileMap, @@ -194,7 +194,6 @@ export default class MapData { public readonly buildings: ImmutableMap, public readonly units: ImmutableMap, ) { - this.id = JSON.stringify(this.buildings.toJSON()); this.players = new Map( teams.flatMap((team) => team.players).sortBy(({ id }) => id), ); @@ -208,10 +207,11 @@ export default class MapData { }), ); this.playerToTeam.set(0, -1); + this.unitAccessibilityCache = new Map(); } - getId() { - return this.id; + getCache() { + return this.unitAccessibilityCache; } contains(size: { x: number; y: number }) { diff --git a/athena/Radius.tsx b/athena/Radius.tsx index 97f04d26..142fb1b4 100644 --- a/athena/Radius.tsx +++ b/athena/Radius.tsx @@ -1,5 +1,4 @@ import FastPriorityQueue from 'fastpriorityqueue'; -import LRUCache from 'mnemonist/lru-cache'; import { Skill } from './info/Skill.tsx'; import { TileInfo, TileTypes } from './info/Tile.tsx'; import { UnitInfo } from './info/Unit.tsx'; @@ -38,18 +37,7 @@ export const RadiusItem = ( vector, }); -let lruCacheMap: LRUCache; -let currentMapId: string; - -function getCache(map: MapData) { - // Reinstantiate the cache if the map changes - if (currentMapId !== map.getId()) { - const cacheCapacity = (map.size.width * map.size.height) / 2; // No need to cache the entire map - lruCacheMap = new LRUCache(cacheCapacity); - currentMapId = map.getId(); - } - return lruCacheMap; -} +let currentUnitId: number; function getCostBase(map: MapData, unit: Unit, vector: Vector, index?: number) { const tileInfo = map.maybeGetTileInfo(vector, undefined, index); @@ -77,12 +65,13 @@ function isAccessibleBase(map: MapData, unit: Unit, vector: Vector) { } function isAccessible(map: MapData, unit: Unit, vector: Vector) { - const cache = getCache(map); + const cache = map.getCache(); const key = vector.hashCode(); let accessible = cache.get(key); - if (accessible != null) { + if (accessible !== undefined && currentUnitId === unit.id) { return accessible; } + currentUnitId = unit.id; accessible = isAccessibleBase(map, unit, vector); cache.set(key, accessible); return accessible; diff --git a/athena/package.json b/athena/package.json index ccb49d2c..d91d7f76 100644 --- a/athena/package.json +++ b/athena/package.json @@ -14,7 +14,6 @@ "@nkzw/immutable-map": "^1.2.2", "array-shuffle": "^3.0.0", "fastpriorityqueue": "^0.7.5", - "mnemonist": "^0.39.8", "skmeans": "^0.11.3" }, "devDependencies": {