├── mixite.benchmarks ├── gradle.properties ├── build.gradle └── src │ └── commonMain │ └── kotlin │ └── org │ └── hexworks │ └── mixite │ └── core │ └── api │ ├── HexagonPerfTest.kt │ ├── HexagonalGridBuilderPerfTest.kt │ ├── HexagonalGridCalculatorPerfTest.kt │ └── HexagonalGridPerfTest.kt ├── USERS.md ├── mixite.core ├── .DS_Store ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── org │ │ │ └── hexworks │ │ │ └── mixite │ │ │ └── core │ │ │ ├── api │ │ │ ├── HexagonOrientation.kt │ │ │ ├── defaults │ │ │ │ ├── DefaultSatelliteData.kt │ │ │ │ └── DefaultHexagonDataStorage.kt │ │ │ ├── contract │ │ │ │ ├── SatelliteData.kt │ │ │ │ └── HexagonDataStorage.kt │ │ │ ├── RotationDirection.kt │ │ │ ├── Point.kt │ │ │ ├── HexagonalGridLayout.kt │ │ │ ├── CubeCoordinate.kt │ │ │ ├── Hexagon.kt │ │ │ ├── Rectangle.kt │ │ │ ├── CoordinateConverter.kt │ │ │ ├── HexagonalGridCalculator.kt │ │ │ ├── HexagonalGrid.kt │ │ │ └── HexagonalGridBuilder.kt │ │ │ └── internal │ │ │ ├── impl │ │ │ ├── layoutstrategy │ │ │ │ ├── TrapezoidGridLayoutStrategy.kt │ │ │ │ ├── TriangularGridLayoutStrategy.kt │ │ │ │ ├── RectangularGridLayoutStrategy.kt │ │ │ │ ├── GridLayoutStrategy.kt │ │ │ │ └── HexagonalGridLayoutStrategy.kt │ │ │ ├── HexagonImpl.kt │ │ │ ├── HexagonalGridCalculatorImpl.kt │ │ │ └── HexagonalGridImpl.kt │ │ │ └── GridData.kt │ └── commonTest │ │ └── kotlin │ │ └── org │ │ └── hexworks │ │ └── mixite │ │ └── core │ │ ├── api │ │ ├── HexagonOrientationTest.kt │ │ ├── PointTest.kt │ │ ├── DefaultSatelliteDataTest.kt │ │ ├── HexagonalGridLayoutTest.kt │ │ ├── CubeCoordinateTest.kt │ │ ├── RectangleTest.kt │ │ ├── HexagonalGridBuilderTest.kt │ │ ├── defaults │ │ │ └── DefaultHexagonDataStorageTest.kt │ │ └── CoordinateConverterTest.kt │ │ ├── GridLayoutStrategyUtil.kt │ │ ├── internal │ │ ├── impl │ │ │ ├── CustomGridLayoutStrategy.kt │ │ │ ├── RectangularGridLayoutHexagonImplTest.kt │ │ │ ├── CustomGridLayoutHexagonImplTest.kt │ │ │ ├── layoutstrategy │ │ │ │ ├── TrapezoidGridLayoutStrategyTest.kt │ │ │ │ ├── TriangularGridLayoutStrategyTest.kt │ │ │ │ ├── RectangularGridLayoutStrategyTest.kt │ │ │ │ └── HexagonalGridLayoutStrategyTest.kt │ │ │ ├── HexagonImplTest.kt │ │ │ ├── HexagonalGridImplTest.kt │ │ │ └── HexagonalGridCalculatorImplTest.kt │ │ └── GridDataTest.kt │ │ └── HexagonStub.kt └── build.gradle.kts ├── AUTHORS ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── settings.gradle.kts ├── .gitignore ├── .circleci └── config.yml ├── CONTRIBUTING.md ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /mixite.benchmarks/gradle.properties: -------------------------------------------------------------------------------- 1 | jmhIncludes=.* 2 | -------------------------------------------------------------------------------- /USERS.md: -------------------------------------------------------------------------------- 1 | # Users of Mixite 2 | 3 | - [Epoch](https://github.com/EnderL2000/Epoch) 4 | -------------------------------------------------------------------------------- /mixite.core/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hexworks/mixite/HEAD/mixite.core/.DS_Store -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | adam-arold 2 | migepatschen 3 | 3xp0n3nt 4 | georgekankava 5 | pubudug 6 | HoldYourWaffle 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hexworks/mixite/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=org.hexworks.mixite 2 | version=2020.1.0 3 | 4 | org.gradle.parallel=true 5 | org.gradle.daemon=true -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "mixite" 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | include(":mixite.core") 10 | //include(":mixite.benchmarks") -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jan 16 13:42:40 CET 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonOrientation.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | /** 4 | * Enum representing the possible orientations of a [Hexagon]. The names 5 | * are self-explanatory. 6 | */ 7 | enum class HexagonOrientation(val coordinateOffset: Float) { 8 | 9 | POINTY_TOP(0.5f), 10 | FLAT_TOP(0f) 11 | 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | out/ 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | nbproject/private/ 22 | build/ 23 | nbbuild/ 24 | dist/ 25 | nbdist/ 26 | .nb-gradle/ -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/defaults/DefaultSatelliteData.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api.defaults 2 | 3 | import org.hexworks.mixite.core.api.contract.SatelliteData 4 | 5 | /** 6 | * Convenience class implementing SatelliteData. 7 | */ 8 | open class DefaultSatelliteData( 9 | override var isPassable: Boolean = false, 10 | override var isOpaque: Boolean = false, 11 | override var movementCost: Double = 0.toDouble()) : SatelliteData 12 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/openjdk:8-jdk 6 | working_directory: ~/repo 7 | 8 | environment: 9 | JVM_OPTS: -Xmx2048m 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | keys: 14 | - v1-dependencies-{{ checksum "build.gradle" }} 15 | - run: gradle dependencies 16 | - save_cache: 17 | paths: 18 | - ~/.gradle 19 | key: v1-dependencies-{{ checksum "build.gradle" }} 20 | - run: ./gradlew build 21 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/api/HexagonOrientationTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.mixite.core.api.HexagonOrientation.* 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class HexagonOrientationTest { 8 | 9 | @Test 10 | fun shouldProperlyCalculateFlatCoordinateOffset() { 11 | assertEquals(expected = 0.0f, actual = FLAT_TOP.coordinateOffset) 12 | } 13 | 14 | @Test 15 | fun shouldProperlyCalculatePointyCoordinateOffset() { 16 | assertEquals(expected = 0.5f, actual = POINTY_TOP.coordinateOffset) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Mixite 2 | 3 | If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request. 4 | When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable 5 | as possible. 6 | 7 | Mixite is coded in Kotlin which [supports multiple runtimes](https://kotlinlang.org/docs/reference/multiplatform.html) (web, 8 | jvm, android etc). Mixite is agnostic so only uses Kotlin common code (which runs equivalently on all targets) 9 | 10 | ## License 11 | 12 | By contributing your code, you agree to license your contribution under the terms of the [Apache 2](https://github.com/Hexworks/hexameter/blob/master/LICENSE) license. 13 | All files are released with the Apache 2 license. 14 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/GridLayoutStrategyUtil.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core 2 | 3 | import org.hexworks.mixite.core.api.HexagonOrientation 4 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 5 | import org.hexworks.mixite.core.api.contract.SatelliteData 6 | 7 | object GridLayoutStrategyTestUtil { 8 | 9 | private const val RADIUS = 30.0 10 | private const val GRID_WIDTH = 3 11 | private const val GRID_HEIGHT = 3 12 | private val ORIENTATION = HexagonOrientation.POINTY_TOP 13 | 14 | fun fetchDefaultBuilder(): HexagonalGridBuilder { 15 | return HexagonalGridBuilder() 16 | .setGridHeight(GRID_HEIGHT) 17 | .setGridWidth(GRID_WIDTH) 18 | .setRadius(RADIUS) 19 | .setOrientation(ORIENTATION) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mixite.core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Libs.cobaltDatatypes 2 | 3 | plugins { 4 | kotlinMultiplatform 5 | } 6 | 7 | group = "org.hexworks.mixite" 8 | 9 | kotlin { 10 | sourceSets { 11 | commonMain { 12 | dependencies { 13 | api(cobaltDatatypes) 14 | } 15 | } 16 | } 17 | jvm { 18 | jvmTarget(JavaVersion.VERSION_1_8) 19 | withJava() 20 | } 21 | 22 | js { 23 | } 24 | 25 | dependencies { 26 | 27 | with(Libs) { 28 | commonMainApi(kotlinStdLibCommon) 29 | commonMainApi(cobaltDatatypes) 30 | 31 | jvmMainApi(kotlinStdLibJdk8) 32 | 33 | jsMainApi(kotlinStdLibJs) 34 | } 35 | 36 | with(TestLibs) { 37 | commonTestApi(kotlinTestCommon) 38 | commonTestApi(kotlinTestAnnotationsCommon) 39 | 40 | jvmTestApi(kotlinTestJunit) 41 | 42 | jsTestApi(kotlinTestJs) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/api/PointTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import kotlin.math.sqrt 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class PointTest { 8 | 9 | @Test 10 | fun shouldProperlyCreatePointWhenConstructorIsCalled() { 11 | val x = 0.0 12 | val y = 1.0 13 | val p = Point.fromPosition(x, y) 14 | assertEquals(expected = x, actual = p.coordinateX) 15 | assertEquals(expected = y, actual = p.coordinateY) 16 | } 17 | 18 | @Test 19 | fun shouldProperlyCalculateDistanceBetweenTwoPoints() { 20 | val y2 = 5.0 21 | val y1 = 4.0 22 | val x2 = 9.0 23 | val x1 = 6.0 24 | val expectedDistance = sqrt(((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))) 25 | val actualDistance = Point.fromPosition(x1, y1).distanceFrom(Point.fromPosition(x2, y2)) 26 | assertEquals(expectedDistance, actualDistance) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mixite.benchmarks/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "kotlin-multiplatform" 3 | id "kotlinx.benchmark" version "0.2.0-dev-6" 4 | id "org.jetbrains.kotlin.plugin.allopen" version "1.3.50" 5 | } 6 | 7 | allOpen { 8 | annotation("org.openjdk.jmh.annotations.State") 9 | } 10 | 11 | kotlin { 12 | 13 | jvm { 14 | compilations.all { 15 | kotlinOptions.jvmTarget = '1.8' 16 | } 17 | } 18 | 19 | sourceSets { 20 | commonMain { 21 | dependencies { 22 | implementation "org.jetbrains.kotlinx:kotlinx.benchmark.runtime:0.2.0-dev-6" // MPP fail, import manually 23 | implementation project(":mixite.core") 24 | } 25 | } 26 | } 27 | 28 | } 29 | 30 | benchmark { 31 | configurations { 32 | main { 33 | iterationTime = 1 34 | iterationTimeUnit = "s" 35 | include(jmhIncludes) 36 | } 37 | } 38 | 39 | targets { 40 | register("jvm") { 41 | jmhVersion = "1.22" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/internal/impl/layoutstrategy/TrapezoidGridLayoutStrategy.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl.layoutstrategy 2 | 3 | import org.hexworks.mixite.core.api.CubeCoordinate 4 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 5 | import org.hexworks.mixite.core.api.contract.SatelliteData 6 | 7 | class TrapezoidGridLayoutStrategy : GridLayoutStrategy() { 8 | 9 | override fun fetchGridCoordinates(builder: HexagonalGridBuilder): Iterable { 10 | val coords = ArrayList(builder.getGridHeight() * builder.getGridWidth()) 11 | for (gridZ in 0 until builder.getGridHeight()) { 12 | for (gridX in 0 until builder.getGridWidth()) { 13 | coords.add(CubeCoordinate.fromCoordinates(gridX, gridZ)) 14 | } 15 | } 16 | return coords 17 | } 18 | 19 | override fun checkParameters(gridHeight: Int, gridWidth: Int): Boolean { 20 | return checkCommonCase(gridHeight, gridWidth) 21 | } 22 | 23 | override fun getName(): String { 24 | return "TRAPEZOID" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/contract/SatelliteData.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api.contract 2 | 3 | /** 4 | * Represents arbitrary data which can be attached to a Hexagon. 5 | * An implementation should contain a set of fields for advanced 6 | * grid algorithms like pathfinding. 7 | */ 8 | interface SatelliteData { 9 | 10 | /** 11 | * Tells whether the Hexagon can be passed over when moving around the map or not. 12 | * 13 | * @return is passable? 14 | */ 15 | /** 16 | * Sets whether the Hexagon is passable or not. 17 | * 18 | * @param passable passable? 19 | */ 20 | var isPassable: Boolean 21 | 22 | /** 23 | * @return true if the [Hexagon] can bee seen through, false otherwise. 24 | */ 25 | /** 26 | * Set whether the [Hexagon] can be seen through. 27 | * 28 | * @param opaque is opaque? 29 | */ 30 | var isOpaque: Boolean 31 | 32 | /** 33 | * Returns the movement cost when moving over the Hexagon. 34 | * 35 | * @return movement cost 36 | */ 37 | /** 38 | * Sets the movement cost. 39 | * 40 | * @param movementCost movement cost 41 | */ 42 | var movementCost: Double 43 | 44 | } 45 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/RotationDirection.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | /** 4 | * Represents a right or left angle (60°) of a Hexagon rotation. 5 | * See: http://www.redblobgames.com/grids/hexagons/#rotation 6 | */ 7 | enum class RotationDirection constructor(private val rotationCalculator: RotationCalculator) { 8 | 9 | RIGHT(object : RotationCalculator { 10 | override fun calculate(coord: CubeCoordinate): CubeCoordinate { 11 | return CubeCoordinate.fromCoordinates(-coord.gridZ, -coord.gridY) 12 | } 13 | }), 14 | LEFT(object : RotationCalculator { 15 | override fun calculate(coord: CubeCoordinate): CubeCoordinate { 16 | return CubeCoordinate.fromCoordinates(-coord.gridY, -coord.gridX) 17 | } 18 | }); 19 | 20 | /** 21 | * Calculates a rotation (right or left) of `coord`. 22 | * 23 | * @param coord coordinate to rotate 24 | * 25 | * @return rotated coordinate 26 | */ 27 | fun calculateRotation(coord: CubeCoordinate): CubeCoordinate { 28 | return rotationCalculator.calculate(coord) 29 | } 30 | 31 | internal interface RotationCalculator { 32 | fun calculate(coord: CubeCoordinate): CubeCoordinate 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/internal/impl/layoutstrategy/TriangularGridLayoutStrategy.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl.layoutstrategy 2 | 3 | import org.hexworks.mixite.core.api.CubeCoordinate 4 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 5 | import org.hexworks.mixite.core.api.contract.SatelliteData 6 | 7 | class TriangularGridLayoutStrategy : GridLayoutStrategy() { 8 | 9 | override fun fetchGridCoordinates(builder: HexagonalGridBuilder): Iterable { 10 | val gridSize = builder.getGridHeight() 11 | val coords = ArrayList(gridSize * (gridSize + 1) / 2) 12 | for (gridZ in 0 until gridSize) { 13 | val endX = gridSize - gridZ 14 | for (gridX in 0 until endX) { 15 | coords.add(CubeCoordinate.fromCoordinates(gridX, gridZ)) 16 | } 17 | } 18 | return coords 19 | } 20 | 21 | override fun checkParameters(gridHeight: Int, gridWidth: Int): Boolean { 22 | val superResult = checkCommonCase(gridHeight, gridWidth) 23 | val result = gridHeight == gridWidth 24 | return superResult && result 25 | } 26 | 27 | override fun getName(): String { 28 | return "TRIANGULAR" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mixite.benchmarks/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonPerfTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import kotlinx.benchmark.* 4 | import org.hexworks.mixite.core.api.contract.HexagonDataStorage 5 | import org.hexworks.mixite.core.api.defaults.DefaultHexagonDataStorage 6 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 7 | import org.hexworks.mixite.core.internal.GridData 8 | 9 | @State(Scope.Benchmark) 10 | @Warmup(iterations = 5) 11 | @Measurement(iterations = 5) 12 | @Suppress("Unused") 13 | open class HexagonPerfTest { 14 | 15 | private lateinit var gridData: GridData 16 | private lateinit var cc: CubeCoordinate 17 | private lateinit var store: HexagonDataStorage 18 | 19 | @Setup 20 | fun setUp() { 21 | gridData = HexagonalGridBuilder() 22 | .setGridLayout(HexagonalGridLayout.RECTANGULAR) 23 | .setGridHeight(1) 24 | .setGridWidth(1) 25 | .setRadius(10.0) 26 | .build() 27 | .gridData 28 | cc = CubeCoordinate.fromCoordinates(-49, 0) 29 | store = DefaultHexagonDataStorage() 30 | } 31 | 32 | 33 | // @Benchmark 34 | fun constructHexagon(bh: Blackhole) { 35 | // bh.consume(HexagonImpl(gridData, cc, store)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/Point.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import kotlin.jvm.JvmStatic 4 | import kotlin.math.sqrt 5 | 6 | /** 7 | * Represents a point. Please note that this represents a point in 8 | * 2d space not an abstract concept of a coordinate. 9 | */ 10 | @Suppress("DataClassPrivateConstructor") 11 | data class Point private constructor(val coordinateX: Double, 12 | val coordinateY: Double) { 13 | 14 | /** 15 | * Calculates a distance between two points. 16 | * 17 | * @param point point 18 | * 19 | * @return distance 20 | */ 21 | fun distanceFrom(point: Point): Double { 22 | return sqrt((this.coordinateX - point.coordinateX) * (this.coordinateX - point.coordinateX) + (this.coordinateY - point.coordinateY) * (this.coordinateY - point.coordinateY)) 23 | } 24 | 25 | companion object { 26 | 27 | /** 28 | * Creates a point from coordinateX and coordinateY positions. 29 | * 30 | * @param coordinateX x 31 | * @param coordinateY y 32 | * 33 | * @return point 34 | */ 35 | @JvmStatic 36 | fun fromPosition(coordinateX: Double, coordinateY: Double): Point { 37 | return Point(coordinateX, coordinateY) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/api/DefaultSatelliteDataTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 4 | import kotlin.test.BeforeTest 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class DefaultSatelliteDataTest { 9 | 10 | lateinit var target: DefaultSatelliteData 11 | 12 | 13 | @BeforeTest 14 | fun setUp() { 15 | target = object : DefaultSatelliteData() { 16 | 17 | } 18 | } 19 | 20 | @Test 21 | fun shouldProperlySetAndGetIsPassable() { 22 | target.isPassable = EXPECTED_IS_PASSABLE 23 | assertEquals(EXPECTED_IS_PASSABLE, target.isPassable) 24 | } 25 | 26 | @Test 27 | fun shouldProperlySetAndGetIsBlocksView() { 28 | target.isOpaque = EXPECTED_IS_BLOCKS_VIEW 29 | assertEquals(EXPECTED_IS_BLOCKS_VIEW, target.isOpaque) 30 | } 31 | 32 | @Test 33 | fun shouldProperlySetAndGetMovementCost() { 34 | target.movementCost = EXPECTED_MOVEMENT_COST 35 | assertEquals(EXPECTED_MOVEMENT_COST, target.movementCost) 36 | } 37 | 38 | companion object { 39 | 40 | private const val EXPECTED_MOVEMENT_COST = 5.1 41 | private const val EXPECTED_IS_PASSABLE = true 42 | private const val EXPECTED_IS_BLOCKS_VIEW = true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/internal/impl/layoutstrategy/RectangularGridLayoutStrategy.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl.layoutstrategy 2 | 3 | import org.hexworks.mixite.core.api.CoordinateConverter 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 6 | import org.hexworks.mixite.core.api.contract.SatelliteData 7 | 8 | class RectangularGridLayoutStrategy : GridLayoutStrategy() { 9 | 10 | override fun fetchGridCoordinates(builder: HexagonalGridBuilder): Iterable { 11 | val coords = ArrayList( builder.getGridHeight() * builder.getGridWidth()) 12 | for (y in 0 until builder.getGridHeight()) { 13 | for (x in 0 until builder.getGridWidth()) { 14 | val gridX = CoordinateConverter.convertOffsetCoordinatesToCubeX(x, y, builder.getOrientation()) 15 | val gridZ = CoordinateConverter.convertOffsetCoordinatesToCubeZ(x, y, builder.getOrientation()) 16 | coords.add(CubeCoordinate.fromCoordinates(gridX, gridZ)) 17 | } 18 | } 19 | return coords 20 | } 21 | 22 | override fun checkParameters(gridHeight: Int, gridWidth: Int): Boolean { 23 | return checkCommonCase(gridHeight, gridWidth) 24 | } 25 | 26 | override fun getName(): String { 27 | return "RECTANGULAR" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/api/HexagonalGridLayoutTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.mixite.core.api.HexagonalGridLayout.* 4 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.HexagonalGridLayoutStrategy 5 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.RectangularGridLayoutStrategy 6 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.TrapezoidGridLayoutStrategy 7 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.TriangularGridLayoutStrategy 8 | import kotlin.test.Test 9 | import kotlin.test.assertTrue 10 | 11 | class HexagonalGridLayoutTest { 12 | 13 | @Test 14 | fun shouldBeHexagonalGridLayoutWhenGetGridLayoutStrategyFromHexagonalIsCalled() { 15 | assertTrue(HEXAGONAL.gridLayoutStrategy is HexagonalGridLayoutStrategy) 16 | } 17 | 18 | @Test 19 | fun shouldBeRectangularGridLayoutWhenGetGridLayoutStrategyFromRectangularIsCalled() { 20 | assertTrue(RECTANGULAR.gridLayoutStrategy is RectangularGridLayoutStrategy) 21 | } 22 | 23 | @Test 24 | fun shouldBeTrapezoidGridLayoutWhenGetGridLayoutStrategyFromTrapezoidIsCalled() { 25 | assertTrue(TRAPEZOID.gridLayoutStrategy is TrapezoidGridLayoutStrategy) 26 | } 27 | 28 | @Test 29 | fun shouldBeTriangularGridLayoutWhenGetGridLayoutStrategyFromTriangularIsCalled() { 30 | assertTrue(TRIANGULAR.gridLayoutStrategy is TriangularGridLayoutStrategy) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGridLayout.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.* 4 | 5 | /** 6 | * This enum represents the possible shapes a [HexagonalGrid] can have. 7 | * The name [HexagonalGridLayout] might seem inconsistent with the other names 8 | * in this package but since the name GridLayout is so common (in SWT for example) 9 | * using this name seemed appropriate. 10 | */ 11 | enum class HexagonalGridLayout(val gridLayoutStrategy: GridLayoutStrategy) { 12 | 13 | /** 14 | * A rectangular layout has no special rules. 15 | */ 16 | RECTANGULAR(RectangularGridLayoutStrategy()), 17 | 18 | /** 19 | * The hexagonal layout must have equal width and height and 20 | * it must be odd. 21 | */ 22 | HEXAGONAL(HexagonalGridLayoutStrategy()), 23 | 24 | /** 25 | * A triangular layout must have equal width and height. 26 | */ 27 | TRIANGULAR(TriangularGridLayoutStrategy()), 28 | 29 | /** 30 | * A trapezoid layout has no special rules. 31 | */ 32 | TRAPEZOID(TrapezoidGridLayoutStrategy()); 33 | 34 | /** 35 | * Checks whether the grid height/width parameters can be used for the given [GridLayoutStrategy]. 36 | * 37 | * @return valid? 38 | */ 39 | internal fun checkParameters(gridHeight: Int, gridWidth: Int): Boolean { 40 | return gridLayoutStrategy.checkParameters(gridHeight, gridWidth) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /mixite.benchmarks/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGridBuilderPerfTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import kotlinx.benchmark.* 4 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 5 | 6 | @State(Scope.Benchmark) 7 | @Warmup(iterations = 5) 8 | @Measurement(iterations = 5) 9 | open class HexagonalGridBuilderPerfTest { 10 | 11 | @Param("RECTANGULAR", "HEXAGONAL", "TRIANGULAR", "TRAPEZOID") 12 | lateinit var layout: HexagonalGridLayout 13 | 14 | lateinit var builder: HexagonalGridBuilder 15 | 16 | @Setup 17 | fun setUp() { 18 | builder = HexagonalGridBuilder() 19 | .setRadius(100.0) 20 | .apply { gridLayoutWithConsistentSize(layout, this) } 21 | } 22 | 23 | private fun gridLayoutWithConsistentSize(layout: HexagonalGridLayout, builder: HexagonalGridBuilder<*>) { 24 | builder.setGridLayout(layout).apply { 25 | when (layout) { 26 | // Each of these is about 10000 in size. Resulting ops/sec should be consistent. 27 | HexagonalGridLayout.RECTANGULAR -> setGridHeight(100).setGridWidth(100) 28 | HexagonalGridLayout.TRAPEZOID -> setGridHeight(100).setGridWidth(100) 29 | HexagonalGridLayout.TRIANGULAR -> setGridHeight(141).setGridWidth(141) 30 | HexagonalGridLayout.HEXAGONAL -> setGridHeight(115).setGridWidth(115) 31 | } 32 | } 33 | } 34 | 35 | @Suppress("Unused") 36 | @Benchmark 37 | fun buildTime() = builder.build() 38 | 39 | } 40 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/internal/GridData.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal 2 | 3 | import org.hexworks.mixite.core.api.HexagonOrientation 4 | import org.hexworks.mixite.core.api.HexagonalGridLayout 5 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.GridLayoutStrategy 6 | import kotlin.math.sqrt 7 | 8 | /** 9 | * Immutable class which holds the shared data between the [org.hexworks.mixite.core.api.Hexagon]s of a 10 | * [org.hexworks.mixite.core.api.HexagonalGrid] and the HexagonalGrid's own immutable properties. 11 | */ 12 | class GridData(val orientation: HexagonOrientation, 13 | val gridLayout: GridLayoutStrategy, 14 | val radius: Double, 15 | val gridWidth: Int, 16 | val gridHeight: Int) { 17 | 18 | val hexagonHeight: Double 19 | val hexagonWidth: Double 20 | val innerRadius: Double 21 | 22 | init { 23 | if (orientation === HexagonOrientation.FLAT_TOP) { 24 | // FIXME These are the wrong way around! flat-top => width > height 25 | hexagonHeight = calculateHeight(radius) 26 | hexagonWidth = calculateWidth(radius) 27 | innerRadius = hexagonWidth / 2 28 | } else { 29 | hexagonHeight = calculateWidth(radius) 30 | hexagonWidth = calculateHeight(radius) 31 | innerRadius = hexagonHeight / 2 32 | } 33 | } 34 | 35 | private fun calculateHeight(radius: Double) = sqrt(3.0) * radius 36 | 37 | private fun calculateWidth(radius: Double) = radius * 3 / 2 38 | } 39 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/internal/impl/layoutstrategy/GridLayoutStrategy.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl.layoutstrategy 2 | 3 | import org.hexworks.mixite.core.api.CubeCoordinate 4 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 5 | import org.hexworks.mixite.core.api.contract.SatelliteData 6 | 7 | /** 8 | * Represents the method of creating a [org.hexworks.mixite.core.api.HexagonalGrid] corresponding to a given shape. 9 | */ 10 | abstract class GridLayoutStrategy { 11 | 12 | /** 13 | * Fetches a monotonically increasing (from left to right, top to bottom) Set of 14 | * grid coordinates corresponding to the shape of the requested grid layout. 15 | * 16 | * @param builder builder 17 | * 18 | * @return observable 19 | */ 20 | abstract fun fetchGridCoordinates(builder: HexagonalGridBuilder): Iterable 21 | 22 | /** 23 | * Checks whether the supplied parameters are valid for the given strategy. 24 | * *For example a hexagonal grid layout only works if the width equals to the height* 25 | * 26 | * @param gridHeight height 27 | * @param gridWidth width 28 | * 29 | * @return valid? 30 | */ 31 | abstract fun checkParameters(gridHeight: Int, gridWidth: Int): Boolean 32 | 33 | protected fun checkCommonCase(gridHeight: Int, gridWidth: Int): Boolean { 34 | return gridHeight > 0 && gridWidth > 0 35 | } 36 | 37 | /** 38 | * Returns the name of this strategy. 39 | * 40 | * @return name 41 | */ 42 | abstract fun getName() : String 43 | 44 | } 45 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/CustomGridLayoutStrategy.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl 2 | 3 | import org.hexworks.mixite.core.api.CoordinateConverter 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 6 | import org.hexworks.mixite.core.api.contract.SatelliteData 7 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.GridLayoutStrategy 8 | 9 | class CustomGridLayoutStrategy : GridLayoutStrategy() { 10 | 11 | override fun fetchGridCoordinates(builder: HexagonalGridBuilder): Iterable { 12 | val coords = ArrayList( builder.getGridHeight() * builder.getGridWidth()) 13 | for (y in 0 until builder.getGridHeight()) { 14 | val xStart = if (y == 0 || y == builder.getGridHeight() - 1) 1 else 0 15 | val xEnd = if (y == 0 || y == builder.getGridHeight() - 1) builder.getGridWidth() - 1 16 | else builder.getGridWidth() 17 | for (x in xStart until xEnd) { 18 | val gridX = CoordinateConverter.convertOffsetCoordinatesToCubeX(x, y, builder.getOrientation()) 19 | val gridZ = CoordinateConverter.convertOffsetCoordinatesToCubeZ(x, y, builder.getOrientation()) 20 | coords.add(CubeCoordinate.fromCoordinates(gridX, gridZ)) 21 | } 22 | } 23 | return coords 24 | } 25 | 26 | override fun checkParameters(gridHeight: Int, gridWidth: Int): Boolean { 27 | return true 28 | } 29 | 30 | override fun getName(): String { 31 | return "CUSTOM" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/defaults/DefaultHexagonDataStorage.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api.defaults 2 | 3 | import org.hexworks.cobalt.datatypes.Maybe 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.contract.HexagonDataStorage 6 | import org.hexworks.mixite.core.api.contract.SatelliteData 7 | 8 | class DefaultHexagonDataStorage : HexagonDataStorage { 9 | 10 | private val storage = LinkedHashMap>() 11 | 12 | override val coordinates: Iterable 13 | get() = storage.keys 14 | 15 | override fun addCoordinate(cubeCoordinate: CubeCoordinate) { 16 | storage[cubeCoordinate] = Maybe.empty() 17 | } 18 | 19 | override fun addCoordinate(cubeCoordinate: CubeCoordinate, satelliteData: T): Boolean { 20 | val previous = storage.put(cubeCoordinate, Maybe.of(satelliteData)) 21 | return previous != null 22 | } 23 | 24 | override fun getSatelliteDataBy(cubeCoordinate: CubeCoordinate): Maybe { 25 | return if (storage.containsKey(cubeCoordinate)) storage[cubeCoordinate]!! else Maybe.empty() 26 | } 27 | 28 | override fun containsCoordinate(cubeCoordinate: CubeCoordinate): Boolean { 29 | return storage.containsKey(cubeCoordinate) 30 | } 31 | 32 | override fun hasDataFor(cubeCoordinate: CubeCoordinate): Boolean { 33 | return storage.containsKey(cubeCoordinate) && storage[cubeCoordinate]!!.isPresent 34 | } 35 | 36 | override fun clearDataFor(cubeCoordinate: CubeCoordinate): Boolean { 37 | var result = false 38 | if (hasDataFor(cubeCoordinate)) { 39 | result = true 40 | } 41 | storage[cubeCoordinate] = Maybe.empty() 42 | return result 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/api/CubeCoordinateTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromAxialKey 4 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromCoordinates 5 | import kotlin.test.BeforeTest 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertFailsWith 9 | 10 | class CubeCoordinateTest { 11 | 12 | lateinit var target: CubeCoordinate 13 | 14 | @BeforeTest 15 | fun setUp() { 16 | target = fromCoordinates(TEST_GRID_X, TEST_GRID_Z) 17 | } 18 | 19 | @Test 20 | fun shouldReturnProperCoordinateWhenGetGridXIsCalled() { 21 | assertEquals(TEST_GRID_X, target.gridX) 22 | } 23 | 24 | @Test 25 | fun shouldBeEqualToItself() { 26 | assertEquals(target, target) 27 | } 28 | 29 | @Test 30 | fun shouldReturnProperCoordinateWhenGetGridZIsCalled() { 31 | assertEquals(TEST_GRID_Z, target.gridZ) 32 | } 33 | 34 | @Test 35 | fun shouldReturnProperKeyWhenToKeyIsCalled() { 36 | assertEquals(TEST_GRID_X.toString() + "," + TEST_GRID_Z, target.toAxialKey()) 37 | } 38 | 39 | @Test 40 | fun shouldCreateProperAxialCoordinateWhenFromKeyIsCalled() { 41 | val result = fromAxialKey(TEST_GRID_X.toString() + "," + TEST_GRID_Z) 42 | assertEquals(target.gridX, result.gridX) 43 | assertEquals(target.gridZ, result.gridZ) 44 | } 45 | 46 | @Test 47 | fun shouldFailToCreateCoordinateFromMalformedKey() { 48 | assertFailsWith { 49 | CubeCoordinate.fromAxialKey("asdf") 50 | } 51 | } 52 | 53 | companion object { 54 | 55 | private const val TEST_GRID_X = 4 56 | private const val TEST_GRID_Z = 5 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/api/RectangleTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertFalse 6 | import kotlin.test.assertTrue 7 | 8 | 9 | class RectangleTest { 10 | 11 | @Test 12 | fun containsPointTest() { 13 | val rectangle = Rectangle(0.0, 0.0, 100.0, 100.0) 14 | assertTrue(rectangle.contains(50.0, 50.0)) 15 | assertFalse(rectangle.contains(150.0, 110.0)) 16 | } 17 | 18 | @Test 19 | fun containsRectTest() { 20 | val rectangle = Rectangle(0.0, 0.0, 100.0, 100.0) 21 | val rectangle2 = Rectangle(0.0, 0.0, 10.0, 10.0) 22 | val rectangle3 = Rectangle(11.0, 11.0, 50.0, 50.0) 23 | assertFalse(rectangle.contains(rectangle)) 24 | assertFalse(rectangle.contains(rectangle2)) 25 | assertTrue(rectangle.contains(rectangle3)) 26 | } 27 | 28 | @Test 29 | fun overlapsTest() { 30 | val rectangle = Rectangle(30.0, 30.0, 100.0, 100.0) 31 | val rectangle2 = Rectangle(0.0, 0.0, 50.0, 50.0) 32 | val rectangle3 = Rectangle(31.0, 31.0, 50.0, 50.0) 33 | val rectangle4 = Rectangle(0.0, 0.0, 10.0, 10.0) 34 | assertTrue(rectangle.overlaps(rectangle2)) 35 | assertTrue(rectangle.overlaps(rectangle3)) 36 | assertFalse(rectangle.overlaps(rectangle4)) 37 | } 38 | 39 | 40 | @Test 41 | fun setCenterTest() { 42 | val rectangle = Rectangle(0.0, 0.0, 1.0, 1.0) 43 | rectangle.withCenter(1.0, 1.0) 44 | assertEquals(expected = 0.5, actual = rectangle.x) 45 | assertEquals(expected = 0.5, actual = rectangle.y) 46 | } 47 | 48 | @Test 49 | fun getAspectRatioTest() { 50 | val rectangle = Rectangle(0.0, 0.0, 1920.0, 1080.0) 51 | assertEquals(rectangle.aspectRatio, 16.0 / 9.0) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/HexagonStub.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core 2 | 3 | import org.hexworks.cobalt.datatypes.Maybe 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.Hexagon 6 | import org.hexworks.mixite.core.api.Point 7 | import org.hexworks.mixite.core.api.Rectangle 8 | import org.hexworks.mixite.core.api.contract.SatelliteData 9 | 10 | class HexagonStub(override val id: String = "", 11 | override val points: List = listOf(), 12 | override val vertices: List = listOf(), 13 | override val externalBoundingBox: Rectangle = Rectangle(0.0, 0.0, 0.0, 0.0), 14 | override val internalBoundingBox: Rectangle = Rectangle(0.0, 0.0, 0.0, 0.0), 15 | override val cubeCoordinate: CubeCoordinate = CubeCoordinate.fromCoordinates(0, 0), 16 | override val gridX: Int = 0, 17 | override val gridY: Int = 0, 18 | override val gridZ: Int = 0, 19 | override val center: Point = Point.fromPosition(0.0, 0.0), 20 | override val centerX: Double = center.coordinateY, 21 | override val centerY: Double = center.coordinateY, 22 | override val satelliteData: Maybe = Maybe.empty()) : Hexagon { 23 | 24 | override fun setSatelliteData(data: T) { 25 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 26 | } 27 | 28 | override fun clearSatelliteData() { 29 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mixite.benchmarks/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGridCalculatorPerfTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import kotlinx.benchmark.* 4 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 5 | import org.hexworks.mixite.core.internal.impl.HexagonalGridCalculatorImpl 6 | 7 | @State(Scope.Benchmark) 8 | @Warmup(iterations = 5) 9 | @Measurement(iterations = 5) 10 | @Suppress("Unused") 11 | open class HexagonalGridCalculatorPerfTest { 12 | 13 | private lateinit var calc: HexagonalGridCalculatorImpl 14 | private lateinit var hexFrom: Hexagon 15 | private lateinit var hexTo: Hexagon 16 | private lateinit var grid: HexagonalGrid 17 | 18 | @Setup 19 | fun setUp() { 20 | grid = HexagonalGridBuilder() 21 | .setGridLayout(HexagonalGridLayout.RECTANGULAR) 22 | .setGridHeight(100) 23 | .setGridWidth(100) 24 | .setRadius(10.0) 25 | .build() 26 | calc = HexagonalGridCalculatorImpl(grid) 27 | hexFrom = grid.getByCubeCoordinate(CubeCoordinate.fromCoordinates(2, 2)).get() 28 | hexTo = grid.getByCubeCoordinate(CubeCoordinate.fromCoordinates(7, 9)).get() 29 | } 30 | 31 | @Benchmark 32 | fun calculateRingFrom(bh: Blackhole) { 33 | val hexs = calc.calculateRingFrom(hexFrom, 2) 34 | for (hexagon in hexs) { 35 | bh.consume(hexagon) 36 | } 37 | } 38 | 39 | @Benchmark 40 | fun calculateMovementRangeFrom(bh: Blackhole) { 41 | val hexs = calc.calculateMovementRangeFrom(hexFrom, 2) 42 | for (hexagon in hexs) { 43 | bh.consume(hexagon) 44 | } 45 | } 46 | 47 | @Benchmark 48 | fun drawLine(bh: Blackhole) { 49 | val hexs = calc.drawLine(hexFrom, hexTo) 50 | for (hexagon in hexs) { 51 | bh.consume(hexagon) 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/internal/impl/layoutstrategy/HexagonalGridLayoutStrategy.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl.layoutstrategy 2 | 3 | import org.hexworks.mixite.core.api.CubeCoordinate 4 | import org.hexworks.mixite.core.api.HexagonOrientation 5 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 6 | import org.hexworks.mixite.core.api.contract.SatelliteData 7 | import kotlin.math.abs 8 | import kotlin.math.floor 9 | import kotlin.math.max 10 | import kotlin.math.round 11 | 12 | class HexagonalGridLayoutStrategy : GridLayoutStrategy() { 13 | 14 | override fun fetchGridCoordinates(builder: HexagonalGridBuilder): Iterable { 15 | val gridSize = builder.getGridHeight() 16 | val coords = ArrayList(1 + (gridSize * gridSize * 6 - 6) / 8) // TODO cell count owned by the builder 17 | var startX = if (HexagonOrientation.FLAT_TOP.equals(builder.getOrientation())) floor(gridSize / 2.0).toInt() else round(gridSize / 4.0).toInt() 18 | val hexRadius = floor(gridSize / 2.0).toInt() 19 | val minX = startX - hexRadius 20 | var y = 0 21 | while (y < gridSize) { 22 | val distanceFromMid = abs(hexRadius - y) 23 | for (x in max(startX, minX)..max(startX, minX) + hexRadius + hexRadius - distanceFromMid) { 24 | val gridZ = if (HexagonOrientation.FLAT_TOP.equals(builder.getOrientation())) y - floor(gridSize / 4.0).toInt() else y 25 | coords.add(CubeCoordinate.fromCoordinates(x, gridZ)) 26 | } 27 | startX-- 28 | y++ 29 | } 30 | return coords 31 | } 32 | 33 | override fun checkParameters(gridHeight: Int, gridWidth: Int): Boolean { 34 | val superResult = checkCommonCase(gridHeight, gridWidth) 35 | val result = gridHeight == gridWidth && abs(gridHeight % 2) == 1 36 | return result && superResult 37 | } 38 | 39 | override fun getName(): String { 40 | return "HEXAGONAL" 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/CubeCoordinate.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import kotlin.jvm.JvmStatic 4 | 5 | /** 6 | * Represents a cube coordinate pair. 7 | * See http://www.redblobgames.com/grids/hexagons/#coordinates to learn more. 8 | * Note that the y coordinate is not stored in this object since it can be 9 | * calculated. 10 | */ 11 | @Suppress("DataClassPrivateConstructor") 12 | data class CubeCoordinate private constructor(val gridX: Int, val gridZ: Int) { 13 | 14 | val gridY: Int 15 | get() = -(gridX + gridZ) 16 | 17 | /** 18 | * Creates an axial (x, z) key which can be used in key-value storage objects based on this 19 | * [CubeCoordinate]. 20 | * 21 | * @return key 22 | */ 23 | fun toAxialKey(): String { 24 | return gridX.toString() + SEP + gridZ 25 | } 26 | 27 | companion object { 28 | 29 | const val SEP = "," 30 | 31 | /** 32 | * Tries to create an [CubeCoordinate] from a key which has the format: 33 | * `%gridX%,%gridZ%`. 34 | * 35 | * @param axialKey key 36 | * 37 | * @return coord 38 | */ 39 | @JvmStatic 40 | fun fromAxialKey(axialKey: String): CubeCoordinate { 41 | val result: CubeCoordinate 42 | try { 43 | val coords = axialKey.split(SEP.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 44 | result = fromCoordinates(coords[0].toInt(), coords[1].toInt()) 45 | } catch (e: Exception) { 46 | throw IllegalArgumentException("Failed to create CubeCoordinate from key: $axialKey", e) 47 | } 48 | 49 | return result 50 | } 51 | 52 | /** 53 | * Creates an instance of [CubeCoordinate] from an x and a z coordinate. 54 | * 55 | * @param gridX grid x 56 | * @param gridZ grid z 57 | * 58 | * @return coord 59 | */ 60 | @JvmStatic 61 | fun fromCoordinates(gridX: Int, gridZ: Int): CubeCoordinate { 62 | return CubeCoordinate(gridX, gridZ) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/contract/HexagonDataStorage.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api.contract 2 | 3 | import org.hexworks.cobalt.datatypes.Maybe 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | 6 | /** 7 | * This interface represents all storage operations which are needed for a working 8 | * [HexagonalGrid]. 9 | * @param the type of the stored [SatelliteData] implementation 10 | */ 11 | interface HexagonDataStorage { 12 | 13 | /** 14 | * Returns all coordinates which are stored in this object. 15 | */ 16 | val coordinates: Iterable 17 | 18 | /** 19 | * Adds a [CubeCoordinate] for this grid without any [SatelliteData]. 20 | * Does not overwrite the coordinate if it is already present. 21 | */ 22 | fun addCoordinate(cubeCoordinate: CubeCoordinate) 23 | 24 | /** 25 | * Adds a [CubeCoordinate] for this grid with [SatelliteData]. 26 | * Overwrites previous [SatelliteData] if it was present. 27 | * @return true if overwrote data false otherwise. 28 | */ 29 | fun addCoordinate(cubeCoordinate: CubeCoordinate, satelliteData: T): Boolean 30 | 31 | /** 32 | * Gets the [SatelliteData] stored on a [CubeCoordinate] if present. 33 | * Also returns empty [Maybe] when `cubeCoordinate` is not present. 34 | * @return optional [SatelliteData]. 35 | */ 36 | fun getSatelliteDataBy(cubeCoordinate: CubeCoordinate): Maybe 37 | 38 | /** 39 | * Tells whether there is a [Hexagon] on the given [CubeCoordinate] or not. 40 | * @return true if present false if not 41 | */ 42 | fun containsCoordinate(cubeCoordinate: CubeCoordinate): Boolean 43 | 44 | /** 45 | * Tells whether there is [SatelliteData] stored for a [CubeCoordinate] or not. 46 | * Also returns false if `cubeCoordinate` is not present in the storage. 47 | */ 48 | fun hasDataFor(cubeCoordinate: CubeCoordinate): Boolean 49 | 50 | /** 51 | * Clears the [SatelliteData] for the given [CubeCoordinate]. 52 | * @return true if the storage was changed false otherwise. 53 | */ 54 | fun clearDataFor(cubeCoordinate: CubeCoordinate): Boolean 55 | 56 | } 57 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/api/HexagonalGridBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.mixite.core.api.HexagonOrientation.FLAT_TOP 4 | import org.hexworks.mixite.core.api.HexagonalGridLayout.RECTANGULAR 5 | import org.hexworks.mixite.core.api.HexagonalGridLayout.TRIANGULAR 6 | import org.hexworks.mixite.core.api.contract.SatelliteData 7 | import kotlin.test.* 8 | 9 | class HexagonalGridBuilderTest { 10 | 11 | lateinit var target: HexagonalGridBuilder 12 | 13 | @BeforeTest 14 | fun setUp() { 15 | target = HexagonalGridBuilder() 16 | target.setGridHeight(GRID_HEIGHT) 17 | .setGridLayout(GRID_LAYOUT) 18 | .setGridWidth(GRID_WIDTH) 19 | .setOrientation(ORIENTATION) 20 | .setRadius(RADIUS) 21 | } 22 | 23 | @Test 24 | fun shouldContainProperValuesWhenGettersAreCalled() { 25 | assertEquals(GRID_HEIGHT, target.getGridHeight()) 26 | assertEquals(GRID_WIDTH, target.getGridWidth()) 27 | assertEquals(GRID_LAYOUT_STRATEGY, target.gridLayoutStrategy) 28 | assertEquals(RADIUS, target.getRadius()) 29 | assertNotNull(target.gridData) 30 | } 31 | 32 | @Test 33 | fun shouldFailGettingSharedHexagonDataWhenOrientationIsNull() { 34 | assertFailsWith { 35 | target.setRadius(0.0) 36 | target.gridData 37 | } 38 | } 39 | 40 | 41 | @Test 42 | fun shouldFailBuildWhenSizeIsNotCompatibleWithLayout() { 43 | assertFailsWith { 44 | target.setGridLayout(TRIANGULAR) 45 | target.setGridHeight(4) 46 | target.build() 47 | } 48 | } 49 | 50 | @Test 51 | fun shouldReturnProperOrientationWhenGetOrientationIsCalled() { 52 | assertEquals(ORIENTATION, target.getOrientation()) 53 | } 54 | 55 | @Test 56 | fun shouldBuildWhenProperParametersArePresent() { 57 | val grid = target.build() 58 | assertNotNull(grid) 59 | } 60 | 61 | companion object { 62 | 63 | private const val GRID_HEIGHT = 9 64 | private const val GRID_WIDTH = 9 65 | private const val RADIUS = 30.0 66 | private val GRID_LAYOUT = RECTANGULAR 67 | private val GRID_LAYOUT_STRATEGY = RECTANGULAR.gridLayoutStrategy 68 | private val ORIENTATION = FLAT_TOP 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/RectangularGridLayoutHexagonImplTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl 2 | 3 | import org.hexworks.mixite.core.api.CoordinateConverter 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.Hexagon 6 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 7 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 8 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.RectangularGridLayoutStrategy 9 | import kotlin.test.assertEquals 10 | import kotlin.test.assertTrue 11 | 12 | class RectangularGridLayoutHexagonImplTest : HexagonalGridImplTest() { 13 | 14 | override fun getBuilder(): HexagonalGridBuilder { 15 | return HexagonalGridBuilder() 16 | .setGridHeight(GRID_HEIGHT) 17 | .setGridWidth(GRID_WIDTH) 18 | .setRadius(RADIUS.toDouble()) 19 | .setOrientation(ORIENTATION) 20 | } 21 | 22 | override fun shouldReturnProperHexagonsWhenEachHexagonIsTestedInAGivenGrid() { 23 | val hexagons = target.hexagons 24 | val expectedCoordinates = HashSet() 25 | for (x in 0 until GRID_WIDTH) { 26 | for (y in 0 until GRID_HEIGHT) { 27 | val gridX = CoordinateConverter.convertOffsetCoordinatesToCubeX(x, y, ORIENTATION) 28 | val gridZ = CoordinateConverter.convertOffsetCoordinatesToCubeZ(x, y, ORIENTATION) 29 | expectedCoordinates.add("$gridX,$gridZ") 30 | } 31 | } 32 | var count = 0 33 | for (hexagon in hexagons) { 34 | expectedCoordinates.remove("${hexagon.gridX},${hexagon.gridZ}") 35 | count++ 36 | } 37 | assertEquals(100, count) 38 | assertTrue(expectedCoordinates.isEmpty()) 39 | } 40 | 41 | override fun shouldReturnProperNeighborsOfHexagonWhenHexIsOnTheEdge() { 42 | val hex = target.getByCubeCoordinate(CubeCoordinate.fromCoordinates(5, 9)).get() 43 | val expected = HashSet>() 44 | expected.add(target.getByCubeCoordinate(CubeCoordinate.fromCoordinates(5, 8)).get()) 45 | expected.add(target.getByCubeCoordinate(CubeCoordinate.fromCoordinates(4, 9)).get()) 46 | val actual = target.getNeighborsOf(hex) 47 | assertEquals(expected, actual) 48 | } 49 | 50 | override fun shouldProperlyReturnGridLayoutWhenGetGridLayoutIsCalled() { 51 | assertTrue(target.gridData.gridLayout is RectangularGridLayoutStrategy) 52 | } 53 | } -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/Hexagon.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.cobalt.datatypes.Maybe 4 | import org.hexworks.mixite.core.api.contract.SatelliteData 5 | 6 | /** 7 | * Represents a Hexagon. 8 | * *Please note* that all coordinates are relative to the [HexagonalGrid] containing this [Hexagon]. 9 | */ 10 | interface Hexagon { 11 | 12 | /** 13 | * Returns an unique [String] representing this [Hexagon]. 14 | */ 15 | val id: String 16 | 17 | /** 18 | * Returns a list containing the [Point]s of this [Hexagon]. 19 | */ 20 | val points: List 21 | 22 | /** 23 | * Returns an array of the vertices of this [Hexagon]. 24 | */ 25 | val vertices: List 26 | 27 | /** 28 | * Returns a rectangle defining the **external** boundary box of this [Hexagon] 29 | * (a rectangle that hits the 2 pointy corners and the 2 flat sides). 30 | */ 31 | val externalBoundingBox: Rectangle 32 | 33 | /** 34 | * Returns a rectangle defining the **internal** boundary box of this [Hexagon] 35 | * (the biggest rectangle that hits the outline of the [Hexagon] exactly 4 times). 36 | */ 37 | val internalBoundingBox: Rectangle 38 | 39 | /** 40 | * Returns the [CubeCoordinate] of this [Hexagon]. 41 | */ 42 | val cubeCoordinate: CubeCoordinate 43 | 44 | /** 45 | * Returns this [Hexagon]'s **x** (cube) coordinate on the [HexagonalGrid]. 46 | */ 47 | val gridX: Int 48 | 49 | /** 50 | * Returns this [Hexagon]'s **y** coordinate on the [HexagonalGrid]. 51 | * The Y coordinate is not present in the cube model but it is in the cube model. 52 | * This method is just for convenience. 53 | */ 54 | val gridY: Int 55 | 56 | /** 57 | * Returns this [Hexagon]'s **z** (cube) coordinate on the [HexagonalGrid]. 58 | */ 59 | val gridZ: Int 60 | 61 | /** 62 | * Returns the center (pixel) [Point] of this [Hexagon]. 63 | */ 64 | val center: Point 65 | 66 | /** 67 | * Returns the center **x** (pixel) coordinate of this [Hexagon]. 68 | */ 69 | @Deprecated("use center") 70 | val centerX: Double 71 | 72 | /** 73 | * Returns the center **y** (pixel) coordinate of this [Hexagon]. 74 | * 75 | * @return center y 76 | */ 77 | @Deprecated("use center") 78 | val centerY: Double 79 | 80 | /** 81 | * Returns this [Hexagon]'s satellite data. 82 | */ 83 | val satelliteData: Maybe 84 | 85 | /** 86 | * Can be used to add arbitrary satellite data to a [Hexagon]. 87 | */ 88 | fun setSatelliteData(data: T) 89 | 90 | /** 91 | * Clears the satellite data of this Hexagon. 92 | */ 93 | fun clearSatelliteData() 94 | 95 | } 96 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/CustomGridLayoutHexagonImplTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl 2 | 3 | import org.hexworks.mixite.core.api.CoordinateConverter 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.Hexagon 6 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 7 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 8 | import kotlin.test.assertEquals 9 | import kotlin.test.assertTrue 10 | 11 | class CustomGridLayoutHexagonImplTest : HexagonalGridImplTest() { 12 | 13 | override fun getBuilder(): HexagonalGridBuilder { 14 | return HexagonalGridBuilder() 15 | .setGridLayout(CustomGridLayoutStrategy()) 16 | .setGridHeight(GRID_HEIGHT) 17 | .setGridWidth(GRID_WIDTH) 18 | .setRadius(RADIUS.toDouble()) 19 | .setOrientation(ORIENTATION) 20 | } 21 | 22 | override fun shouldReturnProperHexagonsWhenEachHexagonIsTestedInAGivenGrid() { 23 | val hexagons = target.hexagons 24 | val expectedCoordinates = HashSet() 25 | for (x in 0 until GRID_WIDTH) { 26 | for (y in 0 until GRID_HEIGHT) { 27 | if (y == 0 && x == 0 || y == 0 && x == GRID_WIDTH - 1 || y == GRID_HEIGHT - 1 && x == 0 || 28 | y == GRID_HEIGHT - 1 && x == GRID_WIDTH - 1) { 29 | continue 30 | } 31 | val gridX = CoordinateConverter.convertOffsetCoordinatesToCubeX(x, y, ORIENTATION) 32 | val gridZ = CoordinateConverter.convertOffsetCoordinatesToCubeZ(x, y, ORIENTATION) 33 | expectedCoordinates.add("$gridX,$gridZ") 34 | } 35 | } 36 | var count = 0 37 | for (hexagon in hexagons) { 38 | expectedCoordinates.remove("${hexagon.gridX},${hexagon.gridZ}") 39 | count++ 40 | } 41 | assertEquals(96, count) 42 | assertTrue(expectedCoordinates.isEmpty()) 43 | } 44 | 45 | override fun shouldReturnProperNeighborsOfHexagonWhenHexIsOnTheEdge() { 46 | val hex = target.getByCubeCoordinate(CubeCoordinate.fromCoordinates(-4, 8)).get() 47 | val expected = HashSet>() 48 | expected.add(target.getByCubeCoordinate(CubeCoordinate.fromCoordinates(-3, 7)).get()) 49 | expected.add(target.getByCubeCoordinate(CubeCoordinate.fromCoordinates(-3, 8)).get()) 50 | val actual = target.getNeighborsOf(hex) 51 | assertEquals(expected, actual) 52 | } 53 | 54 | override fun shouldProperlyReturnGridLayoutWhenGetGridLayoutIsCalled() { 55 | assertTrue(target.gridData.gridLayout is CustomGridLayoutStrategy) 56 | } 57 | } -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/Rectangle.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | class Rectangle { 4 | var x: Double = 0.0 5 | var y: Double = 0.0 6 | var width: Double = 0.0 7 | var height: Double = 0.0 8 | 9 | 10 | val aspectRatio: Double 11 | get() = if (height == 0.0) Double.NaN else width / height 12 | 13 | 14 | constructor(x: Double, y: Double, width: Double, height: Double) { 15 | this.x = x 16 | this.y = y 17 | this.width = width 18 | this.height = height 19 | } 20 | 21 | 22 | constructor(rect: Rectangle) { 23 | x = rect.x 24 | y = rect.y 25 | width = rect.width 26 | height = rect.height 27 | } 28 | 29 | fun withRectangle(x: Double, y: Double, width: Double, height: Double) = also { 30 | this.x = x 31 | this.y = y 32 | this.width = width 33 | this.height = height 34 | 35 | return this 36 | } 37 | 38 | fun withX(x: Double) = also { 39 | this.x = x 40 | } 41 | 42 | fun withY(y: Double) = also { 43 | this.y = y 44 | } 45 | 46 | fun withWidth(width: Double) = also { 47 | this.width = width 48 | } 49 | 50 | fun withHeight(height: Double) = also { 51 | this.height = height 52 | } 53 | 54 | 55 | fun withPosition(x: Double, y: Double) = also { 56 | this.x = x 57 | this.y = y 58 | } 59 | 60 | fun withSize(width: Double, height: Double) = also { 61 | this.width = width 62 | this.height = height 63 | } 64 | 65 | fun withSize(sizeXY: Double) = also { 66 | this.width = sizeXY 67 | this.height = sizeXY 68 | } 69 | 70 | fun withRectangle(rect: Rectangle) = also { 71 | this.x = rect.x 72 | this.y = rect.y 73 | this.width = rect.width 74 | this.height = rect.height 75 | } 76 | 77 | fun contains(x: Double, y: Double): Boolean { 78 | return this.x <= x && this.x + this.width >= x && this.y <= y && this.y + this.height >= y 79 | } 80 | 81 | operator fun contains(rectangle: Rectangle): Boolean { 82 | val xMin = rectangle.x 83 | val xMax = xMin + rectangle.width 84 | 85 | val yMin = rectangle.y 86 | val yMax = yMin + rectangle.height 87 | 88 | return xMin > x && xMin < x + width && xMax > x && xMax < x + width && yMin > y && yMin < y + height && yMax > y && yMax < y + height 89 | } 90 | 91 | fun overlaps(r: Rectangle): Boolean { 92 | return x < r.x + r.width && x + width > r.x && y < r.y + r.height && y + height > r.y 93 | } 94 | 95 | fun withCenter(x: Double, y: Double) = also { 96 | withPosition(x - width / 2, y - height / 2) 97 | } 98 | 99 | override fun toString(): String { 100 | return "[$x,$y,$width,$height]" 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/CoordinateConverter.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import kotlin.jvm.JvmStatic 4 | 5 | /** 6 | * Utility class for converting coordinated from the offset system to 7 | * the cube one (the library uses the latter). 8 | */ 9 | class CoordinateConverter { 10 | init { 11 | throw UnsupportedOperationException("This utility class is not meant to be instantiated.") 12 | } 13 | 14 | companion object { 15 | 16 | /** 17 | * Calculates the cube X coordinate based on an offset coordinate pair. 18 | * 19 | * @param offsetX offset x 20 | * @param offsetY offset y 21 | * @param orientation orientation 22 | * 23 | * @return cube x 24 | */ 25 | @JvmStatic 26 | fun convertOffsetCoordinatesToCubeX(offsetX: Int, offsetY: Int, orientation: HexagonOrientation): Int { 27 | return if (HexagonOrientation.FLAT_TOP.equals(orientation)) offsetX else offsetX - offsetY / 2 28 | } 29 | 30 | /** 31 | * Calculates the cube Z coordinate based on an offset coordinate pair. 32 | * 33 | * @param offsetX offset x 34 | * @param offsetY offset y 35 | * @param orientation orientation 36 | * 37 | * @return cube z 38 | */ 39 | @JvmStatic 40 | fun convertOffsetCoordinatesToCubeZ(offsetX: Int, offsetY: Int, orientation: HexagonOrientation): Int { 41 | return if (HexagonOrientation.FLAT_TOP.equals(orientation)) offsetY - offsetX / 2 else offsetY 42 | } 43 | 44 | /** 45 | * Calculates the offset row based on a CubeCoordinate. 46 | * 47 | * @Param cubeCoordinate a cube coordinate 48 | * @param orientation orientation 49 | * 50 | * @return offset row or y-value 51 | */ 52 | @JvmStatic 53 | fun convertCubeCoordinateToOffsetRow(coordinate: CubeCoordinate, orientation: HexagonOrientation): Int { 54 | return if(HexagonOrientation.FLAT_TOP.equals(orientation)) { 55 | coordinate.gridZ + (coordinate.gridX - (coordinate.gridX and 1)) / 2 56 | } else { 57 | coordinate.gridZ 58 | } 59 | } 60 | 61 | /** 62 | * Calculates the offset column based on a CubeCoordinate. 63 | * 64 | * @Param cubeCoordinate a cube coordinate 65 | * @param orientation orientation 66 | * 67 | * @return offset column or x-value 68 | */ 69 | @JvmStatic 70 | fun convertCubeCoordinateToOffsetColumn(coordinate: CubeCoordinate, orientation: HexagonOrientation): Int { 71 | return if(HexagonOrientation.FLAT_TOP.equals(orientation)) { 72 | coordinate.gridX 73 | } else { 74 | coordinate.gridX + (coordinate.gridZ - (coordinate.gridZ and 1)) / 2 75 | } 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/GridDataTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal 2 | 3 | import org.hexworks.mixite.core.api.HexagonOrientation.FLAT_TOP 4 | import org.hexworks.mixite.core.api.HexagonOrientation.POINTY_TOP 5 | import org.hexworks.mixite.core.api.HexagonalGridLayout.RECTANGULAR 6 | import kotlin.math.sqrt 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | class GridDataTest { 11 | 12 | lateinit var target: GridData 13 | 14 | @Test 15 | fun shouldProperlyReturnRadiusWhenGetRadiusIsCalled() { 16 | target = GridData(ORIENTATION, GRID_LAYOUT, RADIUS, GRID_WIDTH, GRID_HEIGHT) 17 | assertEquals(RADIUS, target.radius) 18 | } 19 | 20 | @Test 21 | fun shouldProperlyCalculateWidthWithPointyHexagonsWhenGetWidthIsCalled() { 22 | target = createWithPointy() 23 | val expectedWidth = sqrt(3.0) * RADIUS 24 | val actualWidth = target.hexagonWidth 25 | assertEquals(expectedWidth, actualWidth) 26 | } 27 | 28 | private fun createWithPointy(): GridData { 29 | return GridData(POINTY_TOP, GRID_LAYOUT, RADIUS, GRID_WIDTH, GRID_HEIGHT) 30 | } 31 | 32 | @Test 33 | fun shouldProperlyCalculateWidthWithFlatHexagonsWhenGetWidthIsCalled() { 34 | target = createWithFlat() 35 | val expectedWidth = RADIUS * 3 / 2 36 | val actualWidth = target.hexagonWidth 37 | assertEquals(expectedWidth, actualWidth) 38 | } 39 | 40 | private fun createWithFlat(): GridData { 41 | return GridData(FLAT_TOP, GRID_LAYOUT, RADIUS, GRID_WIDTH, GRID_HEIGHT) 42 | } 43 | 44 | @Test 45 | fun shouldProperlyCalculateHeightWithPointyHexagonsWhenGetHeightIsCalled() { 46 | target = createWithPointy() 47 | val expectedHeight = RADIUS * 3 / 2 48 | val actualHeight = target.hexagonHeight 49 | assertEquals(expectedHeight, actualHeight) 50 | } 51 | 52 | @Test 53 | fun shouldProperlyCalculateHeightWithFlatHexagonsWhenGetHeightIsCalled() { 54 | target = createWithFlat() 55 | val expectedHeight = sqrt(3.0) * RADIUS 56 | val actualHeight = target.hexagonHeight 57 | assertEquals(expectedHeight, actualHeight) 58 | } 59 | 60 | @Test 61 | fun shouldReturnProperOrientationWhenGetOrientationIsCalled() { 62 | target = GridData(ORIENTATION, GRID_LAYOUT, RADIUS, GRID_WIDTH, GRID_HEIGHT) 63 | assertEquals(ORIENTATION, target.orientation) 64 | } 65 | 66 | @Test 67 | fun shouldReturnProperCoordinateOffsetWhengetCoordinateOffsetIsCalled() { 68 | target = GridData(ORIENTATION, GRID_LAYOUT, RADIUS, GRID_WIDTH, GRID_HEIGHT) 69 | assertEquals(ORIENTATION.coordinateOffset, target.orientation.coordinateOffset) 70 | } 71 | 72 | companion object { 73 | 74 | private const val RADIUS = 30.0 75 | private const val GRID_WIDTH = 30 76 | private const val GRID_HEIGHT = 30 77 | private val ORIENTATION = FLAT_TOP 78 | private val GRID_LAYOUT = RECTANGULAR.gridLayoutStrategy 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/layoutstrategy/TrapezoidGridLayoutStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl.layoutstrategy 2 | 3 | import org.hexworks.mixite.core.GridLayoutStrategyTestUtil 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromCoordinates 6 | import org.hexworks.mixite.core.api.HexagonOrientation.FLAT_TOP 7 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 8 | import org.hexworks.mixite.core.api.contract.SatelliteData 9 | import kotlin.test.BeforeTest 10 | import kotlin.test.Test 11 | import kotlin.test.assertTrue 12 | 13 | class TrapezoidGridLayoutStrategyTest { 14 | 15 | private lateinit var builder: HexagonalGridBuilder 16 | private lateinit var target: TrapezoidGridLayoutStrategy 17 | 18 | @BeforeTest 19 | fun setUp() { 20 | builder = GridLayoutStrategyTestUtil.fetchDefaultBuilder() 21 | target = TrapezoidGridLayoutStrategy() 22 | } 23 | 24 | @Test 25 | fun shouldProperlyCreateHexagonsWithPointyOrientationWhenCreateHexagonsIsCalled() { 26 | testCoordinates(target.fetchGridCoordinates(builder).iterator()) 27 | } 28 | 29 | private fun testCoordinates(coordIter: Iterator) { 30 | val coords = ArrayList() 31 | while (coordIter.hasNext()) { 32 | coords.add(coordIter.next()) 33 | } 34 | 35 | assertTrue(coords.contains(fromCoordinates(0, 0))) 36 | assertTrue(coords.contains(fromCoordinates(1, 0))) 37 | assertTrue(coords.contains(fromCoordinates(2, 0))) 38 | assertTrue(coords.contains(fromCoordinates(2, 1))) 39 | assertTrue(coords.contains(fromCoordinates(2, 2))) 40 | assertTrue(coords.contains(fromCoordinates(1, 2))) 41 | assertTrue(coords.contains(fromCoordinates(0, 2))) 42 | assertTrue(coords.contains(fromCoordinates(0, 1))) 43 | 44 | assertTrue(!coords.contains(fromCoordinates(-1, 0))) 45 | assertTrue(!coords.contains(fromCoordinates(0, -1))) 46 | assertTrue(!coords.contains(fromCoordinates(1, -1))) 47 | assertTrue(!coords.contains(fromCoordinates(2, -1))) 48 | assertTrue(!coords.contains(fromCoordinates(3, -1))) 49 | assertTrue(!coords.contains(fromCoordinates(3, 0))) 50 | assertTrue(!coords.contains(fromCoordinates(3, 1))) 51 | assertTrue(!coords.contains(fromCoordinates(3, 2))) 52 | assertTrue(!coords.contains(fromCoordinates(2, 3))) 53 | assertTrue(!coords.contains(fromCoordinates(1, 3))) 54 | assertTrue(!coords.contains(fromCoordinates(0, 3))) 55 | assertTrue(!coords.contains(fromCoordinates(-1, 2))) 56 | assertTrue(!coords.contains(fromCoordinates(-1, 1))) 57 | } 58 | 59 | @Test 60 | fun shouldProperlyCreateHexagonsWithFlatOrientationWhenCreateHexagonsIsCalled() { 61 | builder.setOrientation(FLAT_TOP) 62 | testCoordinates(target.fetchGridCoordinates(builder).iterator()) 63 | } 64 | 65 | @Test 66 | fun shouldReturnTrueWhenCheckParametersIsCalled() { 67 | assertTrue(target.checkParameters(2, 2)) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGridCalculator.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.cobalt.datatypes.Maybe 4 | import org.hexworks.mixite.core.api.contract.SatelliteData 5 | 6 | /** 7 | * Supports advanced operations on a [HexagonalGrid]. 8 | * Operations supported: 9 | * 10 | * * Calculating distance between 2 [Hexagon]s. 11 | * * Calculating movement range from a [Hexagon] using an arbitrary distance. 12 | * 13 | * *Not implemented yet, but are on the roadmap:* 14 | * 15 | * * Calculating movement range with obstacles 16 | * * Calculating field of view 17 | * * Path finding between two [Hexagon]s (using obstacles) 18 | * 19 | */ 20 | interface HexagonalGridCalculator { 21 | 22 | /** 23 | * The hexagonal grid used for the calculations 24 | */ 25 | val hexagonalGrid: HexagonalGrid 26 | 27 | /** 28 | * Calculates the distance (in hexagons) between two [Hexagon] objects on the grid. 29 | * 30 | * @param hex0 hex 0 31 | * @param hex1 hex 1 32 | * 33 | * @return distance 34 | */ 35 | fun calculateDistanceBetween(hex0: Hexagon, hex1: Hexagon): Int 36 | 37 | /** 38 | * Returns all [Hexagon]s which are within `distance` (inclusive) from the [Hexagon]. 39 | * 40 | * @param hexagon [Hexagon] 41 | * @param distance distance 42 | * 43 | * @return [Hexagon]s within distance (inclusive) 44 | */ 45 | fun calculateMovementRangeFrom(hexagon: Hexagon, distance: Int): Set> 46 | 47 | /** 48 | * Returns the Hexagon on the grid which is at the point resulted by rotating the `targetHex`'s 49 | * coordinates around the `originalHex` by `rotationDirection` degrees. 50 | * 51 | * @param originalHex center hex 52 | * @param targetHex hex to rotate 53 | * @param rotationDirection direction of the rotation 54 | * 55 | * @return result 56 | */ 57 | fun rotateHexagon(originalHex: Hexagon, targetHex: Hexagon, rotationDirection: RotationDirection): Maybe> 58 | 59 | 60 | /** 61 | * Returns the [Set] of [Hexagon]s which are `radius` distance 62 | * from `centerHexagon`. 63 | * 64 | * @param centerHexagon center 65 | * @param radius radius 66 | * 67 | * @return Set of hexagons or empty set if not applicable 68 | */ 69 | fun calculateRingFrom(centerHexagon: Hexagon, radius: Int): Set> 70 | 71 | /** 72 | * Returns a [List] of [Hexagon]s which must be traversed in the 73 | * given order to go from the `from` Hexagon to the `to` Hexagon. 74 | * 75 | * @param from starting hexagon 76 | * @param to target hexagon 77 | * 78 | * @return List of hexagons containing the line 79 | */ 80 | fun drawLine(from: Hexagon, to: Hexagon): List> 81 | 82 | /** 83 | * Returns true if the `from` [Hexagon] is visible from the `to` Hexagon. 84 | * 85 | * @param from the Hexagon that we are testing the visibility from 86 | * @param to the Hexagon from which we are testing the visibility to 87 | * 88 | * @return true if hexagon is visible, false otherwise 89 | */ 90 | fun isVisible(from: Hexagon, to: Hexagon): Boolean 91 | } 92 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/layoutstrategy/TriangularGridLayoutStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl.layoutstrategy 2 | 3 | import org.hexworks.mixite.core.GridLayoutStrategyTestUtil.fetchDefaultBuilder 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromCoordinates 6 | import org.hexworks.mixite.core.api.HexagonOrientation.FLAT_TOP 7 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 8 | import org.hexworks.mixite.core.api.contract.SatelliteData 9 | import kotlin.test.BeforeTest 10 | import kotlin.test.Test 11 | import kotlin.test.assertFalse 12 | import kotlin.test.assertTrue 13 | 14 | class TriangularGridLayoutStrategyTest { 15 | 16 | private lateinit var builder: HexagonalGridBuilder 17 | private lateinit var target: TriangularGridLayoutStrategy 18 | 19 | @BeforeTest 20 | fun setUp() { 21 | builder = fetchDefaultBuilder() 22 | target = TriangularGridLayoutStrategy() 23 | } 24 | 25 | @Test 26 | fun shouldProperlyCreateHexagonsWithPointyOrientationWhenCreateHexagonsIsCalled() { 27 | testCoordinates(target.fetchGridCoordinates(builder).iterator()) 28 | } 29 | 30 | @Test 31 | fun shouldProperlyCreateHexagonsWithFlatOrientationWhenCreateHexagonsIsCalled() { 32 | builder.setOrientation(FLAT_TOP) 33 | testCoordinates(target.fetchGridCoordinates(builder).iterator()) 34 | } 35 | 36 | @Test 37 | fun shouldReturnTrueWhenCheckParametersWithOneAndOne() { 38 | val result = target.checkParameters(1, 1) // super: true, derived: true 39 | assertTrue(result) 40 | } 41 | 42 | @Test 43 | fun shouldReturnTrueWhenCheckParametersWithOneAndTwo() { 44 | val result = target.checkParameters(1, 2) // super: true, derived: false 45 | assertFalse(result) 46 | } 47 | 48 | @Test 49 | fun shouldReturnTrueWhenCheckParametersWithZeroAndZero() { 50 | val result = target.checkParameters(0, 0) // super: false, derived: false; 51 | assertFalse(result) 52 | } 53 | 54 | @Test 55 | fun shouldReturnTrueWhenCheckParametersWithMinusOneAndMinusOne() { 56 | val result = target.checkParameters(-1, -1) // super: false, derived: true; 57 | assertFalse(result) 58 | } 59 | 60 | private fun testCoordinates(coordIter: Iterator) { 61 | 62 | val coords = ArrayList() 63 | while (coordIter.hasNext()) { 64 | coords.add(coordIter.next()) 65 | } 66 | assertTrue(coords.contains(fromCoordinates(0, 0))) 67 | assertTrue(coords.contains(fromCoordinates(1, 0))) 68 | assertTrue(coords.contains(fromCoordinates(2, 0))) 69 | assertTrue(coords.contains(fromCoordinates(0, 1))) 70 | assertTrue(coords.contains(fromCoordinates(1, 1))) 71 | assertTrue(coords.contains(fromCoordinates(0, 2))) 72 | 73 | assertTrue(!coords.contains(fromCoordinates(-1, 0))) 74 | assertTrue(!coords.contains(fromCoordinates(0, -1))) 75 | assertTrue(!coords.contains(fromCoordinates(1, -1))) 76 | assertTrue(!coords.contains(fromCoordinates(2, -1))) 77 | assertTrue(!coords.contains(fromCoordinates(3, -1))) 78 | assertTrue(!coords.contains(fromCoordinates(3, 0))) 79 | assertTrue(!coords.contains(fromCoordinates(2, 1))) 80 | assertTrue(!coords.contains(fromCoordinates(1, 2))) 81 | assertTrue(!coords.contains(fromCoordinates(0, 3))) 82 | assertTrue(!coords.contains(fromCoordinates(-1, 3))) 83 | assertTrue(!coords.contains(fromCoordinates(-1, 2))) 84 | assertTrue(!coords.contains(fromCoordinates(-1, 1))) 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /mixite.benchmarks/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGridPerfTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import kotlinx.benchmark.* 4 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 5 | import kotlin.math.PI 6 | import kotlin.math.cos 7 | import kotlin.math.sin 8 | 9 | @State(Scope.Benchmark) 10 | @Warmup(iterations = 5) 11 | @Measurement(iterations = 5) 12 | @Suppress("Unused") 13 | open class HexagonalGridPerfTest { 14 | 15 | private lateinit var hexRandom: Hexagon 16 | private lateinit var ccTo: CubeCoordinate 17 | private lateinit var ccFrom: CubeCoordinate 18 | private lateinit var grid: HexagonalGrid 19 | private lateinit var spiralPath: List 20 | private lateinit var circlePath: List 21 | 22 | @Setup 23 | fun setUp() { 24 | grid = HexagonalGridBuilder() 25 | .setGridLayout(HexagonalGridLayout.RECTANGULAR) 26 | .setGridHeight(100) 27 | .setGridWidth(100) 28 | .setRadius(10.0) 29 | .build() 30 | ccFrom = CubeCoordinate.fromCoordinates(-49, 0) 31 | ccTo = CubeCoordinate.fromCoordinates(99, 99) 32 | hexRandom = grid.getByCubeCoordinate(CubeCoordinate.fromCoordinates(1, 1)).get() 33 | } 34 | 35 | @Setup 36 | fun createSpiralMousePath() { 37 | val steps = 24 // 24 points around a circle 38 | val turns = 10 39 | val points = ArrayList() 40 | for (p in 0..steps * turns) { 41 | val angle = (steps * 2 * PI) / (p % steps) 42 | val distance = 2000.0 * p / (steps * turns) 43 | points += Point.fromPosition( 44 | distance * cos(angle) + 1000, 45 | distance * sin(angle) + 500) 46 | } 47 | spiralPath = points 48 | } 49 | 50 | @Setup 51 | fun createCircularMousePath() { 52 | val steps = 24 // 24 points around a circle 53 | val turns = 10 54 | val points = ArrayList() 55 | for (p in 0..steps * turns) { 56 | val angle = (steps * 2 * PI) / (p % steps) 57 | val distance = 300 58 | points += Point.fromPosition( 59 | distance * cos(angle) + 1000, 60 | distance * sin(angle) + 500) 61 | } 62 | circlePath = points 63 | } 64 | 65 | @Benchmark 66 | fun iterateHexagons(bh: Blackhole) { 67 | val hexs = grid.hexagons 68 | for (hexagon in hexs) { 69 | bh.consume(hexagon) 70 | } 71 | } 72 | 73 | @Benchmark 74 | fun getHexagonsByOffsetRange(bh: Blackhole) { 75 | // Gets all hexs so the ops/sec is comparable to iterateHexagons 76 | val hexs = grid.getHexagonsByOffsetRange(0, 100, 0, 100) 77 | for (hexagon in hexs) { 78 | bh.consume(hexagon) 79 | } 80 | } 81 | 82 | @Benchmark 83 | fun getHexagonsByCubeRange(bh: Blackhole) { 84 | // Gets all hexs so the ops/sec is comparable to iterateHexagons 85 | // Note % of CC are not in the grid 86 | val hexs = grid.getHexagonsByCubeRange(ccFrom, ccTo) 87 | for (hexagon in hexs) { 88 | bh.consume(hexagon) 89 | } 90 | } 91 | 92 | @Benchmark 93 | fun getByPixelCoordinate50pct(bh: Blackhole) { 94 | // ~ 50% hit ratio 95 | for ((x, y) in spiralPath) { 96 | bh.consume(grid.getByPixelCoordinate(x, y)) 97 | } 98 | } 99 | 100 | @Benchmark 101 | fun getByPixelCoordinate100pct(bh: Blackhole) { 102 | // ~ 100% hit ratio 103 | for ((x, y) in circlePath) { 104 | bh.consume(grid.getByPixelCoordinate(x, y)) 105 | } 106 | } 107 | 108 | @Benchmark 109 | fun getNeighborsOf(bh: Blackhole) { 110 | bh.consume(grid.getNeighborsOf(hexRandom)) 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGrid.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.cobalt.datatypes.Maybe 4 | import org.hexworks.mixite.core.api.contract.SatelliteData 5 | import org.hexworks.mixite.core.internal.GridData 6 | 7 | /** 8 | * 9 | * 10 | * Represents a hexagonal grid. Use [HexagonalGridBuilder] to generate a 11 | * ready-to-use grid. This interface contains all common functionality for dealing with 12 | * Hexagons. See [HexagonalGridCalculator] for more advanced features. 13 | * 14 | * 15 | * 16 | * This [HexagonalGrid] uses an cube (trapezoidal) coordinate system for easier 17 | * computation. This means that apart from the X axis a diagonal axis is used instead of 18 | * the vertical Y axis. 19 | * 20 | */ 21 | interface HexagonalGrid { 22 | 23 | /** 24 | * Returns this HexagonalGrid's GridData. 25 | * 26 | * @return grid data 27 | */ 28 | val gridData: GridData 29 | 30 | /** 31 | * Returns all [Hexagon]s contained in this grid. 32 | * 33 | * @return hexagons 34 | */ 35 | val hexagons: Iterable> 36 | 37 | /** 38 | * Returns all [Hexagon]s contained in the given cube coordinate range. 39 | * If the range contains coordinates which are not part of the grid they will be ignored. 40 | * 41 | * @param from from 42 | * @param to to 43 | * 44 | * @return [Hexagon]s in the given range. 45 | */ 46 | fun getHexagonsByCubeRange(from: CubeCoordinate, to: CubeCoordinate): Iterable> 47 | 48 | /** 49 | * Returns all [Hexagon]s contained in the given offset coordinate range. 50 | * If the range contains coordinates which are not part of the grid they will be ignored. 51 | * 52 | * @param gridXFrom from x inclusive 53 | * @param gridXTo to x inclusive 54 | * @param gridYFrom from z inclusive 55 | * @param gridYTo to z inclusive 56 | * 57 | * @return [Hexagon]s in the given range. 58 | */ 59 | fun getHexagonsByOffsetRange(gridXFrom: Int, gridXTo: Int, gridYFrom: Int, gridYTo: Int): Iterable> 60 | 61 | /** 62 | * Tells whether the given cube coordinate is on the grid or not. 63 | * If you want to look up by offset coordinate use [CoordinateConverter]. 64 | * 65 | * @param coordinate coord 66 | * 67 | * @return is it on the grid? 68 | */ 69 | fun containsCubeCoordinate(coordinate: CubeCoordinate): Boolean 70 | 71 | /** 72 | * Returns a [Hexagon] by its cube coordinate. 73 | * 74 | * @param coordinate coord 75 | * 76 | * @return Maybe with a Hexagon if it is present 77 | */ 78 | fun getByCubeCoordinate(coordinate: CubeCoordinate): Maybe> 79 | 80 | /** 81 | * Returns a [Hexagon] by a pixel coordinate. 82 | * *Please note* that all pixel coordinates are relative to 83 | * the containing [HexagonalGrid]. 84 | * 85 | * @param coordinateX pixel coordinateX coordinate 86 | * @param coordinateY pixel coordinateY coordinate 87 | * 88 | * @return Maybe with a Hexagon if it is present 89 | */ 90 | fun getByPixelCoordinate(coordinateX: Double, coordinateY: Double): Maybe> 91 | 92 | /** 93 | * Returns the coordinate of a neighbor of a Hexagon by its neighbor index. 94 | * 95 | * @return CubeCoordinate 96 | */ 97 | fun getNeighborCoordinateByIndex(coordinate: CubeCoordinate, index: Int): CubeCoordinate 98 | 99 | /** 100 | * Returns a neighbor of a Hexagon by its neighbor index. 101 | * 102 | * @return neighbor or empty Maybe if not applicable 103 | */ 104 | fun getNeighborByIndex(hexagon: Hexagon, index: Int): Maybe> 105 | 106 | /** 107 | * Returns all neighbors of a [Hexagon]. 108 | * 109 | * @param hexagon [Hexagon] 110 | * 111 | * @return the [Hexagon]'s neighbors 112 | */ 113 | fun getNeighborsOf(hexagon: Hexagon): Collection> 114 | 115 | } 116 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/internal/impl/HexagonImpl.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl 2 | 3 | import org.hexworks.cobalt.datatypes.Maybe 4 | import org.hexworks.mixite.core.api.* 5 | import org.hexworks.mixite.core.api.contract.HexagonDataStorage 6 | import org.hexworks.mixite.core.api.contract.SatelliteData 7 | import org.hexworks.mixite.core.internal.GridData 8 | import kotlin.math.PI 9 | import kotlin.math.cos 10 | import kotlin.math.sin 11 | 12 | /** 13 | * Default implementation of the [Hexagon] interface. 14 | */ 15 | class HexagonImpl internal constructor( 16 | private val sharedData: GridData, 17 | override val cubeCoordinate: CubeCoordinate, 18 | private val hexagonDataStorage: HexagonDataStorage) : Hexagon { 19 | 20 | override val vertices: List 21 | override val points: List 22 | override val externalBoundingBox: Rectangle 23 | override val internalBoundingBox: Rectangle 24 | override val center: Point = calculateCenter() 25 | 26 | override val id: String 27 | get() = cubeCoordinate.toAxialKey() 28 | 29 | override val gridX: Int 30 | get() = cubeCoordinate.gridX 31 | 32 | override val gridY: Int 33 | get() = cubeCoordinate.gridY 34 | 35 | override val gridZ: Int 36 | get() = cubeCoordinate.gridZ 37 | 38 | override val centerX = center.coordinateX 39 | 40 | override val centerY = center.coordinateY 41 | 42 | override val satelliteData: Maybe 43 | get() = hexagonDataStorage.getSatelliteDataBy(cubeCoordinate) 44 | 45 | init { 46 | this.points = calculatePoints() 47 | val x1 = points[3].coordinateX 48 | val y1 = points[2].coordinateY 49 | val x2 = points[0].coordinateX 50 | val y2 = points[5].coordinateY 51 | 52 | externalBoundingBox = Rectangle(x1, y1, x2 - x1, y2 - y1) 53 | internalBoundingBox = Rectangle((center.coordinateX - 1.25 * sharedData.radius / 2), 54 | (center.coordinateY - 1.25 * sharedData.radius / 2), 55 | (1.25f * sharedData.radius), 56 | (1.25f * sharedData.radius)) 57 | 58 | this.vertices = ArrayList(12) 59 | for (point in points) { 60 | vertices.add(point.coordinateX) 61 | vertices.add(point.coordinateY) 62 | } 63 | } 64 | 65 | private fun calculateCenter(): Point { 66 | return if (HexagonOrientation.FLAT_TOP.equals(sharedData.orientation)) { 67 | Point.fromPosition( 68 | cubeCoordinate.gridX * sharedData.hexagonWidth + sharedData.radius, 69 | cubeCoordinate.gridZ * sharedData.hexagonHeight + cubeCoordinate.gridX * sharedData.hexagonHeight / 2 + sharedData.hexagonHeight / 2 70 | ) 71 | } else { 72 | Point.fromPosition( 73 | cubeCoordinate.gridX * sharedData.hexagonWidth + cubeCoordinate.gridZ * sharedData.hexagonWidth / 2 + sharedData.hexagonWidth / 2, 74 | cubeCoordinate.gridZ * sharedData.hexagonHeight + sharedData.radius 75 | ) 76 | } 77 | } 78 | 79 | private fun calculatePoints(): List { 80 | val points = ArrayList(6) 81 | for (i in 0..5) { 82 | val angle = 2 * PI / 6 * (i + sharedData.orientation.coordinateOffset) 83 | val x = center.coordinateX + sharedData.radius * cos(angle) 84 | val y = center.coordinateY + sharedData.radius * sin(angle) 85 | points.add(Point.fromPosition(x, y)) 86 | } 87 | return points 88 | } 89 | 90 | override fun setSatelliteData(data: T) { 91 | this.hexagonDataStorage.addCoordinate(cubeCoordinate, data) 92 | } 93 | 94 | override fun clearSatelliteData() { 95 | this.hexagonDataStorage.clearDataFor(cubeCoordinate) 96 | } 97 | 98 | override fun equals(other: Any?): Boolean { 99 | if (this === other) return true 100 | if (other == null || this::class != other::class) return false 101 | 102 | other as HexagonImpl<*> 103 | 104 | if (cubeCoordinate != other.cubeCoordinate) return false 105 | 106 | return true 107 | } 108 | 109 | override fun hashCode(): Int { 110 | return cubeCoordinate.hashCode() 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/api/defaults/DefaultHexagonDataStorageTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api.defaults 2 | 3 | import org.hexworks.mixite.core.api.CubeCoordinate 4 | import kotlin.test.* 5 | 6 | class DefaultHexagonDataStorageTest { 7 | 8 | lateinit var target: DefaultHexagonDataStorage 9 | lateinit var defaultSatelliteDataStub: DefaultSatelliteData 10 | 11 | @BeforeTest 12 | fun setUp() { 13 | target = DefaultHexagonDataStorage() 14 | defaultSatelliteDataStub = DefaultSatelliteData() 15 | } 16 | 17 | @Test 18 | fun shouldAddCoordinateWhenAddCoordinateIsCalledWithCubeCoordinate() { 19 | target.addCoordinate(TEST_CUBE_COORDINATE) 20 | assertTrue { 21 | target.containsCoordinate(TEST_CUBE_COORDINATE) 22 | } 23 | } 24 | 25 | @Test 26 | fun shouldAddCoordinateAndDataWhenCalledWithBoth() { 27 | target.addCoordinate(TEST_CUBE_COORDINATE, defaultSatelliteDataStub) 28 | assertTrue { 29 | target.containsCoordinate(TEST_CUBE_COORDINATE) 30 | } 31 | assertTrue { 32 | target.hasDataFor(TEST_CUBE_COORDINATE) 33 | } 34 | assertEquals(expected = defaultSatelliteDataStub, actual = target.getSatelliteDataBy(TEST_CUBE_COORDINATE).get()) 35 | } 36 | 37 | @Test 38 | fun shouldProperlySignalModificationWhenReplacesPreviousValue() { 39 | target.addCoordinate(TEST_CUBE_COORDINATE) 40 | 41 | assertTrue { 42 | target.addCoordinate(TEST_CUBE_COORDINATE, defaultSatelliteDataStub) 43 | } 44 | } 45 | 46 | @Test 47 | fun shouldProperlyReturnEmptyOptionalWhenTryingToGetNonPresentData() { 48 | target.addCoordinate(TEST_CUBE_COORDINATE) 49 | val result = target.getSatelliteDataBy(TEST_CUBE_COORDINATE) 50 | 51 | assertFalse { 52 | result.isPresent 53 | } 54 | } 55 | 56 | @Test 57 | fun shouldProperlyReturnEmptyOptionalWhenTryingToGetDataWithNonPresentKey() { 58 | val result = target.getSatelliteDataBy(TEST_CUBE_COORDINATE) 59 | 60 | assertFalse { 61 | result.isPresent 62 | } 63 | } 64 | 65 | @Test 66 | fun shouldProperlyReturnFalseWhenContainsCoordinateIsCalledWithNonPresentCoordinate() { 67 | assertFalse { 68 | target.containsCoordinate(TEST_CUBE_COORDINATE) 69 | } 70 | } 71 | 72 | @Test 73 | fun shouldProperlyReturnTrueWhenContainsCoordinateIsCalledWithPresentCoordinate() { 74 | target.addCoordinate(TEST_CUBE_COORDINATE, defaultSatelliteDataStub) 75 | 76 | assertTrue { 77 | target.containsCoordinate(TEST_CUBE_COORDINATE) 78 | } 79 | } 80 | 81 | @Test 82 | fun shouldProperlyReturnFalseWhenHasDataForIsCalledAndThereIsNoData() { 83 | assertFalse { 84 | target.hasDataFor(TEST_CUBE_COORDINATE) 85 | } 86 | } 87 | 88 | @Test 89 | fun shouldProperlyReturnTrueWhenHasDataForIsCalledAndThereIsData() { 90 | target.addCoordinate(TEST_CUBE_COORDINATE, defaultSatelliteDataStub) 91 | 92 | assertTrue { 93 | target.hasDataFor(TEST_CUBE_COORDINATE) 94 | } 95 | } 96 | 97 | @Test 98 | fun shouldProperlyReturnCoordinatesWhenThereAreNonePresent() { 99 | assertFalse { 100 | target.coordinates.iterator().hasNext() 101 | } 102 | } 103 | 104 | @Test 105 | fun shouldProperlyReturnCoordinatesWhenThereAreSeveralPresent() { 106 | target.addCoordinate(TEST_CUBE_COORDINATE, defaultSatelliteDataStub) 107 | target.addCoordinate(TEST_CUBE_COORDINATE, defaultSatelliteDataStub) 108 | target.addCoordinate(ANOTHER_TEST_CUBE_COORDINATE, defaultSatelliteDataStub) 109 | 110 | val iter = target.coordinates.iterator() 111 | var count = 0 112 | while (iter.hasNext()) { 113 | iter.next() 114 | count++ 115 | } 116 | assertEquals(expected = 2, actual = count) 117 | } 118 | 119 | @Test 120 | fun shouldProperlyClearDataForWhenCalledAndDataIsPresent() { 121 | target.addCoordinate(TEST_CUBE_COORDINATE, defaultSatelliteDataStub) 122 | 123 | assertTrue { 124 | target.hasDataFor(TEST_CUBE_COORDINATE) 125 | } 126 | 127 | target.clearDataFor(TEST_CUBE_COORDINATE) 128 | 129 | assertFalse { 130 | target.hasDataFor(TEST_CUBE_COORDINATE) 131 | } 132 | } 133 | 134 | companion object { 135 | 136 | private val TEST_CUBE_COORDINATE = CubeCoordinate.fromCoordinates(1, 1) 137 | private val ANOTHER_TEST_CUBE_COORDINATE = CubeCoordinate.fromCoordinates(2, 1) 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/api/CoordinateConverterTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromAxialKey 4 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromCoordinates 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertFailsWith 8 | 9 | class CoordinateConverterTest { 10 | 11 | @Test 12 | fun shouldThrowExceptionWhenInstantiated() { 13 | assertFailsWith { 14 | CoordinateConverter() 15 | } 16 | } 17 | 18 | @Test 19 | fun shouldConvertOffsetCoordinatesToAxialXWithPointy() { 20 | val result = CoordinateConverter.convertOffsetCoordinatesToCubeX(TEST_X, TEST_Y, HexagonOrientation.POINTY_TOP) 21 | assertEquals(EXPECTED_AXIAL_X_WITH_POINTY, result) 22 | } 23 | 24 | @Test 25 | fun shouldConvertOffsetCoordinatesToAxialXWithFlat() { 26 | val result = CoordinateConverter.convertOffsetCoordinatesToCubeX(TEST_X, TEST_Y, HexagonOrientation.FLAT_TOP) 27 | assertEquals(EXPECTED_AXIAL_X_WITH_FLAT, result) 28 | } 29 | 30 | @Test 31 | fun shouldConvertOffsetCoordinatesToAxialZWithPointy() { 32 | val result = CoordinateConverter.convertOffsetCoordinatesToCubeZ(TEST_X, TEST_Y, HexagonOrientation.POINTY_TOP) 33 | assertEquals(EXPECTED_AXIAL_Z_WITH_POINTY, result) 34 | } 35 | 36 | @Test 37 | fun shouldConvertOffsetCoordinatesToAxialZWithFlat() { 38 | val result = CoordinateConverter.convertOffsetCoordinatesToCubeZ(TEST_X, TEST_Y, HexagonOrientation.FLAT_TOP) 39 | assertEquals(EXPECTED_AXIAL_Z_WITH_FLAT, result) 40 | } 41 | 42 | @Test 43 | fun shouldConvertCubeCoordinatesToOffsetRowWithFlat() { 44 | for((i, cube) in testCubes.withIndex()) { 45 | val result = CoordinateConverter.convertCubeCoordinateToOffsetRow(cube, HexagonOrientation.FLAT_TOP) 46 | assertEquals(expectedOffsetRowWithFlat[i], result, "(" + cube.gridX + "," + cube.gridZ + ") to " + result) 47 | } 48 | } 49 | 50 | @Test 51 | fun shouldConvertCubeCoordinatesToOffsetColWithFlat() { 52 | for((i, cube) in testCubes.withIndex()) { 53 | val result = CoordinateConverter.convertCubeCoordinateToOffsetColumn(cube, HexagonOrientation.FLAT_TOP) 54 | assertEquals(expectedOffsetColWithFlat[i], result, "(" + cube.gridX + "," + cube.gridZ + ") to " + result) 55 | } 56 | } 57 | 58 | @Test 59 | fun shouldConvertCubeCoordinatesToOffsetRowWithPointy() { 60 | for((i, cube) in testCubes.withIndex()) { 61 | val result = CoordinateConverter.convertCubeCoordinateToOffsetRow(cube, HexagonOrientation.POINTY_TOP) 62 | assertEquals(expectedOffsetRowWithPointy[i], result, "(" + cube.gridX + "," + cube.gridZ + ") to " + result) 63 | } 64 | } 65 | 66 | @Test 67 | fun shouldConvertCubeCoordinatesToOffsetColWithPointy() { 68 | for((i, cube) in testCubes.withIndex()) { 69 | val result = CoordinateConverter.convertCubeCoordinateToOffsetColumn(cube, HexagonOrientation.POINTY_TOP) 70 | assertEquals(expectedOffsetColWithPointy[i], result, "(" + cube.gridX + "," + cube.gridZ + ") to " + result) 71 | } 72 | } 73 | 74 | @Test 75 | fun shouldCreateKeyFromCoordinate() { 76 | assertEquals(TEST_KEY, fromCoordinates(TEST_GRID_X, TEST_GRID_Z).toAxialKey()) 77 | } 78 | 79 | @Test 80 | fun shouldCreateCoordinateFromKey() { 81 | val c = fromAxialKey(TEST_KEY) 82 | assertEquals(TEST_GRID_X, c.gridX) 83 | assertEquals(TEST_GRID_Z, c.gridZ) 84 | } 85 | 86 | companion object { 87 | 88 | private const val TEST_X = 3 89 | private const val TEST_Y = 4 90 | private const val TEST_KEY = "7,8" 91 | private const val TEST_GRID_X = 7 92 | private const val TEST_GRID_Z = 8 93 | 94 | private const val EXPECTED_AXIAL_X_WITH_POINTY = 1 95 | private const val EXPECTED_AXIAL_X_WITH_FLAT = 3 96 | private const val EXPECTED_AXIAL_Z_WITH_POINTY = 4 97 | private const val EXPECTED_AXIAL_Z_WITH_FLAT = 3 98 | 99 | private val testCubes = arrayOf( 100 | CubeCoordinate.fromCoordinates(-1, -2), 101 | CubeCoordinate.fromCoordinates( 2, -3), 102 | CubeCoordinate.fromCoordinates( 7, 8) 103 | ) 104 | 105 | private val expectedOffsetColWithFlat = intArrayOf(-1, 2, 7) 106 | private val expectedOffsetRowWithFlat = intArrayOf(-3,-2,11) 107 | private val expectedOffsetColWithPointy = intArrayOf(-2, 0,11) 108 | private val expectedOffsetRowWithPointy = intArrayOf(-2,-3, 8) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/internal/impl/HexagonalGridCalculatorImpl.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl 2 | 3 | import org.hexworks.cobalt.datatypes.Maybe 4 | import org.hexworks.mixite.core.api.* 5 | import org.hexworks.mixite.core.api.contract.SatelliteData 6 | import kotlin.math.abs 7 | import kotlin.math.max 8 | import kotlin.math.min 9 | import kotlin.math.round 10 | 11 | class HexagonalGridCalculatorImpl(override val hexagonalGrid: HexagonalGrid) : HexagonalGridCalculator { 12 | 13 | override fun calculateDistanceBetween(hex0: Hexagon, hex1: Hexagon): Int { 14 | val absX = abs(hex0.gridX - hex1.gridX) 15 | val absY = abs(hex0.gridY - hex1.gridY) 16 | val absZ = abs(hex0.gridZ - hex1.gridZ) 17 | return max(max(absX, absY), absZ) 18 | } 19 | 20 | override fun calculateMovementRangeFrom(hexagon: Hexagon, distance: Int): Set> { 21 | val ret = HashSet>() 22 | for (x in -distance..distance) { 23 | for (y in max(-distance, -x - distance)..min(distance, -x + distance)) { 24 | val z = -x - y 25 | val tempCoordinate = CubeCoordinate.fromCoordinates(hexagon.gridX + x, hexagon.gridZ + z) 26 | hexagonalGrid.getByCubeCoordinate(tempCoordinate).ifPresent { ret += it } 27 | } 28 | } 29 | return ret 30 | } 31 | 32 | override fun rotateHexagon(originalHex: Hexagon, targetHex: Hexagon, rotationDirection: RotationDirection): Maybe> { 33 | val diffX = targetHex.gridX - originalHex.gridX 34 | val diffZ = targetHex.gridZ - originalHex.gridZ 35 | val diffCoord = CubeCoordinate.fromCoordinates(diffX, diffZ) 36 | val rotatedCoord = rotationDirection.calculateRotation(diffCoord) 37 | val resultCoord = CubeCoordinate.fromCoordinates( 38 | originalHex.gridX + rotatedCoord.gridX, 39 | originalHex.gridZ + rotatedCoord.gridZ) // 0, x, 40 | return hexagonalGrid.getByCubeCoordinate(resultCoord) 41 | } 42 | 43 | override fun calculateRingFrom(centerHexagon: Hexagon, radius: Int): Set> { 44 | val result = HashSet>() 45 | 46 | var currentCoordinate = CubeCoordinate.fromCoordinates( 47 | centerHexagon.gridX - radius, 48 | centerHexagon.gridZ + radius 49 | ) 50 | 51 | for (i in 0 until 6) { 52 | for (j in 0 until radius) { 53 | currentCoordinate = hexagonalGrid.getNeighborCoordinateByIndex(currentCoordinate, i) 54 | val hexagon = hexagonalGrid.getByCubeCoordinate(currentCoordinate) 55 | if (hexagon.isPresent) { 56 | result.add(hexagon.get()) 57 | } 58 | } 59 | } 60 | 61 | return result 62 | } 63 | 64 | override fun drawLine(from: Hexagon, to: Hexagon): List> { 65 | val distance = calculateDistanceBetween(from, to) 66 | if (distance == 0) { 67 | return emptyList() 68 | } 69 | val results = ArrayList>(distance+1) 70 | for (i in 0..distance) { 71 | val interpolatedCoordinate = cubeLinearInterpolate(from.cubeCoordinate, 72 | to.cubeCoordinate, 1.0 / distance * i) 73 | results.add(hexagonalGrid.getByCubeCoordinate(interpolatedCoordinate).get()) 74 | } 75 | return results 76 | } 77 | 78 | override fun isVisible(from: Hexagon, to: Hexagon): Boolean { 79 | val traversePath = drawLine(from, to) 80 | for (pathHexagon in traversePath) { 81 | if (pathHexagon.equals(from) || pathHexagon.equals(to)) { 82 | continue 83 | } 84 | if (pathHexagon.satelliteData.isPresent && pathHexagon.satelliteData.get().isOpaque) { 85 | return false 86 | } 87 | } 88 | return true 89 | } 90 | 91 | private fun cubeLinearInterpolate(from: CubeCoordinate, to: CubeCoordinate, sample: Double): CubeCoordinate { 92 | return roundToCubeCoordinate(linearInterpolate(from.gridX, to.gridX, sample), 93 | linearInterpolate(from.gridY, to.gridY, sample), 94 | linearInterpolate(from.gridZ, to.gridZ, sample)) 95 | } 96 | 97 | private fun linearInterpolate(from: Int, to: Int, sample: Double): Double { 98 | return from + (to - from) * sample 99 | } 100 | 101 | private fun roundToCubeCoordinate(gridX: Double, gridY: Double, gridZ: Double): CubeCoordinate { 102 | var rx = round(gridX).toInt() 103 | val ry = round(gridY).toInt() 104 | var rz = round(gridZ).toInt() 105 | 106 | val differenceX = abs(rx - gridX) 107 | val differenceY = abs(ry - gridY) 108 | val differenceZ = abs(rz - gridZ) 109 | 110 | if (differenceX > differenceY && differenceX > differenceZ) { 111 | rx = -ry - rz 112 | } else if (differenceY <= differenceZ) { 113 | rz = -rx - ry 114 | } 115 | return CubeCoordinate.fromCoordinates(rx, rz) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/layoutstrategy/RectangularGridLayoutStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl.layoutstrategy 2 | 3 | import org.hexworks.mixite.core.GridLayoutStrategyTestUtil 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromCoordinates 6 | import org.hexworks.mixite.core.api.HexagonOrientation.FLAT_TOP 7 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 8 | import org.hexworks.mixite.core.api.contract.SatelliteData 9 | import kotlin.test.BeforeTest 10 | import kotlin.test.Test 11 | import kotlin.test.assertFalse 12 | import kotlin.test.assertTrue 13 | 14 | class RectangularGridLayoutStrategyTest { 15 | 16 | private lateinit var target: RectangularGridLayoutStrategy 17 | private lateinit var builder: HexagonalGridBuilder 18 | 19 | @BeforeTest 20 | fun setUp() { 21 | builder = GridLayoutStrategyTestUtil.fetchDefaultBuilder() 22 | target = RectangularGridLayoutStrategy() 23 | } 24 | 25 | @Test 26 | fun shouldProperlyCreateHexagonsWithPointyOrientationWhenCreateHexagonsIsCalled() { 27 | 28 | val coordIter = target.fetchGridCoordinates(builder).iterator() 29 | val coords = ArrayList() 30 | while (coordIter.hasNext()) { 31 | coords.add(coordIter.next()) 32 | } 33 | 34 | assertTrue(coords.contains(fromCoordinates(0, 0))) 35 | assertTrue(coords.contains(fromCoordinates(1, 0))) 36 | assertTrue(coords.contains(fromCoordinates(2, 0))) 37 | assertTrue(coords.contains(fromCoordinates(0, 1))) 38 | assertTrue(coords.contains(fromCoordinates(1, 1))) 39 | assertTrue(coords.contains(fromCoordinates(2, 1))) 40 | assertTrue(coords.contains(fromCoordinates(-1, 2))) 41 | assertTrue(coords.contains(fromCoordinates(0, 2))) 42 | assertTrue(coords.contains(fromCoordinates(1, 2))) 43 | 44 | assertTrue(!coords.contains(fromCoordinates(-1, 0))) 45 | assertTrue(!coords.contains(fromCoordinates(0, -1))) 46 | assertTrue(!coords.contains(fromCoordinates(1, -1))) 47 | assertTrue(!coords.contains(fromCoordinates(2, -1))) 48 | assertTrue(!coords.contains(fromCoordinates(3, -1))) 49 | assertTrue(!coords.contains(fromCoordinates(3, 0))) 50 | assertTrue(!coords.contains(fromCoordinates(3, 1))) 51 | assertTrue(!coords.contains(fromCoordinates(2, 2))) 52 | assertTrue(!coords.contains(fromCoordinates(1, 3))) 53 | assertTrue(!coords.contains(fromCoordinates(0, 3))) 54 | assertTrue(!coords.contains(fromCoordinates(-1, 3))) 55 | assertTrue(!coords.contains(fromCoordinates(-2, 3))) 56 | assertTrue(!coords.contains(fromCoordinates(-2, 2))) 57 | assertTrue(!coords.contains(fromCoordinates(-1, 1))) 58 | } 59 | 60 | @Test 61 | fun shouldProperlyCreateHexagonsWithFlatOrientationWhenCreateHexagonsIsCalled() { 62 | builder.setOrientation(FLAT_TOP) 63 | val coordIter = target.fetchGridCoordinates(builder).iterator() 64 | 65 | val coords = ArrayList() 66 | while (coordIter.hasNext()) { 67 | coords.add(coordIter.next()) 68 | } 69 | 70 | assertTrue(coords.contains(fromCoordinates(0, 0))) 71 | assertTrue(coords.contains(fromCoordinates(1, 0))) 72 | assertTrue(coords.contains(fromCoordinates(2, -1))) 73 | assertTrue(coords.contains(fromCoordinates(0, 1))) 74 | assertTrue(coords.contains(fromCoordinates(1, 1))) 75 | assertTrue(coords.contains(fromCoordinates(2, 0))) 76 | assertTrue(coords.contains(fromCoordinates(2, 1))) 77 | assertTrue(coords.contains(fromCoordinates(0, 2))) 78 | assertTrue(coords.contains(fromCoordinates(1, 2))) 79 | 80 | assertTrue(!coords.contains(fromCoordinates(-1, 0))) 81 | assertTrue(!coords.contains(fromCoordinates(0, -1))) 82 | assertTrue(!coords.contains(fromCoordinates(1, -1))) 83 | assertTrue(!coords.contains(fromCoordinates(2, -2))) 84 | assertTrue(!coords.contains(fromCoordinates(3, -1))) 85 | assertTrue(!coords.contains(fromCoordinates(3, 0))) 86 | assertTrue(!coords.contains(fromCoordinates(3, 1))) 87 | assertTrue(!coords.contains(fromCoordinates(2, 2))) 88 | assertTrue(!coords.contains(fromCoordinates(1, 3))) 89 | assertTrue(!coords.contains(fromCoordinates(0, 3))) 90 | assertTrue(!coords.contains(fromCoordinates(-1, 3))) 91 | assertTrue(!coords.contains(fromCoordinates(-2, 3))) 92 | assertTrue(!coords.contains(fromCoordinates(-2, 2))) 93 | assertTrue(!coords.contains(fromCoordinates(-1, 1))) 94 | } 95 | 96 | @Test 97 | fun testCheckParameters0() { 98 | val result = target.checkParameters(1, 1) // super: true, derived: true 99 | assertTrue(result) 100 | } 101 | 102 | @Test 103 | fun testCheckParameters1() { 104 | val result = target.checkParameters(0, 0) // super: false, derived: false; 105 | assertFalse(result) 106 | } 107 | 108 | @Test 109 | fun testCheckParameters2() { 110 | val result = target.checkParameters(-1, -1) // super: false, derived: true; 111 | assertFalse(result) 112 | } 113 | 114 | @Test 115 | fun testCheckParameters3() { 116 | val result = target.checkParameters(1, 2) // super: true, derived: true 117 | assertTrue(result) 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/HexagonImplTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl 2 | 3 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromCoordinates 4 | import org.hexworks.mixite.core.api.Hexagon 5 | import org.hexworks.mixite.core.api.HexagonOrientation.FLAT_TOP 6 | import org.hexworks.mixite.core.api.HexagonOrientation.POINTY_TOP 7 | import org.hexworks.mixite.core.api.HexagonalGridLayout.RECTANGULAR 8 | import org.hexworks.mixite.core.api.Point.Companion.fromPosition 9 | import org.hexworks.mixite.core.api.defaults.DefaultHexagonDataStorage 10 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 11 | import org.hexworks.mixite.core.internal.GridData 12 | import kotlin.math.round 13 | import kotlin.test.BeforeTest 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | import kotlin.test.assertFalse 17 | 18 | class HexagonImplTest { 19 | 20 | private lateinit var target: Hexagon 21 | 22 | @BeforeTest 23 | fun setUp() { 24 | target = HexagonImpl(TEST_POINTY_DATA, TEST_COORDINATE, TEST_SATELLITE_DATA_MAP) 25 | } 26 | 27 | @Test 28 | fun shouldHaveProperPointsWhenPointy() { 29 | val points = ArrayList(target.points) 30 | for (i in 0..5) { 31 | assertEquals(EXPECTED_POINTY_POINTS[i].coordinateX.toInt(), round(points[i].coordinateX).toInt()) 32 | assertEquals(EXPECTED_POINTY_POINTS[i].coordinateY.toInt(), round(points[i].coordinateY).toInt()) 33 | } 34 | } 35 | 36 | @Test 37 | fun shouldHaveProperPointsWhenFlat() { 38 | target = HexagonImpl(TEST_FLAT_DATA, TEST_COORDINATE, TEST_SATELLITE_DATA_MAP) 39 | val points = ArrayList(target.points) 40 | for (i in 0..5) { 41 | assertEquals(EXPECTED_FLAT_POINTS[i].coordinateX.toInt(), round(points[i].coordinateX).toInt()) 42 | assertEquals(EXPECTED_FLAT_POINTS[i].coordinateY.toInt(), round(points[i].coordinateY).toInt()) 43 | } 44 | } 45 | 46 | @Test 47 | fun shouldReturnProperSatelliteDataWhenSatelliteDataIsSet() { 48 | target.setSatelliteData(TEST_SATELLITE_DATA) 49 | assertEquals(TEST_SATELLITE_DATA, target.satelliteData.get()) 50 | } 51 | 52 | @Test 53 | fun shouldReturnProperSatelliteDataWhenSatelliteDataIsCleared() { 54 | target.setSatelliteData(TEST_SATELLITE_DATA) 55 | assertEquals(TEST_SATELLITE_DATA, target.satelliteData.get()) 56 | target.clearSatelliteData() 57 | assertFalse(target.satelliteData.isPresent) 58 | } 59 | 60 | @Test 61 | fun shouldReturnProperXCoordinateWhenGetGridXIsCalled() { 62 | assertEquals(TEST_GRID_X, target.gridX) 63 | } 64 | 65 | @Test 66 | fun shouldReturnProperXCoordinateWhenGetGridYIsCalled() { 67 | assertEquals(TEST_GRID_Y, target.gridY) 68 | } 69 | 70 | @Test 71 | fun shouldReturnProperXCoordinateWhenGetGridZIsCalled() { 72 | assertEquals(TEST_GRID_Z, target.gridZ) 73 | } 74 | 75 | @Test 76 | fun shouldReturnProperCenterXCoordinateWhenGetCenterXIsCalledWithPointyHexagons() { 77 | assertEquals(EXPECTED_POINTY_CENTER_X, round(target.center.coordinateX).toInt()) 78 | } 79 | 80 | @Test 81 | fun shouldReturnProperCenterXCoordinateWhenGetCenterXIsCalledWithFlatHexagons() { 82 | target = HexagonImpl(TEST_FLAT_DATA, TEST_COORDINATE, TEST_SATELLITE_DATA_MAP) 83 | assertEquals(EXPECTED_FLAT_CENTER_X, round(target.center.coordinateX).toInt()) 84 | } 85 | 86 | @Test 87 | fun shouldReturnProperCenterYCoordinateWhenGetCenterYIsCalledWithPointyHexagons() { 88 | assertEquals(EXPECTED_POINTY_CENTER_Y, round(target.center.coordinateY).toInt()) 89 | } 90 | 91 | @Test 92 | fun shouldReturnProperCenterYCoordinateWhenGetCenterYIsCalledWithFlatHexagons() { 93 | target = HexagonImpl(TEST_FLAT_DATA, TEST_COORDINATE, TEST_SATELLITE_DATA_MAP) 94 | assertEquals(EXPECTED_FLAT_CENTER_Y, round(target.center.coordinateY).toInt()) 95 | } 96 | 97 | @Test 98 | fun shouldBeEqualToItself() { 99 | assertEquals(target, target) 100 | } 101 | 102 | @Test 103 | fun shouldProperlyGetIdWhenGetIdIsCalled() { 104 | assertEquals(TEST_COORDINATE.toAxialKey(), target.id) 105 | } 106 | 107 | companion object { 108 | private const val TEST_RADIUS = 10.0 109 | private const val TEST_GRID_X = 2 110 | private const val TEST_GRID_Z = 3 111 | private const val TEST_GRID_Y = -5 112 | private const val EXPECTED_POINTY_CENTER_X = 69 113 | private const val EXPECTED_FLAT_CENTER_X = 40 114 | private const val EXPECTED_POINTY_CENTER_Y = 55 115 | private const val EXPECTED_FLAT_CENTER_Y = 78 116 | private val TEST_POINTY_DATA = GridData(POINTY_TOP, RECTANGULAR.gridLayoutStrategy, TEST_RADIUS, 1, 1) 117 | private val TEST_FLAT_DATA = GridData(FLAT_TOP, RECTANGULAR.gridLayoutStrategy, TEST_RADIUS, 1, 1) 118 | private val TEST_COORDINATE = fromCoordinates(TEST_GRID_X, TEST_GRID_Z) 119 | private val TEST_SATELLITE_DATA = DefaultSatelliteData() 120 | private val TEST_SATELLITE_DATA_MAP = DefaultHexagonDataStorage() 121 | private val EXPECTED_FLAT_POINTS = arrayOf(fromPosition(50.0, 78.0), fromPosition(45.0, 87.0), fromPosition(35.0, 87.0), fromPosition(30.0, 78.0), fromPosition(35.0, 69.0), fromPosition(45.0, 69.0)) 122 | private val EXPECTED_POINTY_POINTS = arrayOf(fromPosition(78.0, 60.0), fromPosition(69.0, 65.0), fromPosition(61.0, 60.0), fromPosition(61.0, 50.0), fromPosition(69.0, 45.0), fromPosition(78.0, 50.0)) 123 | 124 | init { 125 | TEST_SATELLITE_DATA_MAP.addCoordinate(TEST_COORDINATE, TEST_SATELLITE_DATA) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGridBuilder.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.api 2 | 3 | import org.hexworks.mixite.core.api.HexagonOrientation.POINTY_TOP 4 | import org.hexworks.mixite.core.api.contract.HexagonDataStorage 5 | import org.hexworks.mixite.core.api.contract.SatelliteData 6 | import org.hexworks.mixite.core.api.defaults.DefaultHexagonDataStorage 7 | import org.hexworks.mixite.core.internal.GridData 8 | import org.hexworks.mixite.core.internal.impl.HexagonalGridCalculatorImpl 9 | import org.hexworks.mixite.core.internal.impl.HexagonalGridImpl 10 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.GridLayoutStrategy 11 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.RectangularGridLayoutStrategy 12 | 13 | /** 14 | * 15 | * Builder for a [HexagonalGrid]. 16 | * Can be used to build a [HexagonalGrid]. 17 | * Mandatory parameters are: 18 | * 19 | * * width of the grid 20 | * * height of the grid 21 | * * radius of a [Hexagon] 22 | * 23 | * Defaults for orientation and grid layout are POINTY_TOP and RECTANGULAR. 24 | */ 25 | class HexagonalGridBuilder { 26 | private var gridWidth: Int = 0 27 | private var gridHeight: Int = 0 28 | private var radius: Double = 0.toDouble() 29 | private val hexagonDataStorage: HexagonDataStorage = DefaultHexagonDataStorage() 30 | private var orientation = POINTY_TOP 31 | private var gridLayout : GridLayoutStrategy = RectangularGridLayoutStrategy() 32 | 33 | val gridLayoutStrategy: GridLayoutStrategy 34 | get() = gridLayout 35 | 36 | /** 37 | * Returns the GridData. 38 | * 39 | * @return grid data 40 | */ 41 | val gridData: GridData 42 | get() { 43 | if (radius == 0.0 || gridWidth == 0 || gridHeight == 0) { 44 | throw IllegalStateException("Not all necessary fields are initialized!") 45 | } 46 | return GridData(orientation, gridLayout, radius, gridWidth, gridHeight) 47 | } 48 | 49 | /** 50 | * Builds a [HexagonalGrid] using the parameters supplied. 51 | * Throws [HexagonalGridCreationException] if not all mandatory parameters 52 | * are filled and/or they are not valid. In both cases you will be supplied with 53 | * a [HexagonalGridCreationException] detailing the cause of failure. 54 | * 55 | * @return [HexagonalGrid] 56 | */ 57 | fun build(): HexagonalGrid { 58 | checkParameters() 59 | return HexagonalGridImpl(this) 60 | } 61 | 62 | /** 63 | * Creates a [HexagonalGridCalculator] for your [HexagonalGrid]. 64 | * 65 | * @param hexagonalGrid grid 66 | * 67 | * @return calculator 68 | */ 69 | fun buildCalculatorFor(hexagonalGrid: HexagonalGrid): HexagonalGridCalculator { 70 | return HexagonalGridCalculatorImpl(hexagonalGrid) 71 | } 72 | 73 | fun getRadius(): Double { 74 | return radius 75 | } 76 | 77 | /** 78 | * Sets the radius of the [Hexagon]s contained in the resulting [HexagonalGrid]. 79 | * 80 | * @param radius in pixels 81 | * 82 | * @return this [HexagonalGridBuilder] 83 | */ 84 | fun setRadius(radius: Double): HexagonalGridBuilder = also { 85 | this.radius = radius 86 | } 87 | 88 | fun getGridWidth(): Int { 89 | return gridWidth 90 | } 91 | 92 | /** 93 | * Mandatory parameter. Sets the number of [Hexagon]s in the horizontal direction. 94 | * 95 | * @param gridWidth grid width 96 | * 97 | * @return this [HexagonalGridBuilder] 98 | */ 99 | fun setGridWidth(gridWidth: Int): HexagonalGridBuilder = also { 100 | this.gridWidth = gridWidth 101 | } 102 | 103 | fun getGridHeight(): Int { 104 | return gridHeight 105 | } 106 | 107 | /** 108 | * Mandatory parameter. Sets the number of [Hexagon]s in the vertical direction. 109 | * 110 | * @param gridHeight grid height 111 | * 112 | * @return this [HexagonalGridBuilder] 113 | */ 114 | fun setGridHeight(gridHeight: Int): HexagonalGridBuilder = also { 115 | this.gridHeight = gridHeight 116 | } 117 | 118 | fun getOrientation(): HexagonOrientation { 119 | return orientation 120 | } 121 | 122 | /** 123 | * Sets the [HexagonOrientation] used in the resulting [HexagonalGrid]. 124 | * If it is not set HexagonOrientation.POINTY will be used. 125 | * 126 | * @param orientation orientation 127 | * 128 | * @return this [HexagonalGridBuilder] 129 | */ 130 | fun setOrientation(orientation: HexagonOrientation): HexagonalGridBuilder = also { 131 | this.orientation = orientation 132 | } 133 | 134 | fun getHexagonDataStorage(): HexagonDataStorage { 135 | return hexagonDataStorage 136 | } 137 | 138 | /** 139 | * Sets the [GridLayoutStrategy] which will be used when creating the [HexagonalGrid]. 140 | * If it is not set
RECTANGULAR
will be assumed. 141 | * 142 | * @param gridLayout layout 143 | * 144 | * @return this [HexagonalGridBuilder]. 145 | */ 146 | fun setGridLayout(gridLayout: GridLayoutStrategy): HexagonalGridBuilder = also { 147 | this.gridLayout = gridLayout 148 | } 149 | 150 | /** 151 | * Sets the [GridLayoutStrategy] which will be used when creating the [HexagonalGrid], based on an existing 152 | * [HexagonalGridLayout]. If it is not set
RECTANGULAR
will be assumed. 153 | * 154 | * @param gridLayout layout 155 | * 156 | * @return this [HexagonalGridBuilder]. 157 | */ 158 | fun setGridLayout(gridLayout: HexagonalGridLayout): HexagonalGridBuilder = also { 159 | this.gridLayout = gridLayout.gridLayoutStrategy 160 | } 161 | 162 | private fun checkParameters() { 163 | if (radius <= 0) { 164 | throw IllegalStateException("Radius must be greater than 0.") 165 | } 166 | if (!gridLayout.checkParameters(gridHeight, gridWidth)) { 167 | throw IllegalStateException("Width: " + gridWidth + " and height: " + gridHeight + " is not valid for: " + gridLayout.getName() + " layout.") 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/HexagonalGridImplTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl 2 | 3 | import org.hexworks.mixite.core.api.* 4 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromCoordinates 5 | import org.hexworks.mixite.core.api.HexagonalGridLayout.RECTANGULAR 6 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 7 | import org.hexworks.mixite.core.internal.impl.layoutstrategy.RectangularGridLayoutStrategy 8 | import kotlin.test.* 9 | 10 | abstract class HexagonalGridImplTest { 11 | 12 | internal lateinit var target: HexagonalGrid 13 | private lateinit var builder: HexagonalGridBuilder 14 | 15 | abstract fun getBuilder() : HexagonalGridBuilder 16 | 17 | @BeforeTest 18 | fun setUp() { 19 | builder = getBuilder() 20 | target = builder.build() 21 | } 22 | 23 | @Test 24 | fun shouldReturnHexagonsInProperIterationOrderWhenGetHexagonsIsCalled() { 25 | val expectedCoordinates = ArrayList() 26 | val actualCoordinates = ArrayList() 27 | 28 | for (cubeCoordinate in builder.gridLayoutStrategy.fetchGridCoordinates(builder)) { 29 | expectedCoordinates.add(cubeCoordinate) 30 | } 31 | for (hexagon in target.hexagons) { 32 | actualCoordinates.add(hexagon.cubeCoordinate) 33 | } 34 | 35 | assertEquals(expectedCoordinates, actualCoordinates) 36 | } 37 | 38 | @Test 39 | abstract fun shouldReturnProperHexagonsWhenEachHexagonIsTestedInAGivenGrid() 40 | 41 | @Test 42 | fun shouldReturnProperHexagonsWhenGetHexagonsByAxialRangeIsCalled() { 43 | val expected = HashSet>() 44 | 45 | expected.add(target.getByCubeCoordinate(fromCoordinates(2, 3)).get()) 46 | expected.add(target.getByCubeCoordinate(fromCoordinates(3, 3)).get()) 47 | expected.add(target.getByCubeCoordinate(fromCoordinates(4, 3)).get()) 48 | 49 | expected.add(target.getByCubeCoordinate(fromCoordinates(2, 4)).get()) 50 | expected.add(target.getByCubeCoordinate(fromCoordinates(3, 4)).get()) 51 | expected.add(target.getByCubeCoordinate(fromCoordinates(4, 4)).get()) 52 | 53 | expected.add(target.getByCubeCoordinate(fromCoordinates(2, 5)).get()) 54 | expected.add(target.getByCubeCoordinate(fromCoordinates(3, 5)).get()) 55 | expected.add(target.getByCubeCoordinate(fromCoordinates(4, 5)).get()) 56 | 57 | val actual = target.getHexagonsByCubeRange(fromCoordinates(GRID_X_FROM, GRID_Z_FROM), fromCoordinates(GRID_X_TO, GRID_Z_TO)) 58 | var count = 0 59 | 60 | val actuals = ArrayList>() 61 | for (hex in actual) { 62 | actuals.add(hex) 63 | count++ 64 | } 65 | assertEquals(expected.size, count) 66 | for (hex in actuals) { 67 | expected.remove(hex) 68 | } 69 | assertTrue(expected.isEmpty()) 70 | } 71 | 72 | @Test 73 | fun shouldReturnProperHexagonsWhenGetHexagonsByOffsetRangeIsCalled() { 74 | val expected = HashSet>() 75 | 76 | expected.add(target.getByCubeCoordinate(fromCoordinates(1, 3)).get()) 77 | expected.add(target.getByCubeCoordinate(fromCoordinates(2, 3)).get()) 78 | expected.add(target.getByCubeCoordinate(fromCoordinates(3, 3)).get()) 79 | 80 | expected.add(target.getByCubeCoordinate(fromCoordinates(0, 4)).get()) 81 | expected.add(target.getByCubeCoordinate(fromCoordinates(1, 4)).get()) 82 | expected.add(target.getByCubeCoordinate(fromCoordinates(2, 4)).get()) 83 | 84 | expected.add(target.getByCubeCoordinate(fromCoordinates(0, 5)).get()) 85 | expected.add(target.getByCubeCoordinate(fromCoordinates(1, 5)).get()) 86 | expected.add(target.getByCubeCoordinate(fromCoordinates(2, 5)).get()) 87 | 88 | val actual = target.getHexagonsByOffsetRange(GRID_X_FROM, GRID_X_TO, GRID_Z_FROM, GRID_Z_TO) 89 | var count = 0 90 | val actuals = ArrayList>() 91 | for (hex in actual) { 92 | actuals.add(hex) 93 | count++ 94 | } 95 | assertEquals(expected.size, count) 96 | for (hex in actuals) { 97 | expected.remove(hex) 98 | } 99 | assertTrue(expected.isEmpty()) 100 | } 101 | 102 | @Test 103 | fun shouldContainCoordinateWhenContainsCoorinateIsCalledWithProperParameters() { 104 | val gridX = 2 105 | val gridZ = 3 106 | assertTrue(target.containsCubeCoordinate(fromCoordinates(gridX, gridZ))) 107 | } 108 | 109 | @Test 110 | fun shouldReturnHexagonWhenGetByGridCoordinateIsCalledWithProperCoordinates() { 111 | val gridX = 2 112 | val gridZ = 3 113 | val hex = target.getByCubeCoordinate(fromCoordinates(gridX, gridZ)) 114 | assertTrue(hex.isPresent) 115 | } 116 | 117 | @Test 118 | fun shouldBeEmptyWhenGetByGridCoordinateIsCalledWithInvalidCoordinates() { 119 | val gridX = 20 120 | val gridZ = 30 121 | val result = target.getByCubeCoordinate(fromCoordinates(gridX, gridZ)) 122 | assertFalse(result.isPresent) 123 | } 124 | 125 | @Test 126 | fun shouldReturnHexagonWhenCalledWithProperCoordinates() { 127 | val x = 310.0 128 | val y = 255.0 129 | val hex = target.getByPixelCoordinate(x, y).get() 130 | assertEquals(hex.gridX, 3) 131 | assertEquals(hex.gridZ, 5) 132 | } 133 | 134 | @Test 135 | fun shouldReturnHexagonWhenCalledWithProperCoordinates2() { 136 | val x = 300.0 137 | val y = 275.0 138 | val hex = target.getByPixelCoordinate(x, y).get() 139 | assertEquals(hex.gridX, 3) 140 | assertEquals(hex.gridZ, 5) 141 | } 142 | 143 | @Test 144 | fun shouldReturnHexagonWhenCalledWithProperCoordinates3() { 145 | val x = 325.0 146 | val y = 275.0 147 | val hex = target.getByPixelCoordinate(x, y).get() 148 | assertEquals(hex.gridX, 3) 149 | assertEquals(hex.gridZ, 5) 150 | } 151 | 152 | @Test 153 | fun shouldReturnProperNeighborsOfHexagonWhenHexIsInMiddle() { 154 | val hex = target.getByCubeCoordinate(fromCoordinates(3, 7)).get() 155 | val expected = HashSet>() 156 | expected.add(target.getByCubeCoordinate(fromCoordinates(3, 6)).get()) 157 | expected.add(target.getByCubeCoordinate(fromCoordinates(4, 6)).get()) 158 | expected.add(target.getByCubeCoordinate(fromCoordinates(4, 7)).get()) 159 | expected.add(target.getByCubeCoordinate(fromCoordinates(3, 8)).get()) 160 | expected.add(target.getByCubeCoordinate(fromCoordinates(2, 8)).get()) 161 | expected.add(target.getByCubeCoordinate(fromCoordinates(2, 7)).get()) 162 | val actual = target.getNeighborsOf(hex) 163 | assertEquals(expected, actual) 164 | } 165 | 166 | @Test 167 | abstract fun shouldReturnProperNeighborsOfHexagonWhenHexIsOnTheEdge() 168 | 169 | @Test 170 | abstract fun shouldProperlyReturnGridLayoutWhenGetGridLayoutIsCalled() 171 | 172 | @Test 173 | fun shouldProperlyReturnSharedHexagonDataWhenGetSharedHexagonDataIsCalled() { 174 | assertEquals(builder.gridData.hexagonHeight, target.gridData.hexagonHeight) 175 | assertEquals(builder.gridData.hexagonWidth, target.gridData.hexagonWidth) 176 | assertEquals(builder.gridData.radius, target.gridData.radius) 177 | assertEquals(builder.gridData.orientation, target.gridData.orientation) 178 | } 179 | 180 | @Test 181 | fun shouldProperlyReturnGridWidthWhenGetGridWidthIsCalled() { 182 | assertEquals(GRID_WIDTH, target.gridData.gridWidth) 183 | } 184 | 185 | @Test 186 | fun shouldProperlyReturnGridHeightWhenGetGridHeightIsCalled() { 187 | assertEquals(GRID_HEIGHT, target.gridData.gridHeight) 188 | } 189 | 190 | companion object { 191 | 192 | internal const val RADIUS = 30 193 | internal const val GRID_WIDTH = 10 194 | internal const val GRID_HEIGHT = 10 195 | private const val GRID_X_FROM = 2 196 | private const val GRID_X_TO = 4 197 | private const val GRID_Z_FROM = 3 198 | private const val GRID_Z_TO = 5 199 | internal val ORIENTATION = HexagonOrientation.POINTY_TOP 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/internal/impl/HexagonalGridImpl.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl 2 | 3 | import org.hexworks.cobalt.datatypes.Maybe 4 | import org.hexworks.mixite.core.api.* 5 | import org.hexworks.mixite.core.api.contract.HexagonDataStorage 6 | import org.hexworks.mixite.core.api.contract.SatelliteData 7 | import org.hexworks.mixite.core.internal.GridData 8 | import kotlin.math.abs 9 | 10 | class HexagonalGridImpl(builder: HexagonalGridBuilder) : HexagonalGrid { 11 | 12 | override val gridData: GridData = builder.gridData 13 | private val hexagonDataStorage: HexagonDataStorage = builder.getHexagonDataStorage() 14 | 15 | override val hexagons: Iterable> 16 | get() { 17 | val coordIter = hexagonDataStorage.coordinates.iterator() 18 | 19 | return object : Iterable> { 20 | override fun iterator(): Iterator> { 21 | return object : Iterator> { 22 | override fun hasNext(): Boolean { 23 | return coordIter.hasNext() 24 | } 25 | 26 | override fun next(): Hexagon { 27 | return hexagon(coordIter.next()) 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | init { 35 | for (cubeCoordinate in builder.gridLayoutStrategy.fetchGridCoordinates(builder)) { 36 | this@HexagonalGridImpl.hexagonDataStorage.addCoordinate(cubeCoordinate) 37 | } 38 | } 39 | 40 | private fun hexagon(coordinate: CubeCoordinate): HexagonImpl { 41 | return HexagonImpl(gridData, coordinate, hexagonDataStorage) 42 | } 43 | 44 | override fun getHexagonsByCubeRange(from: CubeCoordinate, to: CubeCoordinate): Iterable> { 45 | val coordinates = ArrayList(abs(from.gridZ-to.gridZ) + abs(from.gridX-to.gridX)) 46 | 47 | for (gridZ in from.gridZ..to.gridZ) { 48 | for (gridX in from.gridX..to.gridX) { 49 | val coord = CubeCoordinate.fromCoordinates(gridX, gridZ) 50 | if (containsCubeCoordinate(coord)) { 51 | coordinates.add(coord) 52 | } 53 | } 54 | } 55 | 56 | val coordIter = coordinates.iterator() 57 | 58 | return object : Iterable> { 59 | override fun iterator(): Iterator> { 60 | return object : Iterator> { 61 | override fun hasNext(): Boolean { 62 | return coordIter.hasNext() 63 | } 64 | 65 | override fun next(): Hexagon { 66 | return hexagon(coordIter.next()) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | override fun getHexagonsByOffsetRange(gridXFrom: Int, gridXTo: Int, gridYFrom: Int, gridYTo: Int): Iterable> { 74 | val coords = ArrayList() 75 | 76 | for (gridX in gridXFrom..gridXTo) { 77 | for (gridY in gridYFrom..gridYTo) { 78 | val cubeX = CoordinateConverter.convertOffsetCoordinatesToCubeX(gridX, gridY, gridData.orientation) 79 | val cubeZ = CoordinateConverter.convertOffsetCoordinatesToCubeZ(gridX, gridY, gridData.orientation) 80 | val coord = CubeCoordinate.fromCoordinates(cubeX, cubeZ) 81 | if (containsCubeCoordinate(coord)) { 82 | coords.add(coord) 83 | } 84 | } 85 | } 86 | 87 | val coordIter = coords.iterator() 88 | 89 | return object : Iterable> { 90 | override fun iterator(): Iterator> { 91 | return object : Iterator> { 92 | override fun hasNext(): Boolean { 93 | return coordIter.hasNext() 94 | } 95 | 96 | override fun next(): Hexagon { 97 | return hexagon(coordIter.next()) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | override fun containsCubeCoordinate(coordinate: CubeCoordinate): Boolean { 105 | return this.hexagonDataStorage.containsCoordinate(coordinate) 106 | } 107 | 108 | private fun _getByCubeCoordinate(coordinate: CubeCoordinate) = 109 | if (containsCubeCoordinate(coordinate)) hexagon(coordinate) else null 110 | 111 | override fun getByCubeCoordinate(coordinate: CubeCoordinate): Maybe> = 112 | Maybe.ofNullable(_getByCubeCoordinate(coordinate)) 113 | 114 | override fun getByPixelCoordinate(coordinateX: Double, coordinateY: Double): Maybe> { 115 | var estimatedGridX = (coordinateX / gridData.hexagonWidth).toInt() 116 | var estimatedGridZ = (coordinateY / gridData.hexagonHeight).toInt() 117 | estimatedGridX = CoordinateConverter.convertOffsetCoordinatesToCubeX(estimatedGridX, estimatedGridZ, gridData.orientation) 118 | estimatedGridZ = CoordinateConverter.convertOffsetCoordinatesToCubeZ(estimatedGridX, estimatedGridZ, gridData.orientation) 119 | // it is possible that the estimated coordinates are off-grid so we 120 | // create a virtual hexagon 121 | val estimatedCoordinate = CubeCoordinate.fromCoordinates(estimatedGridX, estimatedGridZ) 122 | val centerHex = hexagon(estimatedCoordinate) 123 | val nearestHex = nearestHexagonToPoint(centerHex, Point.fromPosition(coordinateX, coordinateY)) 124 | 125 | return if (nearestHex === centerHex) { 126 | getByCubeCoordinate(estimatedCoordinate) // centerHex may have been off-grid so look it up again 127 | } else { 128 | Maybe.of(nearestHex) // Any other result must be a (real) neighbour 129 | } 130 | } 131 | 132 | private fun _getNeighborByIndex(hexagon: Hexagon, index: Int) = 133 | CubeCoordinate.fromCoordinates( 134 | hexagon.gridX + NEIGHBORS[index][NEIGHBOR_X_INDEX], 135 | hexagon.gridZ + NEIGHBORS[index][NEIGHBOR_Z_INDEX] 136 | ) 137 | 138 | override fun getNeighborCoordinateByIndex(coordinate: CubeCoordinate, index: Int) = 139 | CubeCoordinate.fromCoordinates( 140 | coordinate.gridX + NEIGHBORS[index][NEIGHBOR_X_INDEX], 141 | coordinate.gridZ + NEIGHBORS[index][NEIGHBOR_Z_INDEX] 142 | ) 143 | 144 | override fun getNeighborByIndex(hexagon: Hexagon, index: Int) = 145 | getByCubeCoordinate(_getNeighborByIndex(hexagon, index)) 146 | 147 | override fun getNeighborsOf(hexagon: Hexagon): Collection> { 148 | val neighbors = HashSet>() 149 | for (i in NEIGHBORS.indices) { 150 | val retHex = getNeighborByIndex(hexagon, i) 151 | if (retHex.isPresent) { 152 | neighbors.add(retHex.get()) 153 | } 154 | } 155 | return neighbors 156 | } 157 | 158 | /* 159 | * Returns either the original center hex or the nearest (real) hex around it 160 | */ 161 | private fun nearestHexagonToPoint(centerHex: Hexagon, point: Point): Hexagon { 162 | var nearest = centerHex 163 | var nearestDistance = Double.MAX_VALUE 164 | var current: Hexagon? = nearest // Start with center then check six neighbours 165 | 166 | var i = 0; 167 | while(true) { 168 | current?.let { 169 | val currentDistance = point.distanceFrom(it.center) 170 | when { 171 | currentDistance < gridData.innerRadius -> return it // Shortcut if well inside bounds of current hex 172 | currentDistance < nearestDistance -> { 173 | // This covers points right in the corner between three hex's (not within innerRadius of any of them) 174 | // TODO In theory, we can shortcut if we have refined twice! 175 | nearest = it 176 | nearestDistance = currentDistance 177 | } 178 | } 179 | } 180 | 181 | if (i == 6) { 182 | return nearest // No direct match, pick the nearest one 183 | } 184 | current = _getByCubeCoordinate(_getNeighborByIndex(centerHex, i++)) 185 | } 186 | } 187 | 188 | companion object { 189 | 190 | private val NEIGHBORS = arrayOf(intArrayOf(+1, 0), intArrayOf(+1, -1), intArrayOf(0, -1), intArrayOf(-1, 0), intArrayOf(-1, +1), intArrayOf(0, +1)) 191 | private const val NEIGHBOR_X_INDEX = 0 192 | private const val NEIGHBOR_Z_INDEX = 1 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/layoutstrategy/HexagonalGridLayoutStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl.layoutstrategy 2 | 3 | import org.hexworks.mixite.core.GridLayoutStrategyTestUtil.fetchDefaultBuilder 4 | import org.hexworks.mixite.core.api.CubeCoordinate 5 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromCoordinates 6 | import org.hexworks.mixite.core.api.HexagonOrientation.FLAT_TOP 7 | import org.hexworks.mixite.core.api.HexagonalGridBuilder 8 | import org.hexworks.mixite.core.api.contract.SatelliteData 9 | import kotlin.test.BeforeTest 10 | import kotlin.test.Test 11 | import kotlin.test.assertFalse 12 | import kotlin.test.assertTrue 13 | 14 | class HexagonalGridLayoutStrategyTest { 15 | 16 | private lateinit var target: HexagonalGridLayoutStrategy 17 | private lateinit var builder: HexagonalGridBuilder 18 | 19 | @BeforeTest 20 | fun setUp() { 21 | builder = fetchDefaultBuilder() 22 | target = HexagonalGridLayoutStrategy() 23 | } 24 | 25 | @Test 26 | fun shouldProperlyCreateHexagonsWithPointyOrientationWhenCreateHexagonsIsCalled() { 27 | val coordIter = target.fetchGridCoordinates(builder).iterator() 28 | 29 | val coords = ArrayList() 30 | while (coordIter.hasNext()) { 31 | coords.add(coordIter.next()) 32 | } 33 | 34 | assertTrue(coords.contains(fromCoordinates(1, 0))) 35 | assertTrue(coords.contains(fromCoordinates(2, 0))) 36 | assertTrue(coords.contains(fromCoordinates(2, 1))) 37 | assertTrue(coords.contains(fromCoordinates(1, 2))) 38 | assertTrue(coords.contains(fromCoordinates(0, 2))) 39 | assertTrue(coords.contains(fromCoordinates(0, 1))) 40 | 41 | assertTrue(!coords.contains(fromCoordinates(0, 0))) 42 | assertTrue(!coords.contains(fromCoordinates(1, -1))) 43 | assertTrue(!coords.contains(fromCoordinates(2, -1))) 44 | assertTrue(!coords.contains(fromCoordinates(3, -1))) 45 | assertTrue(!coords.contains(fromCoordinates(3, 0))) 46 | assertTrue(!coords.contains(fromCoordinates(3, 1))) 47 | assertTrue(!coords.contains(fromCoordinates(2, 2))) 48 | assertTrue(!coords.contains(fromCoordinates(1, 3))) 49 | assertTrue(!coords.contains(fromCoordinates(0, 3))) 50 | assertTrue(!coords.contains(fromCoordinates(-1, 3))) 51 | assertTrue(!coords.contains(fromCoordinates(-1, 2))) 52 | assertTrue(!coords.contains(fromCoordinates(-1, 1))) 53 | } 54 | 55 | @Test 56 | fun shouldProperlyCreateHexagonsWithPointyOrientationWhenCreateHexagonsIsCalledWithBiggerSize() { 57 | builder.setGridHeight(5).setGridWidth(5) 58 | val coordIter = target.fetchGridCoordinates(builder).iterator() 59 | 60 | val coords = ArrayList() 61 | while (coordIter.hasNext()) { 62 | coords.add(coordIter.next()) 63 | } 64 | 65 | assertTrue(coords.contains(fromCoordinates(1, 0))) 66 | assertTrue(coords.contains(fromCoordinates(2, 0))) 67 | assertTrue(coords.contains(fromCoordinates(3, 0))) 68 | assertTrue(coords.contains(fromCoordinates(3, 1))) 69 | assertTrue(coords.contains(fromCoordinates(3, 2))) 70 | assertTrue(coords.contains(fromCoordinates(2, 3))) 71 | assertTrue(coords.contains(fromCoordinates(1, 4))) 72 | assertTrue(coords.contains(fromCoordinates(0, 4))) 73 | assertTrue(coords.contains(fromCoordinates(-1, 4))) 74 | assertTrue(coords.contains(fromCoordinates(-1, 3))) 75 | assertTrue(coords.contains(fromCoordinates(-1, 2))) 76 | assertTrue(coords.contains(fromCoordinates(0, 1))) 77 | 78 | assertTrue(!coords.contains(fromCoordinates(0, 0))) 79 | assertTrue(!coords.contains(fromCoordinates(1, -1))) 80 | assertTrue(!coords.contains(fromCoordinates(2, -1))) 81 | assertTrue(!coords.contains(fromCoordinates(3, -1))) 82 | assertTrue(!coords.contains(fromCoordinates(4, -1))) 83 | assertTrue(!coords.contains(fromCoordinates(4, 0))) 84 | assertTrue(!coords.contains(fromCoordinates(4, 1))) 85 | assertTrue(!coords.contains(fromCoordinates(4, 2))) 86 | assertTrue(!coords.contains(fromCoordinates(3, 3))) 87 | assertTrue(!coords.contains(fromCoordinates(2, 4))) 88 | assertTrue(!coords.contains(fromCoordinates(1, 5))) 89 | assertTrue(!coords.contains(fromCoordinates(0, 5))) 90 | assertTrue(!coords.contains(fromCoordinates(-1, 5))) 91 | assertTrue(!coords.contains(fromCoordinates(-2, 5))) 92 | assertTrue(!coords.contains(fromCoordinates(-2, 4))) 93 | assertTrue(!coords.contains(fromCoordinates(-2, 3))) 94 | assertTrue(!coords.contains(fromCoordinates(-2, 2))) 95 | assertTrue(!coords.contains(fromCoordinates(-1, 1))) 96 | } 97 | 98 | @Test 99 | fun shouldProperlyCreateHexagonsWithFlatOrientationWhenCreateHexagonsIsCalled() { 100 | builder.setOrientation(FLAT_TOP) 101 | val coordIter = target.fetchGridCoordinates(builder).iterator() 102 | 103 | val coords = ArrayList() 104 | while (coordIter.hasNext()) { 105 | coords.add(coordIter.next()) 106 | } 107 | 108 | 109 | assertTrue(coords.contains(fromCoordinates(1, 0))) 110 | assertTrue(coords.contains(fromCoordinates(2, 0))) 111 | assertTrue(coords.contains(fromCoordinates(2, 1))) 112 | assertTrue(coords.contains(fromCoordinates(1, 2))) 113 | assertTrue(coords.contains(fromCoordinates(0, 2))) 114 | assertTrue(coords.contains(fromCoordinates(0, 1))) 115 | 116 | assertTrue(!coords.contains(fromCoordinates(0, 0))) 117 | assertTrue(!coords.contains(fromCoordinates(0, -1))) 118 | assertTrue(!coords.contains(fromCoordinates(2, -1))) 119 | assertTrue(!coords.contains(fromCoordinates(3, -1))) 120 | assertTrue(!coords.contains(fromCoordinates(3, 0))) 121 | assertTrue(!coords.contains(fromCoordinates(3, 1))) 122 | assertTrue(!coords.contains(fromCoordinates(2, 2))) 123 | assertTrue(!coords.contains(fromCoordinates(1, 3))) 124 | assertTrue(!coords.contains(fromCoordinates(0, 3))) 125 | assertTrue(!coords.contains(fromCoordinates(-1, 3))) 126 | assertTrue(!coords.contains(fromCoordinates(-1, 2))) 127 | assertTrue(!coords.contains(fromCoordinates(-1, 1))) 128 | } 129 | 130 | @Test 131 | fun shouldProperlyCreateHexagonsWithFlatOrientationWhenCreateHexagonsIsCalledWithBiggerSize() { 132 | builder.setGridHeight(5).setGridWidth(5).setOrientation(FLAT_TOP) 133 | val coordIter = target.fetchGridCoordinates(builder).iterator() 134 | 135 | val coords = ArrayList() 136 | while (coordIter.hasNext()) { 137 | coords.add(coordIter.next()) 138 | } 139 | 140 | 141 | assertTrue(coords.contains(fromCoordinates(2, -1))) 142 | assertTrue(coords.contains(fromCoordinates(3, -1))) 143 | assertTrue(coords.contains(fromCoordinates(4, -1))) 144 | assertTrue(coords.contains(fromCoordinates(4, 0))) 145 | assertTrue(coords.contains(fromCoordinates(4, 1))) 146 | assertTrue(coords.contains(fromCoordinates(3, 2))) 147 | assertTrue(coords.contains(fromCoordinates(2, 3))) 148 | assertTrue(coords.contains(fromCoordinates(1, 3))) 149 | assertTrue(coords.contains(fromCoordinates(0, 3))) 150 | assertTrue(coords.contains(fromCoordinates(0, 2))) 151 | assertTrue(coords.contains(fromCoordinates(0, 1))) 152 | assertTrue(coords.contains(fromCoordinates(1, 0))) 153 | 154 | assertTrue(!coords.contains(fromCoordinates(0, 0))) 155 | assertTrue(!coords.contains(fromCoordinates(1, -1))) 156 | assertTrue(!coords.contains(fromCoordinates(2, -2))) 157 | assertTrue(!coords.contains(fromCoordinates(3, -2))) 158 | assertTrue(!coords.contains(fromCoordinates(4, -2))) 159 | assertTrue(!coords.contains(fromCoordinates(5, -2))) 160 | assertTrue(!coords.contains(fromCoordinates(5, -1))) 161 | assertTrue(!coords.contains(fromCoordinates(5, 0))) 162 | assertTrue(!coords.contains(fromCoordinates(5, 1))) 163 | assertTrue(!coords.contains(fromCoordinates(4, 2))) 164 | assertTrue(!coords.contains(fromCoordinates(3, 3))) 165 | assertTrue(!coords.contains(fromCoordinates(2, 4))) 166 | assertTrue(!coords.contains(fromCoordinates(1, 4))) 167 | assertTrue(!coords.contains(fromCoordinates(0, 4))) 168 | assertTrue(!coords.contains(fromCoordinates(-1, 4))) 169 | assertTrue(!coords.contains(fromCoordinates(-1, 3))) 170 | assertTrue(!coords.contains(fromCoordinates(-1, 2))) 171 | assertTrue(!coords.contains(fromCoordinates(-1, 1))) 172 | } 173 | 174 | @Test 175 | fun testCheckParameters0() { 176 | val result = target.checkParameters(1, 1) // super: true, derived: true 177 | assertTrue(result) 178 | } 179 | 180 | @Test 181 | fun testCheckParameters1() { 182 | val result = target.checkParameters(1, 2) // super: true, derived: false 183 | assertFalse(result) 184 | } 185 | 186 | @Test 187 | fun testCheckParameters2() { 188 | val result = target.checkParameters(2, 2) // super: true, derived: false 189 | assertFalse(result) 190 | } 191 | 192 | @Test 193 | fun testCheckParameters3() { 194 | val result = target.checkParameters(0, 0) // super: false, derived: false; 195 | assertFalse(result) 196 | } 197 | 198 | @Test 199 | fun testCheckParameters4() { 200 | val result = target.checkParameters(-1, -1) // super: false, derived: true; 201 | assertFalse(result) 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mixite 2 | 3 | Mixite is a hexagonal grid library. The motivation behind it is to have an optimized, simple and usable library for drawing 4 | hexagonal grids without being tied to any GUI framework. 5 | 6 | This means that you can use Mixite on Android, your backend or your desktop app. 7 | 8 | There is a REST-based web example which you can tinker with [here][herokurestlink]. *(not recommended, this is under rewrite)* 9 | 10 | You can also check out the mixite.example.swt project [here][exampleprojectslink]. 11 | 12 | Mixite currently supports a maximum grid size of 1000 * 1000 (1.000.000 cells) with the default implementation but 13 | you can provide your own storage implementation to alleviate this limitation. 14 | 15 | > **Disclaimer for Java users**: 16 | > There is no need to worry, Mixite works in exactly the same way as Hexameter worked before. 17 | > Java interop is seamless, you only have to change the imports / project dependency. 18 | > 19 | > As always with Maven Central artifacts: previous versions of Hexameter also work, they 20 | > are not affected. 21 | --- 22 | 23 | Need info? [Ask us on Discord][discord] 24 | | or [Create an issue](issues/new) 25 | | Support us on [Patreon](https://www.patreon.com/hexworks) 26 | 27 | [![][circleci img]][circleci] 28 | [![][maven img]][maven] 29 | [![](https://jitpack.io/v/Hexworks/mixite.svg)](https://jitpack.io/#Hexworks/mixite) 30 | [![][license img]][license] 31 | 32 | [discord]:https://discord.gg/vSNgvBh 33 | 34 | [circleci]:https://circleci.com/gh/Hexworks/mixite 35 | [circleci img]:https://circleci.com/gh/Hexworks/mixite/tree/master.svg?style=shield 36 | 37 | [license]:LICENSE 38 | [license img]:https://img.shields.io/badge/License-Apache2.0-blue.svg 39 | 40 | [maven]:https://search.maven.org/search?q=g:org.hexworks.mixite 41 | [maven img]:https://maven-badges.herokuapp.com/maven-central/org.hexworks.mixite/mixite.core/badge.svg 42 | 43 | --- 44 | 45 | ## Getting started 46 | 47 | This library uses [Amit's guide to hexagonal grids][amitlink]. The coordinate system used by this library is the Cubic 48 | coordinate system. Please check [here][cubecoords] for further details. 49 | 50 | Hexagonal grids come in **flat topped** and **pointy topped** shapes. The grid can have several layouts: 51 | - Hexagonal: the width and height of a this layout has to be equal and both have to be an odd number. 52 | - Triangular: the width and height of a this layout has to be equal. 53 | - Rectangular: no special rules 54 | - Trapezoid: no special rules 55 | - Custom: your own implementation of [`GridLayoutStrategy`][gridlayoutstrategylink] 56 | 57 | All layouts have *width* and *height* values of at least **1**. 58 | You can consult [HexagonalGridLayout][hexgridlayout] if you need further details. 59 | 60 | This library is not tied to any GUI implementation. All operations provided by the [API][api] work using the most 61 | abstract concept possible. 62 | 63 | ## Basic usage 64 | 65 | ### Importing Mixite 66 | Let's start by adding Mixite as a dependency. 67 | 68 | #### Maven 69 | 70 | ```xml 71 | 72 | 73 | jitpack.io 74 | https://jitpack.io 75 | 76 | 77 | ... 78 | 79 | com.github.Hexworks.mixite 80 | mixite.core-jvm 81 | 2018.2.0-RELEASE 82 | 83 | ``` 84 | 85 | #### Gradle 86 | 87 | ```groovy 88 | allprojects { 89 | repositories { 90 | maven { url 'https://jitpack.io' } // Mixite uses Jitpack repository 91 | } 92 | } 93 | ... 94 | dependencies { 95 | implementation 'com.github.Hexworks.mixite:mixite.core-jvm:2018.2.0-RELEASE' 96 | } 97 | ``` 98 | 99 | > Note that if you are using Javascript you need `mixite.core-web`: 100 | > `'org.hexworks.mixite:mixite.core-web:2018.2.0-RELEASE'` 101 | 102 | You can also use the latest preview versions, more info [here](https://jitpack.io/#Hexworks/Mixite). 103 | 104 | ### Creating a grid 105 | 106 | You can use the [HexagonalGridBuilder][hexgridbuilder] to create a [HexagonalGrid][hexgrid]: 107 | 108 | ```java 109 | HexagonalGridBuilder builder = new HexagonalGridBuilder<>() 110 | .setGridHeight(9) 111 | .setGridWidth(9) 112 | .setGridLayout(HexagonalGridLayout.RECTANGULAR) 113 | .setOrientation(HexagonOrientation.FLAT_TOP) 114 | .setRadius(30.0); 115 | HexagonalGrid grid = builder.build(); 116 | ``` 117 | 118 | You can also use it to create a [HexagonalGridCalculator][hexgridcalc] for you which 119 | supports advanced operations on [HexagonalGrid][hexgrid]s: 120 | 121 | ```java 122 | HexagonalGridCalculator calc = builder.buildCalculatorFor(grid); 123 | calc.calculateDistanceBetween(sourceHex, targetHex) 124 | ``` 125 | 126 | ### Drawing a grid 127 | 128 | Method `Grid.getHexagons` returns an iterable collection of hexagons. Each `Point` represents a coordinate in 2D space that can 129 | be transformed for rendering. 130 | ```java 131 | for (Hexagon hexagon : grid.getHexagons()) { 132 | for(Point p : hexagon.getPoints()) { 133 | // Do you stuff with point.coordinateX, point.coordinateY 134 | } 135 | } 136 | ``` 137 | 138 | 139 | ### Manipulating your grid 140 | 141 | There are basically only one operation for manipulating your data on the grid: 142 | The `Hexagon#setSatelliteData(T data)` operation with which you can add your own arbitrary 143 | data to a `Hexagon` object. This means that once created a `HexagonalGrid` is immutable apart from the 144 | satellite data you add. 145 | 146 | There is also a `HexagonalGrid#clearSatelliteData()` method for clearing all satellite data from your grid. 147 | 148 | The implementation of the `HexagonalGrid` is lazy. This means that it only stores data which is absolutely necessary 149 | to keep in memory (the coordinates and your satellite data). Everything else is generated on the fly. The only limiting 150 | factor of a grid at the moment is the coordinates (which consume memory) and the satellite data. 151 | 152 | ### GUI example: 153 | 154 | You can find a simple GUI example in the `mixite.example.swt` project. Run it by doing the following steps. 155 | 156 | 1. Clone the project: `git clone git@github.com:Hexworks/mixite.git` 157 | 2. cd to the newly created `mixite` folder: `cd mixite/` 158 | 3. build the project: `./gradlew clean build` (or `gradlew clean build` on Windows) 159 | 4. run the created uberjar: `java -jar mixite.example.swt/build/libs/mixite.example.swt.jar` 160 | 161 | 162 | ### Supported operations 163 | - Querying the characteristics of the `HexagonGrid` 164 | - Fetching all the `Hexagon` objects from the grid 165 | - Getting a subset of Hexagons (using cube or offset coordinate range) from the grid 166 | - Checking whether a Hexagon is on a grid or not 167 | - Getting a `Hexagon` by its grid coordinate (cube) 168 | - Getting a `Hexagon` by its pixel coordinate 169 | - Getting the neighbors of a hexagon (also by index) 170 | 171 | ### Advanced operations 172 | - Calculating the distance between two `Hexagon`s 173 | - Calculating the movement range from a `Hexagon` to an other 174 | - Rotating a `Hexagon` 175 | - Calculating a ring from a `Hexagon` 176 | - Draw a line from a `Hexagon` to an other 177 | - Checking visibility of a `Hexagon` from an other 178 | - Adding custom data to a Hexagon 179 | - Clearing all custom data from the HexagonalGrid 180 | 181 | Check these interfaces for more details: 182 | 183 | - [HexagonalGridBuilder][hexgridbuilder] 184 | - [HexagonalGrid][hexgrid] 185 | - [HexagonalGridCalculator][hexgridcalc] 186 | - [Hexagon][hex] 187 | 188 | ### Usage tips 189 | - You can add satellite data (any arbitrary data you have) to a `Hexagon`. By implementing the [`SatelliteData`][satdatlink] 190 | interface you gain operations like visibility checking 191 | - Mixite comes with a sensible default implementation of [`SatelliteData`][satdatlink] so if you don't want to add extra data 192 | you can use [`DefaultSatelliteData`][defsatdatlink]. 193 | - You can use your own implementation of [`HexagonDataStorage`][hexdatstorlink] for storing your `Hexagon`s 194 | - Mixite comes with a sensible [`DefaultHexagonDataStorage`][defhexdatstorlink] implementation which stores all data in memory 195 | - You don't have to fetch all `Hexagon` objects by using the `getHexagons` method. You can query `Hexagon`s by a range using 196 | offset or cube coordinates 197 | 198 | ## Road map 199 | - Path finding with obstacles (blocking movement) 200 | - Movement range with obstacles and movement cost calculation 201 | - Android example 202 | 203 | ## License 204 | Mixite is made available under the [Apache2 License](https://opensource.org/licenses/Apache-2.0). 205 | 206 | ## Credits 207 | Mixite is created and maintained by Adam Arold 208 | 209 | *I'm open to suggestions, feel free to comment or to send me a message. 210 | Pull requests are also welcome!* 211 | 212 | 213 | [amitlink]:http://www.redblobgames.com/grids/hexagons/ 214 | [cubecoords]:http://www.redblobgames.com/grids/hexagons/#coordinates 215 | [herokurestlink]:http://hexameter-rest-example.herokuapp.com/ 216 | [exampleprojectslink]:https://github.com/Hexworks/mixite.example/tree/master/mixite.example.swt 217 | 218 | [hexgridlayout]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGridLayout.kt 219 | [hexgridbuilder]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGridBuilder.kt 220 | [api]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api 221 | [hexgrid]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGrid.kt 222 | [hexgridcalc]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/HexagonalGridCalculator.kt 223 | [hex]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/Hexagon.kt 224 | [satdatlink]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/contract/SatelliteData.kt 225 | [defsatdatlink]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/defaults/DefaultSatelliteData.kt 226 | [hexdatstorlink]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/contract/HexagonDataStorage.kt 227 | [defhexdatstorlink]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/defaults/DefaultHexagonDataStorage.kt 228 | [gridlayoutstrategylink]:mixite.core/src/commonMain/kotlin/org/hexworks/mixite/core/api/GridLayoutStrategy.kt 229 | -------------------------------------------------------------------------------- /mixite.core/src/commonTest/kotlin/org/hexworks/mixite/core/internal/impl/HexagonalGridCalculatorImplTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.mixite.core.internal.impl 2 | 3 | import org.hexworks.mixite.core.HexagonStub 4 | import org.hexworks.mixite.core.api.* 5 | import org.hexworks.mixite.core.api.CubeCoordinate.Companion.fromCoordinates 6 | import org.hexworks.mixite.core.api.RotationDirection.LEFT 7 | import org.hexworks.mixite.core.api.RotationDirection.RIGHT 8 | import org.hexworks.mixite.core.api.defaults.DefaultSatelliteData 9 | import kotlin.test.* 10 | 11 | class HexagonalGridCalculatorImplTest { 12 | 13 | private lateinit var grid: HexagonalGrid 14 | private lateinit var target: HexagonalGridCalculator 15 | 16 | private lateinit var originalHex: Hexagon 17 | private lateinit var targetHex: Hexagon 18 | 19 | @BeforeTest 20 | fun setUp() { 21 | val builder = HexagonalGridBuilder() 22 | .setGridHeight(10) 23 | .setGridWidth(10) 24 | .setRadius(10.0) 25 | grid = builder.build() 26 | target = builder.buildCalculatorFor(grid) 27 | } 28 | 29 | @Test 30 | fun shouldProperlyCalculateDistanceBetweenTwoHexes() { 31 | val hex0 = grid.getByCubeCoordinate(fromCoordinates(1, 1)).get() 32 | val hex1 = grid.getByCubeCoordinate(fromCoordinates(4, 5)).get() 33 | assertEquals(7, target.calculateDistanceBetween(hex0, hex1)) 34 | } 35 | 36 | @Test 37 | fun shouldProperlyCalculateMovementRangeFromHexWith1() { 38 | val hex = grid.getByCubeCoordinate(fromCoordinates(3, 7)).get() 39 | val expected = HashSet>() 40 | expected.add(hex) 41 | expected.add(grid.getByCubeCoordinate(fromCoordinates(3, 6)).get()) 42 | expected.add(grid.getByCubeCoordinate(fromCoordinates(4, 6)).get()) 43 | expected.add(grid.getByCubeCoordinate(fromCoordinates(4, 7)).get()) 44 | expected.add(grid.getByCubeCoordinate(fromCoordinates(3, 8)).get()) 45 | expected.add(grid.getByCubeCoordinate(fromCoordinates(2, 8)).get()) 46 | expected.add(grid.getByCubeCoordinate(fromCoordinates(2, 7)).get()) 47 | val actual = target.calculateMovementRangeFrom(hex, 1) 48 | assertEquals(expected, actual) 49 | } 50 | 51 | @Test 52 | fun shouldProperlyCalculateMovementRangeFromHexWith2() { 53 | val hex = grid.getByCubeCoordinate(fromCoordinates(3, 7)).get() 54 | val expected = HashSet>() 55 | expected.add(hex) 56 | expected.add(grid.getByCubeCoordinate(fromCoordinates(3, 6)).get()) 57 | expected.add(grid.getByCubeCoordinate(fromCoordinates(4, 6)).get()) 58 | expected.add(grid.getByCubeCoordinate(fromCoordinates(4, 7)).get()) 59 | expected.add(grid.getByCubeCoordinate(fromCoordinates(3, 8)).get()) 60 | expected.add(grid.getByCubeCoordinate(fromCoordinates(2, 8)).get()) 61 | expected.add(grid.getByCubeCoordinate(fromCoordinates(2, 7)).get()) 62 | 63 | expected.add(grid.getByCubeCoordinate(fromCoordinates(3, 5)).get()) 64 | expected.add(grid.getByCubeCoordinate(fromCoordinates(4, 5)).get()) 65 | expected.add(grid.getByCubeCoordinate(fromCoordinates(5, 5)).get()) 66 | expected.add(grid.getByCubeCoordinate(fromCoordinates(2, 6)).get()) 67 | expected.add(grid.getByCubeCoordinate(fromCoordinates(5, 6)).get()) 68 | expected.add(grid.getByCubeCoordinate(fromCoordinates(1, 7)).get()) 69 | expected.add(grid.getByCubeCoordinate(fromCoordinates(5, 7)).get()) 70 | expected.add(grid.getByCubeCoordinate(fromCoordinates(1, 8)).get()) 71 | expected.add(grid.getByCubeCoordinate(fromCoordinates(4, 8)).get()) 72 | expected.add(grid.getByCubeCoordinate(fromCoordinates(1, 9)).get()) 73 | expected.add(grid.getByCubeCoordinate(fromCoordinates(3, 9)).get()) 74 | expected.add(grid.getByCubeCoordinate(fromCoordinates(2, 9)).get()) 75 | 76 | val actual = target.calculateMovementRangeFrom(hex, 2) 77 | assertEquals(expected, actual) 78 | } 79 | 80 | @Test 81 | fun shouldProperlyCalculateLineWithMultipleElements() { 82 | val actual = target.drawLine(grid.getByCubeCoordinate(fromCoordinates(3, 7)).get(), 83 | grid.getByCubeCoordinate(fromCoordinates(8, 1)).get()) 84 | assertEquals(expected = listOf(grid.getByCubeCoordinate(fromCoordinates(3, 7)).get(), 85 | grid.getByCubeCoordinate(fromCoordinates(4, 6)).get(), 86 | grid.getByCubeCoordinate(fromCoordinates(5, 5)).get(), 87 | grid.getByCubeCoordinate(fromCoordinates(6, 4)).get(), 88 | grid.getByCubeCoordinate(fromCoordinates(6, 3)).get(), 89 | grid.getByCubeCoordinate(fromCoordinates(7, 2)).get(), 90 | grid.getByCubeCoordinate(fromCoordinates(8, 1)).get()), 91 | actual = actual) 92 | } 93 | 94 | @Test 95 | fun shouldProperlyCalculateLineWithOneElement() { 96 | val actual = target.drawLine(grid.getByCubeCoordinate(fromCoordinates(3, 7)).get(), 97 | grid.getByCubeCoordinate(fromCoordinates(3, 7)).get()) 98 | assertTrue { 99 | actual.isEmpty() 100 | } 101 | } 102 | 103 | @Test 104 | fun shouldCheckForVisibilityCorrectly() { 105 | 106 | val hexagon = grid.getByCubeCoordinate(fromCoordinates(2, 5)) 107 | val data = DefaultSatelliteData() 108 | data.isOpaque = true 109 | hexagon.get().setSatelliteData(data) 110 | 111 | assertFalse { 112 | target.isVisible(grid.getByCubeCoordinate(fromCoordinates(8, 1)).get(), 113 | grid.getByCubeCoordinate(fromCoordinates(-3, 8)).get()) 114 | } 115 | assertFalse { 116 | target.isVisible(grid.getByCubeCoordinate(fromCoordinates(4, 4)).get(), 117 | grid.getByCubeCoordinate(fromCoordinates(-3, 8)).get()) 118 | } 119 | assertTrue { 120 | target.isVisible(grid.getByCubeCoordinate(fromCoordinates(8, 3)).get(), 121 | grid.getByCubeCoordinate(fromCoordinates(-3, 8)).get()) 122 | } 123 | assertTrue { 124 | target.isVisible(grid.getByCubeCoordinate(fromCoordinates(7, 1)).get(), 125 | grid.getByCubeCoordinate(fromCoordinates(-3, 8)).get()) 126 | } 127 | } 128 | 129 | @Test 130 | fun shouldProperlyCalculateRotationRightWhenGivenAValidGrid() { 131 | originalHex = HexagonStub( 132 | gridX = 3, 133 | gridY = -2, 134 | gridZ = -1) 135 | targetHex = HexagonStub( 136 | gridX = 5, 137 | gridY = -4, 138 | gridZ = -1) 139 | 140 | val resultOpt = target.rotateHexagon(originalHex, targetHex, RIGHT) 141 | 142 | val result = resultOpt.get() 143 | 144 | assertEquals(3, result.gridX) 145 | assertEquals(-4, result.gridY) 146 | assertEquals(1, result.gridZ) 147 | } 148 | 149 | @Test 150 | fun shouldProperlyCalculateRotationLeftWhenGivenAValidGrid() { 151 | originalHex = HexagonStub( 152 | gridX = 5, 153 | gridY = -4, 154 | gridZ = -1) 155 | targetHex = HexagonStub( 156 | gridX = 3, 157 | gridY = -2, 158 | gridZ = -1) 159 | 160 | val resultOpt = target.rotateHexagon(originalHex, targetHex, LEFT) 161 | 162 | val result = resultOpt.get() 163 | 164 | assertEquals(3, result.gridX) 165 | assertEquals(-4, result.gridY) 166 | assertEquals(1, result.gridZ) 167 | } 168 | 169 | @Test 170 | fun shouldProperlyCalculateRingWhenGivenValidParameters() { 171 | targetHex = HexagonStub( 172 | gridX = 4, 173 | gridY = 0, 174 | gridZ = 4) 175 | 176 | val expected = HashSet>() 177 | 178 | expected.add(grid.getByCubeCoordinate(fromCoordinates(2, 7)).get()) 179 | expected.add(grid.getByCubeCoordinate(fromCoordinates(3, 7)).get()) 180 | expected.add(grid.getByCubeCoordinate(fromCoordinates(4, 7)).get()) 181 | 182 | expected.add(grid.getByCubeCoordinate(fromCoordinates(5, 6)).get()) 183 | expected.add(grid.getByCubeCoordinate(fromCoordinates(6, 5)).get()) 184 | expected.add(grid.getByCubeCoordinate(fromCoordinates(7, 4)).get()) 185 | 186 | expected.add(grid.getByCubeCoordinate(fromCoordinates(7, 3)).get()) 187 | expected.add(grid.getByCubeCoordinate(fromCoordinates(7, 2)).get()) 188 | expected.add(grid.getByCubeCoordinate(fromCoordinates(7, 1)).get()) 189 | 190 | expected.add(grid.getByCubeCoordinate(fromCoordinates(6, 1)).get()) 191 | expected.add(grid.getByCubeCoordinate(fromCoordinates(5, 1)).get()) 192 | expected.add(grid.getByCubeCoordinate(fromCoordinates(4, 1)).get()) 193 | 194 | expected.add(grid.getByCubeCoordinate(fromCoordinates(3, 2)).get()) 195 | expected.add(grid.getByCubeCoordinate(fromCoordinates(2, 3)).get()) 196 | expected.add(grid.getByCubeCoordinate(fromCoordinates(1, 4)).get()) 197 | 198 | expected.add(grid.getByCubeCoordinate(fromCoordinates(1, 5)).get()) 199 | expected.add(grid.getByCubeCoordinate(fromCoordinates(1, 6)).get()) 200 | expected.add(grid.getByCubeCoordinate(fromCoordinates(1, 7)).get()) 201 | 202 | val actual = target.calculateRingFrom(targetHex, 3) 203 | 204 | assertEquals(expected, actual) 205 | } 206 | 207 | @Test 208 | fun shouldProperlyCalculateRingWhenNearAnEdge() { 209 | targetHex = HexagonStub( 210 | gridX = 9, 211 | gridY = -9, 212 | gridZ = 0) 213 | 214 | val expected = HashSet>() 215 | expected.add(grid.getByCubeCoordinate(fromCoordinates(8, 0)).get()) 216 | expected.add(grid.getByCubeCoordinate(fromCoordinates(8, 1)).get()) 217 | expected.add(grid.getByCubeCoordinate(fromCoordinates(9, 1)).get()) 218 | 219 | val actual = target.calculateRingFrom(targetHex, 1) 220 | assertEquals(expected, actual) 221 | 222 | 223 | 224 | targetHex = HexagonStub( 225 | gridX = 0, 226 | gridY = 0, 227 | gridZ = 0) 228 | 229 | expected.clear() 230 | expected.add(grid.getByCubeCoordinate(fromCoordinates(1, 0)).get()) 231 | expected.add(grid.getByCubeCoordinate(fromCoordinates(0, 1)).get()) 232 | 233 | val newActual = target.calculateRingFrom(targetHex, 1) 234 | assertEquals(expected, newActual) 235 | } 236 | 237 | @Test 238 | fun shouldProperlyCalculateRingWhenCenterOffEdge() { 239 | targetHex = HexagonStub( 240 | gridX = 0, 241 | gridY = 1, 242 | gridZ = -1 243 | ) 244 | 245 | val expected = HashSet>() 246 | expected.add(grid.getByCubeCoordinate(fromCoordinates(2, 0)).get()) 247 | expected.add(grid.getByCubeCoordinate(fromCoordinates(1, 1)).get()) 248 | expected.add(grid.getByCubeCoordinate(fromCoordinates(0, 2)).get()) 249 | expected.add(grid.getByCubeCoordinate(fromCoordinates(-1, 2)).get()) 250 | 251 | val actual = target.calculateRingFrom(targetHex, 3) 252 | 253 | assertEquals(expected, actual) 254 | } 255 | 256 | @Test 257 | fun shouldProperlyCalculateRingAtRadiusOne() { 258 | targetHex = HexagonStub( 259 | gridX = 4, 260 | gridY = 4, 261 | gridZ = 4) 262 | 263 | val expected = 6 264 | 265 | val actual = target.calculateRingFrom(targetHex, 1).size 266 | 267 | assertEquals(expected, actual) 268 | } 269 | 270 | } 271 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------