chore: Hexmath purity final round

This commit is contained in:
yairm210 2025-07-17 23:57:39 +03:00
parent e56680edda
commit 673c33bb01
2 changed files with 54 additions and 18 deletions

View File

@ -37,7 +37,7 @@ plugins {
// This is *with* gradle 8.2 downloaded according the project specs, no idea what that's about // This is *with* gradle 8.2 downloaded according the project specs, no idea what that's about
kotlin("multiplatform") version "1.9.24" kotlin("multiplatform") version "1.9.24"
kotlin("plugin.serialization") version "1.9.24" kotlin("plugin.serialization") version "1.9.24"
id("io.github.yairm210.purity-plugin") version "0.0.23" apply(false) id("io.github.yairm210.purity-plugin") version "0.0.25" apply(false)
} }
allprojects { allprojects {
@ -56,6 +56,7 @@ allprojects {
wellKnownReadonlyFunctions = setOf( wellKnownReadonlyFunctions = setOf(
// Looks like the Collection.contains is not considered overridden :thunk: // Looks like the Collection.contains is not considered overridden :thunk:
"com.badlogic.gdx.math.Vector2.len", "com.badlogic.gdx.math.Vector2.len",
"com.badlogic.gdx.math.Vector2.cpy",
"java.util.AbstractCollection.contains", "java.util.AbstractCollection.contains",
"java.util.AbstractList.get", "java.util.AbstractList.get",
) )
@ -167,7 +168,7 @@ project(":core") {
"implementation"("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") "implementation"("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
"implementation"("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") "implementation"("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
"implementation"("io.github.yairm210:purity-annotations:0.0.23") "implementation"("io.github.yairm210:purity-annotations:0.0.25")
"implementation"("io.ktor:ktor-client-core:$ktorVersion") "implementation"("io.ktor:ktor-client-core:$ktorVersion")
"implementation"("io.ktor:ktor-client-cio:$ktorVersion") "implementation"("io.ktor:ktor-client-cio:$ktorVersion")

View File

@ -2,6 +2,8 @@ package com.unciv.logic.map
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.math.Vector3 import com.badlogic.gdx.math.Vector3
import yairm210.purity.annotations.Immutable
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Pure import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
import kotlin.math.abs import kotlin.math.abs
@ -114,26 +116,39 @@ object HexMath {
* *
* @see [com.unciv.logic.map.TileMap.getUnWrappedPosition] * @see [com.unciv.logic.map.TileMap.getUnWrappedPosition]
*/ */
fun getUnwrappedNearestTo(unwrapHexCoord: Vector2, staticHexCoord: Vector2, longitudinalRadius: Number): Vector2 { @Readonly
fun getUnwrappedNearestTo(unwrapHexCoord: Vector2, staticHexCoord: Vector2, longitudinalRadius: Float): Vector2 {
val referenceLong = getLongitude(staticHexCoord) val referenceLong = getLongitude(staticHexCoord)
val toWrapLat = getLatitude(unwrapHexCoord) // Working in Cartesian space is easier. val toWrapLat = getLatitude(unwrapHexCoord) // Working in Cartesian space is easier.
val toWrapLong = getLongitude(unwrapHexCoord) val toWrapLong = getLongitude(unwrapHexCoord)
val longRadius = longitudinalRadius.toFloat() return hexFromLatLong(toWrapLat, (toWrapLong - referenceLong + longitudinalRadius).mod(longitudinalRadius * 2f)
return hexFromLatLong(toWrapLat, (toWrapLong - referenceLong + longRadius).mod(longRadius * 2f) - longRadius + referenceLong) - longitudinalRadius + referenceLong)
} }
@Readonly
fun hex2WorldCoords(hexCoord: Vector2): Vector2 { fun hex2WorldCoords(hexCoord: Vector2): Vector2 {
// Distance between cells = 2* normal of triangle = 2* (sqrt(3)/2) = sqrt(3) // Distance between cells = 2* normal of triangle = 2* (sqrt(3)/2) = sqrt(3)
val xVector = getVectorByClockHour(10).scl(sqrt(3.0).toFloat()) @LocalState
val yVector = getVectorByClockHour(2).scl(sqrt(3.0).toFloat()) val xVector = getVectorByClockHour(10)
return xVector.scl(hexCoord.x).add(yVector.scl(hexCoord.y)) xVector.scl(sqrt(3.0).toFloat() * hexCoord.x)
@LocalState
val yVector = getVectorByClockHour(2)
yVector.scl(sqrt(3.0).toFloat() * hexCoord.y)
return xVector.add(yVector)
} }
@Suppress("LocalVariableName") // clearer @Suppress("LocalVariableName") // clearer
@Readonly
fun world2HexCoords(worldCoord: Vector2): Vector2 { fun world2HexCoords(worldCoord: Vector2): Vector2 {
// D: diagonal, A: antidiagonal versors // D: diagonal, A: antidiagonal versors
val D = getVectorByClockHour(10).scl(sqrt(3.0).toFloat()) @LocalState
val A = getVectorByClockHour(2).scl(sqrt(3.0).toFloat()) val D = getVectorByClockHour(10)
D.scl(sqrt(3.0).toFloat())
@LocalState
val A = getVectorByClockHour(2)
A.scl(sqrt(3.0).toFloat())
val den = D.x * A.y - D.y * A.x val den = D.x * A.y - D.y * A.x
val x = (worldCoord.x * A.y - worldCoord.y * A.x) / den val x = (worldCoord.x * A.y - worldCoord.y * A.x) / den
val y = (worldCoord.y * D.x - worldCoord.x * D.y) / den val y = (worldCoord.y * D.x - worldCoord.x * D.y) / den
@ -148,7 +163,7 @@ object HexMath {
@Readonly @Readonly
fun getColumn(hexCoord: Vector2): Int = (hexCoord.y - hexCoord.x).toInt() fun getColumn(hexCoord: Vector2): Int = (hexCoord.y - hexCoord.x).toInt()
@Readonly @Pure
fun getTileCoordsFromColumnRow(column: Int, row: Int): Vector2 { fun getTileCoordsFromColumnRow(column: Int, row: Int): Vector2 {
// we know that column = y-x in hex coords // we know that column = y-x in hex coords
// And we know that row = (y+x)/2 in hex coords // And we know that row = (y+x)/2 in hex coords
@ -202,34 +217,49 @@ object HexMath {
return cubic2HexCoords(roundCubicCoords(hex2CubicCoords(hexCoord))) return cubic2HexCoords(roundCubicCoords(hex2CubicCoords(hexCoord)))
} }
@Readonly
fun getVectorsAtDistance(origin: Vector2, distance: Int, maxDistance: Int, worldWrap: Boolean): List<Vector2> { fun getVectorsAtDistance(origin: Vector2, distance: Int, maxDistance: Int, worldWrap: Boolean): List<Vector2> {
@LocalState
val vectors = mutableListOf<Vector2>() val vectors = mutableListOf<Vector2>()
if (distance == 0) { if (distance == 0) {
vectors += origin.cpy() return listOf(origin.cpy())
return vectors
} }
val current = origin.cpy().sub(distance.toFloat(), distance.toFloat()) // start at 6 o clock
@Readonly
fun getVectorOnOtherSideOfClock(vector: Vector2): Vector2 {
@LocalState
val vec = origin.cpy()
vec.scl(2f)
vec.sub(vector)
return vec
}
@LocalState
val current = origin.cpy()
current.sub(distance.toFloat(), distance.toFloat()) // start at 6 o clock
for (i in 0 until distance) { // From 6 to 8 for (i in 0 until distance) { // From 6 to 8
vectors += current.cpy() vectors += current.cpy()
vectors += origin.cpy().scl(2f).sub(current) // Get vector on other side of clock vectors += getVectorOnOtherSideOfClock(current)
current.add(1f, 0f) current.add(1f, 0f)
} }
for (i in 0 until distance) { // 8 to 10 for (i in 0 until distance) { // 8 to 10
vectors += current.cpy() vectors += current.cpy()
if (!worldWrap || distance != maxDistance) if (!worldWrap || distance != maxDistance)
vectors += origin.cpy().scl(2f).sub(current) // Get vector on other side of clock vectors += getVectorOnOtherSideOfClock(current)
current.add(1f, 1f) current.add(1f, 1f)
} }
for (i in 0 until distance) { // 10 to 12 for (i in 0 until distance) { // 10 to 12
vectors += current.cpy() vectors += current.cpy()
if (!worldWrap || distance != maxDistance || i != 0) if (!worldWrap || distance != maxDistance || i != 0)
vectors += origin.cpy().scl(2f).sub(current) // Get vector on other side of clock vectors += getVectorOnOtherSideOfClock(current)
current.add(0f, 1f) current.add(0f, 1f)
} }
return vectors return vectors
} }
@Readonly
fun getVectorsInDistance(origin: Vector2, distance: Int, worldWrap: Boolean): List<Vector2> { fun getVectorsInDistance(origin: Vector2, distance: Int, worldWrap: Boolean): List<Vector2> {
@LocalState
val hexesToReturn = mutableListOf<Vector2>() val hexesToReturn = mutableListOf<Vector2>()
for (i in 0..distance) { for (i in 0..distance) {
hexesToReturn += getVectorsAtDistance(origin, i, distance, worldWrap) hexesToReturn += getVectorsAtDistance(origin, i, distance, worldWrap)
@ -253,6 +283,7 @@ object HexMath {
(abs(relativeX) + abs(relativeY)) (abs(relativeX) + abs(relativeY))
} }
@Immutable
private val clockPositionToHexVectorMap: Map<Int, Vector2> = mapOf( private val clockPositionToHexVectorMap: Map<Int, Vector2> = mapOf(
0 to Vector2(1f, 1f), // This alias of 12 makes clock modulo logic easier 0 to Vector2(1f, 1f), // This alias of 12 makes clock modulo logic easier
12 to Vector2(1f, 1f), 12 to Vector2(1f, 1f),
@ -264,12 +295,14 @@ object HexMath {
) )
/** Returns the hex-space distance corresponding to [clockPosition], or a zero vector if [clockPosition] is invalid */ /** Returns the hex-space distance corresponding to [clockPosition], or a zero vector if [clockPosition] is invalid */
@Pure
fun getClockPositionToHexVector(clockPosition: Int): Vector2 { fun getClockPositionToHexVector(clockPosition: Int): Vector2 {
return clockPositionToHexVectorMap[clockPosition]?: Vector2.Zero return clockPositionToHexVectorMap[clockPosition]?: Vector2.Zero
} }
// Statically allocate the Vectors (in World coordinates) // Statically allocate the Vectors (in World coordinates)
// of the 6 clock directions for border and road drawing in TileGroup // of the 6 clock directions for border and road drawing in TileGroup
@Immutable
private val clockPositionToWorldVectorMap: Map<Int,Vector2> = mapOf( private val clockPositionToWorldVectorMap: Map<Int,Vector2> = mapOf(
2 to hex2WorldCoords(Vector2(0f, -1f)), 2 to hex2WorldCoords(Vector2(0f, -1f)),
4 to hex2WorldCoords(Vector2(1f, 0f)), 4 to hex2WorldCoords(Vector2(1f, 0f)),
@ -279,10 +312,11 @@ object HexMath {
12 to hex2WorldCoords(Vector2(-1f, -1f)) ) 12 to hex2WorldCoords(Vector2(-1f, -1f)) )
/** Returns the world/screen-space distance corresponding to [clockPosition], or a zero vector if [clockPosition] is invalid */ /** Returns the world/screen-space distance corresponding to [clockPosition], or a zero vector if [clockPosition] is invalid */
@Pure @Suppress("purity") @Pure
fun getClockPositionToWorldVector(clockPosition: Int): Vector2 = fun getClockPositionToWorldVector(clockPosition: Int): Vector2 =
clockPositionToWorldVectorMap[clockPosition] ?: Vector2.Zero clockPositionToWorldVectorMap[clockPosition] ?: Vector2.Zero
@Readonly
fun getDistanceFromEdge(vector: Vector2, mapParameters: MapParameters): Int { fun getDistanceFromEdge(vector: Vector2, mapParameters: MapParameters): Int {
val x = vector.x.toInt() val x = vector.x.toInt()
val y = vector.y.toInt() val y = vector.y.toInt()
@ -315,6 +349,7 @@ object HexMath {
* 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 * 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. * Places 1-6 are ring 1, 7-18 are ring 2, etc.
*/ */
@Pure
fun getZeroBasedIndex(x: Int, y: Int): Int { fun getZeroBasedIndex(x: Int, y: Int): Int {
if (x == 0 && y == 0) return 0 if (x == 0 && y == 0) return 0
val ring = getDistance(0,0, x, y) val ring = getDistance(0,0, x, y)