From 673c33bb01abac5ce9cd315b0c490d7234161ba4 Mon Sep 17 00:00:00 2001 From: yairm210 Date: Thu, 17 Jul 2025 23:57:39 +0300 Subject: [PATCH] chore: Hexmath purity final round --- build.gradle.kts | 5 +- core/src/com/unciv/logic/map/HexMath.kt | 67 +++++++++++++++++++------ 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 0fc0022d03..dc4f61252a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,7 +37,7 @@ plugins { // This is *with* gradle 8.2 downloaded according the project specs, no idea what that's about kotlin("multiplatform") 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 { @@ -56,6 +56,7 @@ allprojects { wellKnownReadonlyFunctions = setOf( // Looks like the Collection.contains is not considered overridden :thunk: "com.badlogic.gdx.math.Vector2.len", + "com.badlogic.gdx.math.Vector2.cpy", "java.util.AbstractCollection.contains", "java.util.AbstractList.get", ) @@ -167,7 +168,7 @@ project(":core") { "implementation"("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") "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-cio:$ktorVersion") diff --git a/core/src/com/unciv/logic/map/HexMath.kt b/core/src/com/unciv/logic/map/HexMath.kt index 6bf8716788..291fd6ed77 100644 --- a/core/src/com/unciv/logic/map/HexMath.kt +++ b/core/src/com/unciv/logic/map/HexMath.kt @@ -2,6 +2,8 @@ package com.unciv.logic.map import com.badlogic.gdx.math.Vector2 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.Readonly import kotlin.math.abs @@ -114,26 +116,39 @@ object HexMath { * * @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 toWrapLat = getLatitude(unwrapHexCoord) // Working in Cartesian space is easier. val toWrapLong = getLongitude(unwrapHexCoord) - val longRadius = longitudinalRadius.toFloat() - return hexFromLatLong(toWrapLat, (toWrapLong - referenceLong + longRadius).mod(longRadius * 2f) - longRadius + referenceLong) + return hexFromLatLong(toWrapLat, (toWrapLong - referenceLong + longitudinalRadius).mod(longitudinalRadius * 2f) + - longitudinalRadius + referenceLong) } + @Readonly fun hex2WorldCoords(hexCoord: Vector2): Vector2 { // Distance between cells = 2* normal of triangle = 2* (sqrt(3)/2) = sqrt(3) - val xVector = getVectorByClockHour(10).scl(sqrt(3.0).toFloat()) - val yVector = getVectorByClockHour(2).scl(sqrt(3.0).toFloat()) - return xVector.scl(hexCoord.x).add(yVector.scl(hexCoord.y)) + @LocalState + val xVector = getVectorByClockHour(10) + 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 + @Readonly fun world2HexCoords(worldCoord: Vector2): Vector2 { // D: diagonal, A: antidiagonal versors - val D = getVectorByClockHour(10).scl(sqrt(3.0).toFloat()) - val A = getVectorByClockHour(2).scl(sqrt(3.0).toFloat()) + @LocalState + 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 x = (worldCoord.x * A.y - worldCoord.y * A.x) / den val y = (worldCoord.y * D.x - worldCoord.x * D.y) / den @@ -148,7 +163,7 @@ object HexMath { @Readonly fun getColumn(hexCoord: Vector2): Int = (hexCoord.y - hexCoord.x).toInt() - @Readonly + @Pure fun getTileCoordsFromColumnRow(column: Int, row: Int): Vector2 { // we know that column = y-x 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))) } + @Readonly fun getVectorsAtDistance(origin: Vector2, distance: Int, maxDistance: Int, worldWrap: Boolean): List { + @LocalState val vectors = mutableListOf() if (distance == 0) { - vectors += origin.cpy() - return vectors + return listOf(origin.cpy()) } - 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 vectors += current.cpy() - vectors += origin.cpy().scl(2f).sub(current) // Get vector on other side of clock + vectors += getVectorOnOtherSideOfClock(current) current.add(1f, 0f) } for (i in 0 until distance) { // 8 to 10 vectors += current.cpy() 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) } for (i in 0 until distance) { // 10 to 12 vectors += current.cpy() 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) } return vectors } + @Readonly fun getVectorsInDistance(origin: Vector2, distance: Int, worldWrap: Boolean): List { + @LocalState val hexesToReturn = mutableListOf() for (i in 0..distance) { hexesToReturn += getVectorsAtDistance(origin, i, distance, worldWrap) @@ -253,6 +283,7 @@ object HexMath { (abs(relativeX) + abs(relativeY)) } + @Immutable private val clockPositionToHexVectorMap: Map = mapOf( 0 to Vector2(1f, 1f), // This alias of 12 makes clock modulo logic easier 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 */ + @Pure fun getClockPositionToHexVector(clockPosition: Int): Vector2 { return clockPositionToHexVectorMap[clockPosition]?: Vector2.Zero } // Statically allocate the Vectors (in World coordinates) // of the 6 clock directions for border and road drawing in TileGroup + @Immutable private val clockPositionToWorldVectorMap: Map = mapOf( 2 to hex2WorldCoords(Vector2(0f, -1f)), 4 to hex2WorldCoords(Vector2(1f, 0f)), @@ -279,10 +312,11 @@ object HexMath { 12 to hex2WorldCoords(Vector2(-1f, -1f)) ) /** 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 = clockPositionToWorldVectorMap[clockPosition] ?: Vector2.Zero + @Readonly fun getDistanceFromEdge(vector: Vector2, mapParameters: MapParameters): Int { val x = vector.x.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 * Places 1-6 are ring 1, 7-18 are ring 2, etc. */ + @Pure fun getZeroBasedIndex(x: Int, y: Int): Int { if (x == 0 && y == 0) return 0 val ring = getDistance(0,0, x, y)