diff --git a/core/src/com/unciv/logic/map/HexMath.kt b/core/src/com/unciv/logic/map/HexMath.kt index 8dacc69df8..7d1cb18198 100644 --- a/core/src/com/unciv/logic/map/HexMath.kt +++ b/core/src/com/unciv/logic/map/HexMath.kt @@ -232,12 +232,16 @@ object HexMath { /** Get number of hexes from [origin] to [destination] _without respecting world-wrap_ */ fun getDistance(origin: Vector2, destination: Vector2): Int { - val relativeX = origin.x - destination.x - val relativeY = origin.y - destination.y + return getDistance(origin.x.toInt(), origin.y.toInt(), destination.x.toInt(), destination.y.toInt()) + } + + fun getDistance(originX: Int, originY: Int, destinationX: Int, destinationY: Int): Int { + val relativeX = originX - destinationX + val relativeY = originY - destinationY return if (relativeX * relativeY >= 0) - max(abs(relativeX), abs(relativeY)).toInt() + max(abs(relativeX), abs(relativeY)) else - (abs(relativeX) + abs(relativeY)).toInt() + (abs(relativeX) + abs(relativeY)) } private val clockPositionToHexVectorMap: Map = mapOf( @@ -296,4 +300,40 @@ object HexMath { return min(getDistance(vector, Vector2(1f, radius.toFloat())), getDistance(vector, Vector2(-radius.toFloat(), -1f))) } } + + /** + * The goal here is to map from hexagonal positions (centered on 0,0) to positive integers (starting from 0) so we can replace hashmap/hashset with arrays/bitsets + * Places 1-6 are ring 1, 7-18 are ring 2, etc. + */ + fun getZeroBasedIndex(x: Int, y: Int): Int { + if (x == 0 && y == 0) return 0 + val ring = getDistance(0,0, x, y) + val ringStart = 1 + 6 * ring * (ring - 1) / 2 // 1 for the center tile, then 6 for each ring + + // total number of elements in the ring is 6 * ring + + // We divide the ring into its 6 edges, each of which can be determined by an equality comparison + // Each edge has a start index, a variable from 0 to the number of elements in that edge + val positionInRing = when (ring) { + y -> 0 /* start index*/ + x /*variable*/ // contains `ring+1` elements + x -> ring + 1 /* start index */ + y /*variable*/ // contains `ring` elements - 1 already taken by x=y=ring above + -x -> 2 * ring + 1 /* start index */ -y /*variable*/ // contains `ring+1` elements + -y -> 3 * ring + 2 /*start index*/ -x /*variable*/ // contains `ring` elements - 1 already taken by -x=-y=ring above + x-y -> 4 * ring + 2 /* start index */ +x-1 /*variable*/ // contains `ring-1` elements. -1 because x=0 is already taken by ring=-y above + y-x -> 5 * ring + 1 /* start index */ +y-1 /*variable*/ // contains `ring-1` elements. -1 because y=0 is already taken by ring=-x above + else -> throw Exception("How???") + } + return ringStart + positionInRing + } + + // Much simpler to understand, passes same tests, but ~5x slower than the above + fun mapRelativePositionToPositiveIntRedblob(x: Int, y: Int): Int { + if (x == 0 && y == 0) return 0 + val ring = getDistance(0,0, x, y) + val ringStart = 1 + 6 * ring * (ring - 1) / 2 // 1 for the center tile, then 6 for each ring + val vectorsInRing = getVectorsAtDistance(Vector2.Zero, ring, ring, false) + val positionInRing = vectorsInRing.indexOf(Vector2(x.toFloat(), y.toFloat())) + return ringStart + positionInRing + } + } diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 0501ef4a88..7e807ce998 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -516,6 +516,7 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization { // looks at tileMatrix. Thus filling Tiles into tileMatrix and setting their // transients in the same loop will leave incomplete cached `neighbors`. tileInfo.tileMap = this + tileInfo.zeroBasedIndex = HexMath.getZeroBasedIndex(tileInfo.position.x.toInt(), tileInfo.position.y.toInt()) tileInfo.ruleset = this.ruleset!! tileInfo.setTerrainTransients() tileInfo.setUnitTransients(setUnitCivTransients) diff --git a/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt b/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt index 832e9a3219..8bd21206f7 100644 --- a/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt +++ b/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt @@ -11,6 +11,7 @@ import com.unciv.logic.map.tile.Tile import com.unciv.models.UnitActionType import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.components.UnitMovementMemoryType +import java.util.BitSet class UnitMovement(val unit: MapUnit) { @@ -30,7 +31,7 @@ class UnitMovement(val unit: MapUnit) { position: Vector2, unitMovement: Float, considerZoneOfControl: Boolean = true, - tilesToIgnore: HashSet? = null, + tilesToIgnoreBitset: BitSet? = null, passThroughCache: HashMap = HashMap(), movementCostCache: HashMap, Float> = HashMap(), includeOtherEscortUnit: Boolean = true @@ -49,12 +50,13 @@ class UnitMovement(val unit: MapUnit) { && unit.getOtherEscortUnit()?.currentMovement == 0f) return distanceToTiles var tilesToCheck = listOf(unitTile) - + while (tilesToCheck.isNotEmpty()) { val updatedTiles = ArrayList() for (tileToCheck in tilesToCheck) for (neighbor in tileToCheck.neighbors) { - if (tilesToIgnore?.contains(neighbor) == true) continue // ignore this tile + // ignore this tile + if (tilesToIgnoreBitset != null && tilesToIgnoreBitset.get(neighbor.zeroBasedIndex)) continue // ignore this tile var totalDistanceToTile: Float = when { !neighbor.isExplored(unit.civ) -> distanceToTiles[tileToCheck]!!.totalMovement + 1f // If we don't know then we just guess it to be 1. @@ -131,7 +133,7 @@ class UnitMovement(val unit: MapUnit) { var distance = 1 val unitMaxMovement = unit.getMaxMovement().toFloat() val newTilesToCheck = ArrayList() - val visitedTiles: HashSet = hashSetOf(currentTile) + val visitedTilesBitset = BitSet().apply { set(currentTile.zeroBasedIndex) } val civilization = unit.civ val passThroughCache = HashMap() @@ -160,7 +162,7 @@ class UnitMovement(val unit: MapUnit) { tileToCheck.position, unitMaxMovement, false, - visitedTiles, + visitedTilesBitset, passThroughCache, movementCostCache ) @@ -203,11 +205,11 @@ class UnitMovement(val unit: MapUnit) { } // add newTilesToCheck to visitedTiles so we do not path over these tiles in a later iteration - visitedTiles.addAll(newTilesToCheck) + for (tile in newTilesToCheck) visitedTilesBitset.set(tile.zeroBasedIndex) // no need to check tiles that are surrounded by reachable tiles, only need to check the edgemost tiles. // Because anything we can reach from intermediate tiles, can be more easily reached by the edgemost tiles, // since we'll have to pass through an edgemost tile in order to reach the destination anyway - tilesToCheck = newTilesToCheck.filterNot { tile -> tile.neighbors.all { it in visitedTiles } } + tilesToCheck = newTilesToCheck.filterNot { tile -> tile.neighbors.all { visitedTilesBitset.get(it.zeroBasedIndex) } } distance++ } diff --git a/core/src/com/unciv/logic/map/tile/Tile.kt b/core/src/com/unciv/logic/map/tile/Tile.kt index 81b0a77dec..1bfa8bef7d 100644 --- a/core/src/com/unciv/logic/map/tile/Tile.kt +++ b/core/src/com/unciv/logic/map/tile/Tile.kt @@ -98,6 +98,9 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable { //region Transient fields @Transient lateinit var tileMap: TileMap + + @Transient + var zeroBasedIndex: Int = 0 @Transient lateinit var ruleset: Ruleset // a tile can be a tile with a ruleset, even without a map. diff --git a/tests/src/com/unciv/logic/map/HexmathTests.kt b/tests/src/com/unciv/logic/map/HexmathTests.kt new file mode 100644 index 0000000000..cdb4f7577d --- /dev/null +++ b/tests/src/com/unciv/logic/map/HexmathTests.kt @@ -0,0 +1,30 @@ +package com.unciv.logic.map + +import com.badlogic.gdx.math.Vector2 +import org.junit.Assert +import org.junit.Test + + +class HexmathTests { + // Looks like our current movement is actually unoptimized, since it fails this test :) + @Test + fun zeroIndexed(){ + Assert.assertEquals(0, HexMath.getZeroBasedIndex(0,0)) + } + + @Test + fun testMappingIsOneToOne(){ + val seenCoordsMapping = hashSetOf() + for (ring in 1..100) { + val coords = HexMath.getVectorsAtDistance(Vector2.Zero, ring, 100, false) + val ringStartCoordinate = 1 + 6 * ring * (ring - 1) / 2 + for (coord in coords) { + val mapping = HexMath.getZeroBasedIndex(coord.x.toInt(), coord.y.toInt()) + Assert.assertFalse("Duplicate coords found: $coord", seenCoordsMapping.contains(mapping)) + Assert.assertTrue("Coords $coord should be in ring $ring, actual mapping $mapping", mapping in ringStartCoordinate .. (ringStartCoordinate + 6 * ring - 1)) + + seenCoordsMapping.add(mapping) + } + } + } +}