├── grid.png ├── Sources └── HexGrid │ ├── CustomErrors │ └── InvalidArgumentError.swift │ ├── Enumerations │ ├── RotationEnumeration.swift │ ├── OrientationEnumeration.swift │ ├── OffsetLayoutEnumeration.swift │ ├── DirectionsEnumeration.swift │ └── GridShapeEnumeration.swift │ ├── DataStructures │ ├── Point.swift │ ├── HexSize.swift │ ├── Node.swift │ ├── Coordinates │ │ ├── CubeFractionalCoordinates.swift │ │ ├── AxialCoordinates.swift │ │ ├── OffsetCoordinates.swift │ │ └── CubeCoordinates.swift │ ├── PriorityQueue.swift │ ├── Attributes │ │ ├── Attribute.swift │ │ ├── AttributeDecodable.swift │ │ └── AttributeEncodable.swift │ └── Heap.swift │ ├── OrientationMatrix.swift │ ├── Cell.swift │ ├── Convertor.swift │ ├── Generator.swift │ ├── Math.swift │ └── HexGrid.swift ├── .gitignore ├── .github ├── workflows │ ├── swift.yml │ └── documentation.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── Tests └── HexGridTests │ ├── XCTestManifests.swift │ ├── CodableTests.swift │ ├── CellTests.swift │ ├── AttributeCodableTests.swift │ ├── PriorityQueueTests.swift │ ├── CoordinatesTests.swift │ ├── GridGeneratorTests.swift │ ├── ConvertorTests.swift │ ├── HeapTests.swift │ └── MathTests.swift ├── Package.swift ├── LICENSE.md └── README.md /grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fananek/hex-grid/HEAD/grid.png -------------------------------------------------------------------------------- /Sources/HexGrid/CustomErrors/InvalidArgumentError.swift: -------------------------------------------------------------------------------- 1 | public struct InvalidArgumentsError: Error { 2 | var message: String 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.swiftpm 3 | /.build 4 | /Packages 5 | /*.xcodeproj 6 | Package.pins 7 | Package.resolved 8 | DerivedData 9 | xcuserdata/ 10 | -------------------------------------------------------------------------------- /Sources/HexGrid/Enumerations/RotationEnumeration.swift: -------------------------------------------------------------------------------- 1 | /// Rotation Enumeration for hexagonal grid shapes 2 | public enum Rotation: String, Codable { 3 | case left 4 | case right 5 | } 6 | -------------------------------------------------------------------------------- /Sources/HexGrid/Enumerations/OrientationEnumeration.swift: -------------------------------------------------------------------------------- 1 | /// Enumeration for hexagon orientation 2 | /// Options are either `pointyOnTop` or `flatOnTop`. 3 | public enum Orientation: String, Codable { 4 | /// ⬢ - pointy side on top of a hexagon 5 | case pointyOnTop 6 | /// ⬣ - flat side on top of a hexagon 7 | case flatOnTop 8 | } 9 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Point.swift: -------------------------------------------------------------------------------- 1 | /// Basic structure containing an `x` and `y` value. 2 | public struct Point: Codable, Equatable { 3 | public var x, y: Double 4 | 5 | /// Initializing the `Point`. 6 | /// - Parameters: 7 | /// - x: horizontal coordinate value 8 | /// - y: vertical coordinate value 9 | public init (x: Double, y: Double) { 10 | self.x = x 11 | self.y = y 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/HexSize.swift: -------------------------------------------------------------------------------- 1 | /// Basic structure holding a `width` and `height`. 2 | public struct HexSize: Codable, Equatable { 3 | public var width, height: Double 4 | 5 | /// Width and Height of a hex 6 | /// - Parameters: 7 | /// - width: width of a hex 8 | /// - height: height of a hex 9 | /// - Note: 10 | /// Using different width and height you can generate various aspect ratios. 11 | public init (width: Double, height: Double) { 12 | self.width = width 13 | self.height = height 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Install Swift 14 | uses: slashmo/install-swift@v0.4.0 15 | with: 16 | version: 5.6 17 | 18 | - uses: actions/checkout@v1 19 | - name: Build 20 | run: swift build -v 21 | 22 | - name: Run tests 23 | run: swift test -v --enable-test-discovery --sanitize=thread 24 | 25 | # - name: Test coverage 26 | # uses: maxep/spm-lcov-action@0.3.0 27 | -------------------------------------------------------------------------------- /Tests/HexGridTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(HexGridTests.allTests), 7 | testCase(GridGeneratorTests.allTests), 8 | testCase(ConvertorTests.allTests), 9 | testCase(CoordinatesTests.allTests), 10 | testCase(MathTests.allTests), 11 | testCase(CellTests.allTests), 12 | testCase(PriorityQueueTests.allTests), 13 | testCase(HeapTests.allTests), 14 | testCase(CodableTests.allTests), 15 | testCase(AttributeCodableTests.allTests) 16 | ] 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "hex-grid", 6 | products: [ 7 | .library( 8 | name: "HexGrid", 9 | targets: ["HexGrid"]) 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0") 13 | ], 14 | targets: [ 15 | .target( 16 | name: "HexGrid", 17 | dependencies: []), 18 | .testTarget( 19 | name: "HexGridTests", 20 | dependencies: [ 21 | .target(name: "HexGrid") 22 | ]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Sources/HexGrid/Enumerations/OffsetLayoutEnumeration.swift: -------------------------------------------------------------------------------- 1 | /// Enumeration for hexagonal grid offset layout 2 | /** 3 | There are four offset types depending on orientation of hexagons. 4 | The “row” types are used with with pointy top hexagons and the “column” types are used with flat top. 5 | 6 | See `OrientationEnumeration` 7 | 8 | - Pointy on top orientation 9 | - odd-row (slide alternate rows right) 10 | - even-row (slide alternate rows left) 11 | - Flat on top orientation 12 | - odd-column (slide alternate columns up) 13 | - even-column (slide alternate columns down) 14 | */ 15 | public enum OffsetLayout: Int, Codable { 16 | case even = 1 17 | case odd = -1 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Technical details (please complete the following information):** 27 | - OS: [e.g. MacOS] 28 | - Version [e.g. 11.1] 29 | - HexGrid version [e.g. 0.4.1] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Node.swift: -------------------------------------------------------------------------------- 1 | internal class Node { 2 | let coordinates: T 3 | let parent: Node? 4 | let costScore: Double 5 | let heuristicScore: Double 6 | var totalScore: Double { 7 | return costScore + heuristicScore 8 | } 9 | 10 | init(coordinates: T, parent: Node?, costScore: Double, heuristicScore: Double) { 11 | self.coordinates = coordinates 12 | self.parent = parent 13 | self.costScore = costScore 14 | self.heuristicScore = heuristicScore 15 | } 16 | } 17 | 18 | extension Node: Comparable { 19 | static func == (lhs: Node, rhs: Node) -> Bool { 20 | return lhs === rhs 21 | } 22 | 23 | static func < (lhs: Node, rhs: Node) -> Bool { 24 | return (lhs.totalScore) < (rhs.totalScore) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Coordinates/CubeFractionalCoordinates.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents coordinates in Cube format using x, y and z axis 4 | public struct CubeFractionalCoordinates: Hashable { 5 | public let x, y, z: Double 6 | 7 | /// Basic Initializer 8 | /// 9 | /// - parameters: 10 | /// - x: Value of x axis. 11 | /// - y: Value of y axis. 12 | /// - z: Value of z axis. 13 | /// - throws: `InvalidArgumentsError` in case sum of coordinates is not equal to zero. 14 | public init(x: Double, y: Double, z: Double) throws { 15 | self.x = x 16 | self.y = y 17 | self.z = z 18 | 19 | if (round(self.x + self.y + self.z)) != 0 { 20 | throw InvalidArgumentsError(message: "Sum of coordinate values is not equal to zero.") 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/HexGridTests/CodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HexGrid 3 | 4 | class CodableTests: XCTestCase { 5 | 6 | func testEncode() { 7 | do { 8 | let grid = HexGrid(shape: GridShape.hexagon(1) ) 9 | let encoder = JSONEncoder() 10 | let data = try encoder.encode(grid) 11 | 12 | // if let JSONString = String(data: data, encoding: String.Encoding.utf8) { 13 | // print(JSONString) 14 | // } 15 | 16 | let decoder = JSONDecoder() 17 | let secondGrid = try decoder.decode(HexGrid.self, from: data) 18 | XCTAssertEqual(secondGrid.cells, grid.cells) 19 | 20 | } catch { 21 | print("An error occured during JSON endocing/decoding: \(error)") 22 | } 23 | } 24 | 25 | static var allTests = [ 26 | ("Test encode/decode grid to/from JSON", testEncode) 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 František Mikš 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/HexGrid/Enumerations/DirectionsEnumeration.swift: -------------------------------------------------------------------------------- 1 | /// Enumeration for direction 2 | public enum Direction { 3 | /// ⬣ - flat side on top of a hexagon 4 | case flat(Flat) 5 | 6 | /// ⬢ - pointy side on top of a hexagon 7 | case pointy(Pointy) 8 | 9 | 10 | /// ⬣ - flat side on top of a hexagon 11 | /// individual cases correspond to common cardinal directions 12 | /// (`east` and `west` directions are not available for **flat on top** orientation) 13 | public enum Flat: Int { 14 | case north = 5 15 | case northEast = 0 16 | case southEast = 1 17 | case south = 2 18 | case southWest = 3 19 | case northWest = 4 20 | } 21 | 22 | /// ⬢ - pointy side on top of a hexagon 23 | /// individual cases correspond to common cardinal directions 24 | /// (`north` and `south` directions are not available for **pointy on top** orientation) 25 | public enum Pointy: Int { 26 | case northEast = 0 27 | case east = 1 28 | case southEast = 2 29 | case southWest = 3 30 | case west = 4 31 | case northWest = 5 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/HexGrid/OrientationMatrix.swift: -------------------------------------------------------------------------------- 1 | internal struct OrientationMatrix { 2 | let f00, f10, f01, f11: Double 3 | let b00, b10, b01, b11: Double 4 | 5 | // Pointy on top hexagons starts at 30° and flat on top starts at 0° 6 | let startAngle: Double 7 | 8 | init (orientation: Orientation) { 9 | switch orientation { 10 | case Orientation.pointyOnTop: 11 | self.f00 = (3.0).squareRoot() 12 | self.f10 = (3.0).squareRoot()/2 13 | self.f01 = 0.0 14 | self.f11 = 3.0/2.0 15 | self.b00 = (3.0).squareRoot() / 3.0 16 | self.b10 = -1.0/3.0 17 | self.b01 = 0.0 18 | self.b11 = 2.0/3.0 19 | self.startAngle = 0.5 20 | case Orientation.flatOnTop: 21 | self.f00 = 3.0 / 2.0 22 | self.f10 = 0.0 23 | self.f01 = (3.0).squareRoot() / 2.0 24 | self.f11 = (3.0).squareRoot() 25 | self.b00 = 2.0 / 3.0 26 | self.b10 = 0.0 27 | self.b01 = -1.0/3.0 28 | self.b11 = (3.0).squareRoot() / 3.0 29 | self.startAngle = 0.0 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Coordinates/AxialCoordinates.swift: -------------------------------------------------------------------------------- 1 | /// Represents coordinates in Axial format using q, r axis. 2 | /// q - you can imagine as a column 3 | /// r - you can imagine as a row 4 | /// 5 | /// Axial coordinates are used mainly for storage purposes. 6 | /// Use it carefully because it can't be validated like Cube coordinates. 7 | public struct AxialCoordinates: Codable { 8 | public let q, r: Int 9 | 10 | /// Basic Initializer 11 | /// 12 | /// - parameters: 13 | /// - q: Value of q axis (column). 14 | /// - r: Value of r axis (row). 15 | public init(q: Int, r: Int) { 16 | self.q = q 17 | self.r = r 18 | } 19 | } 20 | 21 | // conversion functions 22 | extension AxialCoordinates { 23 | 24 | /// Converts coordinates to cube system 25 | /// - throws: `InvalidArgumentsError` in case sum of result coordinates is not equal to zero. 26 | public func toCube() throws -> CubeCoordinates { 27 | return try Convertor.axialToCube(from: self) 28 | } 29 | } 30 | 31 | // Helpers 32 | extension AxialCoordinates: Equatable { 33 | public static func ==(lhs: AxialCoordinates, rhs: AxialCoordinates) -> Bool { 34 | return lhs.q == rhs.q && lhs.r == rhs.r 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/HexGrid/Enumerations/GridShapeEnumeration.swift: -------------------------------------------------------------------------------- 1 | /// Enumeration for built-in types of hexagonal grid shapes. 2 | /// 3 | /// Options are: `hexagon`, `rectangle`, and `triangle`. 4 | /// Each option has stored properties relevant to that shape. 5 | public enum GridShape { 6 | 7 | /// Hexagonal `GridShape`. Stored property corresponds to side-length. 8 | case hexagon(Int) 9 | 10 | /// Elongated hexagon `GridShape`. Stored properties correspond to side-lengths. 11 | /// Note that if the side lengths are the same, this falls back to the default hexagon shape. 12 | case elongatedHexagon(Int, Int) 13 | 14 | /// Irregular hexagon `GridShape`. Stored properties correspond to side-lengths. 15 | /// Note that if the side lengths are the same, this falls back to the default hexagon shape. 16 | case irregularHexagon(Int, Int) 17 | 18 | /// Rectangular `GridShape` that does not attempt to be square. Stored properties are for width and height. 19 | case parallelogram(Int, Int) 20 | 21 | /// Rectangular `GridShape` that attempts to create a more or less square shape. Stored properties are for width and height. 22 | case rectangle(Int, Int) 23 | 24 | /// Triangular `GridShape`. Stored property is side-length. 25 | case triangle(Int) 26 | } 27 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Coordinates/OffsetCoordinates.swift: -------------------------------------------------------------------------------- 1 | /// Represents coordinates in Offset format using column and row. 2 | /// 3 | /// Offset coordinates, usually used for rendering and UI purposes. 4 | public struct OffsetCoordinates { 5 | public let column, row: Int 6 | public let orientation: Orientation 7 | public let offsetLayout: OffsetLayout 8 | 9 | /// Basic Initializer 10 | /// 11 | /// - parameters: 12 | /// - column: Value of horizontal axis. 13 | /// - row: Value of vertical axis. 14 | /// - orientation: See `OrientationEnumeration` options 15 | /// - offsetLayout: See `OffsetLayoutEnumeration` options 16 | public init(column: Int, row: Int, orientation: Orientation, offsetLayout: OffsetLayout) { 17 | self.column = column 18 | self.row = row 19 | self.orientation = orientation 20 | self.offsetLayout = offsetLayout 21 | } 22 | } 23 | 24 | // conversion functions 25 | extension OffsetCoordinates { 26 | 27 | /// Converts coordinates to cube system 28 | /// - throws: `InvalidArgumentsError` in case sum of result coordinates is not equal to zero. 29 | public func toCube () throws -> CubeCoordinates { 30 | return try Convertor.offsetToCube(from: self) 31 | } 32 | } 33 | 34 | // Helpers 35 | extension OffsetCoordinates: Equatable { 36 | public static func ==(lhs: OffsetCoordinates, rhs: OffsetCoordinates) -> Bool { 37 | return lhs.column == rhs.column && lhs.row == rhs.row 38 | && lhs.orientation == rhs.orientation && lhs.offsetLayout == rhs.offsetLayout 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/documentation.yml 2 | name: Documentation 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow one concurrent deployment 17 | concurrency: 18 | group: "pages" 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | docs: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Set up Pages 28 | uses: actions/configure-pages@v3 29 | - name: Set up Swift 30 | uses: swift-actions/setup-swift@v1.22.0 31 | with: 32 | swift-version: '5.7.3' 33 | - name: Generate Docs 34 | uses: fwcd/swift-docc-action@v1 35 | with: 36 | target: HexGrid 37 | output: ./public 38 | transform-for-static-hosting: 'true' 39 | disable-indexing: 'true' 40 | hosting-base-path: hex-grid 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v1 43 | with: 44 | path: ./public 45 | 46 | deploy: 47 | environment: 48 | name: github-pages 49 | url: ${{ steps.deployment.outputs.page_url }} 50 | runs-on: ubuntu-latest 51 | needs: docs 52 | 53 | steps: 54 | - name: Deploy Docs 55 | uses: actions/deploy-pages@v1 56 | 57 | # name: Documentation 58 | 59 | # on: 60 | # push: 61 | # branches: 62 | # - main 63 | 64 | # jobs: 65 | # build: 66 | # runs-on: ubuntu-latest 67 | 68 | # steps: 69 | # - uses: actions/checkout@v2 70 | # - name: Generate Documentation 71 | # uses: SwiftDocOrg/swift-doc@master 72 | # with: 73 | # inputs: "Sources" 74 | # module-name: hex-grid 75 | # format: "html" 76 | # base-url: "https://fananek.github.io/hex-grid/" 77 | # github_token: ${{ secrets.GITHUB_TOKEN }} 78 | # output: "Documentation" 79 | # - name: Update Permissions 80 | # run: 'sudo chown --recursive $USER Documentation' 81 | # - name: Deploy 82 | # uses: peaceiris/actions-gh-pages@v3 83 | # with: 84 | # github_token: ${{ secrets.GITHUB_TOKEN }} 85 | # publish_dir: "Documentation" 86 | -------------------------------------------------------------------------------- /Tests/HexGridTests/CellTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HexGrid 3 | 4 | class CellTests: XCTestCase { 5 | 6 | /// create valid cell 7 | func testCreateCell() throws { 8 | let cell = try Cell(CubeCoordinates(x: 1, y: -1, z: 0)) 9 | XCTAssertEqual(cell.coordinates.x, 1) 10 | XCTAssertEqual(cell.coordinates.y, -1) 11 | XCTAssertEqual(cell.coordinates.z, 0) 12 | } 13 | 14 | /// Rotate cell left 15 | func testRotateLeft() throws { 16 | let cell = try Cell(CubeCoordinates(x: 1, y: -1, z: 0)) 17 | try cell.rotate(Rotation.left) 18 | XCTAssertEqual(cell.coordinates.x, 1) 19 | XCTAssertEqual(cell.coordinates.y, 0) 20 | XCTAssertEqual(cell.coordinates.z, -1) 21 | } 22 | 23 | /// Rotate cell right 24 | func testRotateRight() throws { 25 | let cell = try Cell(CubeCoordinates(x: 1, y: -1, z: 0)) 26 | try cell.rotate(Rotation.right) 27 | XCTAssertEqual(cell.coordinates.x, 0) 28 | XCTAssertEqual(cell.coordinates.y, -1) 29 | XCTAssertEqual(cell.coordinates.z, 1) 30 | } 31 | 32 | func testDistanceForCoordinates() throws { 33 | let origin = try Cell(CubeCoordinates(x: 0, y: 0, z: 0)) 34 | let target = try CubeCoordinates(x: 2, y: 0, z: -2) 35 | XCTAssertEqual(try origin.distance(to: target), 2) 36 | } 37 | 38 | func testDistanceForCell() throws { 39 | let origin = try Cell(CubeCoordinates(x: 0, y: 0, z: 0)) 40 | let target = try Cell(CubeCoordinates(x: 2, y: 0, z: -2)) 41 | XCTAssertEqual(try origin.distance(to: target), 2) 42 | } 43 | 44 | func testCellAttributes() throws { 45 | let cell = try Cell(CubeCoordinates(x: 0, y: 0, z: 0)) 46 | cell.attributes["isVisible"] = true 47 | cell.attributes["customValue"] = 20 48 | cell.attributes["title"] = "Cell Title" 49 | 50 | XCTAssertEqual(cell.attributes["isVisible"], true) 51 | XCTAssertEqual(cell.attributes["customValue"], 20) 52 | XCTAssertEqual(cell.attributes["title"], "Cell Title") 53 | 54 | // assign new title 55 | cell.attributes["title"] = "New Title" 56 | XCTAssertEqual(cell.attributes["title"], "New Title") 57 | } 58 | 59 | static var allTests = [ 60 | ("Test Create cell", testCreateCell), 61 | ("Test Rotate cell left", testRotateLeft), 62 | ("Test Rotate cell right", testRotateRight), 63 | ("Test Distance for coordinates", testDistanceForCoordinates), 64 | ("Test Distance for cell", testDistanceForCell), 65 | ("Test cell attributes", testCellAttributes) 66 | ] 67 | } 68 | 69 | -------------------------------------------------------------------------------- /Sources/HexGrid/Cell.swift: -------------------------------------------------------------------------------- 1 | /// Represents single position in a grid 2 | public class Cell: Codable { 3 | public var coordinates: CubeCoordinates 4 | public var isBlocked: Bool 5 | public var isOpaque: Bool 6 | public var cost: Double 7 | public var attributes: [String: Attribute] 8 | 9 | /// Basic Initializer 10 | /// 11 | /// - parameters: 12 | /// - coordinates: Cube coordinates of a cell 13 | /// - isBlocked: Bool value specipyfing whether cell is blocked or not 14 | /// - cost: Cost of passing the cell (used in pathfinding). 15 | public init(_ coordinates: CubeCoordinates, isBlocked: Bool = false, isOpaque: Bool = false, cost: Double = 0, attributes: [String: Attribute] = [String: Attribute]()) { 16 | self.coordinates = coordinates 17 | self.isBlocked = isBlocked 18 | self.isOpaque = isOpaque 19 | self.cost = cost 20 | self.attributes = attributes 21 | } 22 | 23 | /// Rotate cell coordinates 24 | /// 25 | /// - parameters: 26 | /// - side: Rotation enumeration (left or right) 27 | /// - throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 28 | public func rotate(_ side: Rotation) throws { 29 | switch side { 30 | case Rotation.left: 31 | self.coordinates = try Math.rotateLeft(coordinates: self.coordinates) 32 | case Rotation.right: 33 | self.coordinates = try Math.rotateRight(coordinates: self.coordinates) 34 | } 35 | } 36 | 37 | /// Manhattan distance to coordinates 38 | /// 39 | /// - parameters: 40 | /// - coordinates: Target coordinates 41 | /// - returns: Distance from current cell to target coordinates 42 | /// - throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 43 | public func distance(to coordinates: CubeCoordinates) throws -> Int { 44 | return try Math.distance(from: self.coordinates, to: coordinates) 45 | } 46 | 47 | /// Manhattan distance to cell 48 | /// 49 | /// - parameters: 50 | /// - cell: Target cell 51 | /// - returns: Distance from current cell to target cell 52 | /// - throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 53 | public func distance(to cell: Cell) throws -> Int { 54 | return try Math.distance(from: self.coordinates, to: cell.coordinates) 55 | } 56 | } 57 | 58 | extension Cell: Hashable { 59 | public func hash(into hasher: inout Hasher) { 60 | hasher.combine(coordinates) 61 | hasher.combine(cost) 62 | } 63 | } 64 | 65 | extension Cell: Equatable { 66 | public static func == (lhs: Cell, rhs: Cell) -> Bool { 67 | return lhs.coordinates == rhs.coordinates 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/PriorityQueue.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2016 Matthijs Hollemans and contributors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | 23 | Priority Queue, a queue where the most "important" items are at the front of 24 | the queue. 25 | 26 | The heap is a natural data structure for a priority queue, so this object 27 | simply wraps the Heap struct. 28 | 29 | All operations are O(lg n). 30 | 31 | Just like a heap can be a max-heap or min-heap, the queue can be a max-priority 32 | queue (largest element first) or a min-priority queue (smallest element first). 33 | */ 34 | internal struct PriorityQueue { 35 | fileprivate var heap: Heap 36 | 37 | /* 38 | To create a max-priority queue, supply a > sort function. For a min-priority 39 | queue, use <. 40 | */ 41 | init(sort: @escaping (T, T) -> Bool) { 42 | heap = Heap(sort: sort) 43 | } 44 | 45 | var isEmpty: Bool { 46 | return heap.isEmpty 47 | } 48 | 49 | var count: Int { 50 | return heap.count 51 | } 52 | 53 | func peek() -> T? { 54 | return heap.peek() 55 | } 56 | 57 | mutating func enqueue(_ element: T) { 58 | heap.insert(element) 59 | } 60 | 61 | mutating func dequeue() -> T? { 62 | return heap.remove() 63 | } 64 | 65 | /* 66 | Allows you to change the priority of an element. In a max-priority queue, 67 | the new priority should be larger than the old one; in a min-priority queue 68 | it should be smaller. 69 | */ 70 | mutating func changePriority(index ind: Int, value: T) { 71 | return heap.replace(index: ind, value: value) 72 | } 73 | } 74 | 75 | extension PriorityQueue where T: Equatable { 76 | func index(of element: T) -> Int? { 77 | return heap.index(of: element) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/HexGridTests/AttributeCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HexGrid 3 | 4 | class AttributeCodableTests: XCTestCase { 5 | 6 | /// test JSON Encoding 7 | func testJSONEncoding() throws { 8 | let dictionary: [String: Attribute] = [ 9 | "boolean": true, 10 | "integer": 10, 11 | "double": 8.23423, 12 | "string": "string value", 13 | "array": [0, 1, 2], 14 | "nested": [ 15 | "first": "fisrt value", 16 | "second": "second value", 17 | "third": "third value" 18 | ], 19 | ] 20 | 21 | let encoder = JSONEncoder() 22 | 23 | let json = try encoder.encode(dictionary) 24 | let encodedJSONObject = try JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary 25 | let expected = """ 26 | { 27 | "boolean": true, 28 | "integer": 10, 29 | "double": 8.23423, 30 | "string": "string value", 31 | "array": [0, 1, 2], 32 | "nested": { 33 | "first": "fisrt value", 34 | "second": "second value", 35 | "third": "third value" 36 | } 37 | } 38 | """.data(using: .utf8)! 39 | let expectedJSONObject = try JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary 40 | 41 | XCTAssertEqual(encodedJSONObject, expectedJSONObject) 42 | } 43 | 44 | /// test JSON Decoding 45 | func testJSONDecoding() throws { 46 | let json = """ 47 | { 48 | "boolean": true, 49 | "integer": 10, 50 | "double": 8.23423, 51 | "string": "string value", 52 | "array": [0, 1, 2], 53 | "nested": { 54 | "first": "fisrt value", 55 | "second": "second value", 56 | "third": "third value" 57 | } 58 | } 59 | """.data(using: .utf8)! 60 | 61 | let decoder = JSONDecoder() 62 | let dictionary = try decoder.decode([String: Attribute].self, from: json) 63 | 64 | XCTAssertEqual(dictionary["boolean"]?.value as! Bool, true) 65 | XCTAssertEqual(dictionary["integer"]?.value as! Int, 10) 66 | XCTAssertEqual(dictionary["double"]?.value as! Double, 8.23423, accuracy: 0.001) 67 | XCTAssertEqual(dictionary["string"]?.value as! String, "string value") 68 | XCTAssertEqual(dictionary["array"]?.value as! [Int], [0, 1, 2]) 69 | XCTAssertEqual(dictionary["nested"]?.value as! [String: String], 70 | ["first": "fisrt value", 71 | "second": "second value", 72 | "third": "third value"]) 73 | } 74 | 75 | static var allTests = [ 76 | ("Test JSON Encoding", testJSONEncoding), 77 | ("Test JSON Decoding", testJSONDecoding) 78 | ] 79 | } 80 | 81 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Coordinates/CubeCoordinates.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents coordinates in Cube format using x, y and z axis 4 | public struct CubeCoordinates: Hashable, Codable { 5 | public let x, y, z: Int 6 | 7 | /// Basic Initializer 8 | /// 9 | /// - parameters: 10 | /// - x: Value of x axis. 11 | /// - y: Value of y axis. 12 | /// - z: Value of z axis. 13 | /// - throws: `InvalidArgumentsError` in case sum of coordinates is not equal to zero. 14 | public init(x: Int, y: Int, z: Int) throws { 15 | self.x = x 16 | self.y = y 17 | self.z = z 18 | 19 | if (self.x + self.y + self.z) != 0 { 20 | throw InvalidArgumentsError(message: "Sum of coordinate values is not equal to zero.") 21 | } 22 | } 23 | 24 | /// Initializer for Double type 25 | /// 26 | /// - parameters: 27 | /// - x: Value of x axis. 28 | /// - y: Value of y axis. 29 | /// - z: Value of z axis. 30 | /// - note: 31 | /// There is an algorithm in place which make sure that rule `x + y + z = 0`is not violated 32 | public init(x: Double, y: Double, z: Double) { 33 | var xInt = round(x) 34 | var yInt = round(y) 35 | var zInt = round(z) 36 | let xDiff = abs(xInt - x) 37 | let yDiff = abs(yInt - y) 38 | let zDiff = abs(zInt - z) 39 | if xDiff > yDiff && xDiff > zDiff { 40 | xInt = -yInt - zInt 41 | } else if yDiff > zDiff { 42 | yInt = -xInt - zInt 43 | } else { 44 | zInt = -xInt - yInt 45 | } 46 | self.x = Int(xInt.rounded()) 47 | self.y = Int(yInt.rounded()) 48 | self.z = Int(zInt.rounded()) 49 | } 50 | } 51 | 52 | // conversion functions 53 | extension CubeCoordinates { 54 | 55 | /// Converts coordinates to axial system 56 | public func toAxial () -> AxialCoordinates { 57 | return Convertor.cubeToAxial(from: self) 58 | } 59 | 60 | /// Converts coordinates to offset system 61 | /// - parameters: 62 | /// - orientation: See `OrientationEnumeration` options 63 | /// - offsetLayout: See `OffsetLayoutEnumeration` options 64 | public func toOffset (orientation: Orientation, offsetLayout: OffsetLayout) -> OffsetCoordinates { 65 | return Convertor.cubeToOffset(from: self, orientation: orientation, offsetLayout: offsetLayout) 66 | } 67 | 68 | /// Converts coordinates to pixel coordinates 69 | /// - parameters: 70 | /// - orientation: See `OrientationEnumeration` options 71 | /// - hexSize: pixel size of a single hex in x and y axis 72 | /// - origin: pixel coordinates of a grid origin 73 | public func toPixel (orientation: Orientation, hexSize: HexSize, origin: Point) -> Point { 74 | return Convertor.cubeToPixel(from: self, hexSize: hexSize, gridOrigin: origin, orientation: orientation) 75 | } 76 | } 77 | 78 | extension CubeCoordinates: Equatable { 79 | public static func ==(lhs: CubeCoordinates, rhs: CubeCoordinates) -> Bool { 80 | return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Attributes/Attribute.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A type-erased `Codable` value. 5 | 6 | The `Attribute` type forwards encoding and decoding responsibilities 7 | to an underlying value, hiding its specific underlying type. 8 | 9 | You can encode or decode mixed-type values in dictionaries 10 | and other collections that require `Encodable` or `Decodable` conformance 11 | by declaring their contained type to be `Attribute`. 12 | 13 | - SeeAlso: `AttributeEncodable` 14 | - SeeAlso: `AttributeDecodable` 15 | */ 16 | public struct Attribute: Codable { 17 | public let value: Any 18 | 19 | public init(_ value: T?) { 20 | self.value = value ?? () 21 | } 22 | } 23 | 24 | extension Attribute: _AttributeEncodable, _AttributeDecodable {} 25 | 26 | extension Attribute: Equatable { 27 | public static func == (lhs: Attribute, rhs: Attribute) -> Bool { 28 | switch (lhs.value, rhs.value) { 29 | case is (Void, Void): 30 | return true 31 | case let (lhs as Bool, rhs as Bool): 32 | return lhs == rhs 33 | case let (lhs as Int, rhs as Int): 34 | return lhs == rhs 35 | case let (lhs as Int8, rhs as Int8): 36 | return lhs == rhs 37 | case let (lhs as Int16, rhs as Int16): 38 | return lhs == rhs 39 | case let (lhs as Int32, rhs as Int32): 40 | return lhs == rhs 41 | case let (lhs as Int64, rhs as Int64): 42 | return lhs == rhs 43 | case let (lhs as UInt, rhs as UInt): 44 | return lhs == rhs 45 | case let (lhs as UInt8, rhs as UInt8): 46 | return lhs == rhs 47 | case let (lhs as UInt16, rhs as UInt16): 48 | return lhs == rhs 49 | case let (lhs as UInt32, rhs as UInt32): 50 | return lhs == rhs 51 | case let (lhs as UInt64, rhs as UInt64): 52 | return lhs == rhs 53 | case let (lhs as Float, rhs as Float): 54 | return lhs == rhs 55 | case let (lhs as Double, rhs as Double): 56 | return lhs == rhs 57 | case let (lhs as String, rhs as String): 58 | return lhs == rhs 59 | case let (lhs as [String: Attribute], rhs as [String: Attribute]): 60 | return lhs == rhs 61 | case let (lhs as [Attribute], rhs as [Attribute]): 62 | return lhs == rhs 63 | default: 64 | return false 65 | } 66 | } 67 | } 68 | 69 | extension Attribute: CustomStringConvertible { 70 | public var description: String { 71 | switch value { 72 | case is Void: 73 | return String(describing: nil as Any?) 74 | case let value as CustomStringConvertible: 75 | return value.description 76 | default: 77 | return String(describing: value) 78 | } 79 | } 80 | } 81 | 82 | extension Attribute: CustomDebugStringConvertible { 83 | public var debugDescription: String { 84 | switch value { 85 | case let value as CustomDebugStringConvertible: 86 | return "Attribute(\(value.debugDescription))" 87 | default: 88 | return "Attribute(\(description))" 89 | } 90 | } 91 | } 92 | 93 | extension Attribute: ExpressibleByNilLiteral {} 94 | extension Attribute: ExpressibleByBooleanLiteral {} 95 | extension Attribute: ExpressibleByIntegerLiteral {} 96 | extension Attribute: ExpressibleByFloatLiteral {} 97 | extension Attribute: ExpressibleByStringLiteral {} 98 | extension Attribute: ExpressibleByArrayLiteral {} 99 | extension Attribute: ExpressibleByDictionaryLiteral {} 100 | -------------------------------------------------------------------------------- /Tests/HexGridTests/PriorityQueueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HexGrid 3 | 4 | private struct Message: Equatable, Comparable { 5 | let text: String 6 | let priority: Int 7 | } 8 | 9 | private func == (lhs: Message, rhs: Message) -> Bool { 10 | return lhs.text == rhs.text 11 | } 12 | 13 | 14 | private func < (m1: Message, m2: Message) -> Bool { 15 | return m1.priority < m2.priority 16 | } 17 | 18 | class PriorityQueueTests: XCTestCase { 19 | 20 | func testEmpty() { 21 | var queue = PriorityQueue(sort: <) 22 | XCTAssertTrue(queue.isEmpty) 23 | XCTAssertEqual(queue.count, 0) 24 | XCTAssertNil(queue.peek()) 25 | XCTAssertNil(queue.dequeue()) 26 | } 27 | 28 | func testOneElement() { 29 | var queue = PriorityQueue(sort: <) 30 | let message = Message(text: "hello", priority: 100) 31 | queue.enqueue(message) 32 | XCTAssertFalse(queue.isEmpty) 33 | XCTAssertEqual(queue.count, 1) 34 | XCTAssertEqual(queue.peek()!.priority, 100) 35 | XCTAssertEqual(queue.index(of: message), 0) 36 | 37 | let result = queue.dequeue() 38 | XCTAssertEqual(result!.priority, 100) 39 | XCTAssertTrue(queue.isEmpty) 40 | XCTAssertEqual(queue.count, 0) 41 | XCTAssertNil(queue.peek()) 42 | } 43 | 44 | func testTwoElementsInOrder() { 45 | var queue = PriorityQueue(sort: <) 46 | let messageHello = Message(text: "hello", priority: 100) 47 | let messageWorld = Message(text: "world", priority: 50) 48 | let messageWorld2 = Message(text: "world", priority: 200) 49 | queue.enqueue(messageHello) 50 | queue.enqueue(messageWorld) 51 | XCTAssertFalse(queue.isEmpty) 52 | XCTAssertEqual(queue.count, 2) 53 | XCTAssertEqual(queue.peek()!.priority, 50) 54 | 55 | queue.changePriority(index: queue.index(of: messageWorld)!, value: messageWorld2) 56 | 57 | let result1 = queue.dequeue() 58 | XCTAssertEqual(result1!.priority, 100) 59 | XCTAssertFalse(queue.isEmpty) 60 | XCTAssertEqual(queue.count, 1) 61 | XCTAssertEqual(queue.peek()!.priority, 200) 62 | 63 | let result2 = queue.dequeue() 64 | XCTAssertEqual(result2!.priority, 200) 65 | XCTAssertTrue(queue.isEmpty) 66 | XCTAssertEqual(queue.count, 0) 67 | XCTAssertNil(queue.peek()) 68 | } 69 | 70 | func testTwoElementsOutOfOrder() { 71 | var queue = PriorityQueue(sort: <) 72 | 73 | queue.enqueue(Message(text: "world", priority: 200)) 74 | queue.enqueue(Message(text: "hello", priority: 100)) 75 | XCTAssertFalse(queue.isEmpty) 76 | XCTAssertEqual(queue.count, 2) 77 | XCTAssertEqual(queue.peek()!.priority, 100) 78 | 79 | let result1 = queue.dequeue() 80 | XCTAssertEqual(result1!.priority, 100) 81 | XCTAssertFalse(queue.isEmpty) 82 | XCTAssertEqual(queue.count, 1) 83 | XCTAssertEqual(queue.peek()!.priority, 200) 84 | 85 | let result2 = queue.dequeue() 86 | XCTAssertEqual(result2!.priority, 200) 87 | XCTAssertTrue(queue.isEmpty) 88 | XCTAssertEqual(queue.count, 0) 89 | XCTAssertNil(queue.peek()) 90 | } 91 | 92 | func testCompareNodes() throws { 93 | let coords = try CubeCoordinates(x: 0, y: 0, z: 0) 94 | let nodeA = Node(coordinates: coords, parent: nil, costScore: 0.0, heuristicScore: 0.0) 95 | let nodeB = nodeA 96 | XCTAssertEqual(nodeA, nodeB) 97 | } 98 | 99 | static var allTests = [ 100 | ("Test empty - priority queue", testEmpty), 101 | ("Test one element - priority queue", testOneElement), 102 | ("Test two elements in order - priority queue", testTwoElementsInOrder), 103 | ("Test teo element out of order - priority queue", testTwoElementsOutOfOrder), 104 | ("Test == func for nodes", testCompareNodes) 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Attributes/AttributeDecodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type-erased `Decodable` value. 4 | /// 5 | /// The `AttributeDecodable` type forwards decoding responsibilities 6 | /// to an underlying value, hiding its specific underlying type. 7 | /// 8 | /// You can decode mixed-type values in dictionaries 9 | /// and other collections that require `Decodable` conformance 10 | /// by declaring their contained type to be `AttributeDecodable`. 11 | public struct AttributeDecodable: Decodable { 12 | public let value: Any 13 | 14 | public init(_ value: T?) { 15 | self.value = value ?? () 16 | } 17 | } 18 | 19 | @usableFromInline 20 | protocol _AttributeDecodable { 21 | var value: Any { get } 22 | init(_ value: T?) 23 | } 24 | 25 | extension AttributeDecodable: _AttributeDecodable {} 26 | 27 | extension _AttributeDecodable { 28 | public init(from decoder: Decoder) throws { 29 | let container = try decoder.singleValueContainer() 30 | 31 | if container.decodeNil() { 32 | self.init(NSNull()) 33 | } else if let bool = try? container.decode(Bool.self) { 34 | self.init(bool) 35 | } else if let int = try? container.decode(Int.self) { 36 | self.init(int) 37 | } else if let uint = try? container.decode(UInt.self) { 38 | self.init(uint) 39 | } else if let double = try? container.decode(Double.self) { 40 | self.init(double) 41 | } else if let string = try? container.decode(String.self) { 42 | self.init(string) 43 | } else if let array = try? container.decode([Attribute].self) { 44 | self.init(array.map { $0.value }) 45 | } else if let dictionary = try? container.decode([String: Attribute].self) { 46 | self.init(dictionary.mapValues { $0.value }) 47 | } else { 48 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Attribute value cannot be decoded") 49 | } 50 | } 51 | } 52 | 53 | extension AttributeDecodable: Equatable { 54 | public static func == (lhs: AttributeDecodable, rhs: AttributeDecodable) -> Bool { 55 | switch (lhs.value, rhs.value) { 56 | case is (NSNull, NSNull), is (Void, Void): 57 | return true 58 | case let (lhs as Bool, rhs as Bool): 59 | return lhs == rhs 60 | case let (lhs as Int, rhs as Int): 61 | return lhs == rhs 62 | case let (lhs as Int8, rhs as Int8): 63 | return lhs == rhs 64 | case let (lhs as Int16, rhs as Int16): 65 | return lhs == rhs 66 | case let (lhs as Int32, rhs as Int32): 67 | return lhs == rhs 68 | case let (lhs as Int64, rhs as Int64): 69 | return lhs == rhs 70 | case let (lhs as UInt, rhs as UInt): 71 | return lhs == rhs 72 | case let (lhs as UInt8, rhs as UInt8): 73 | return lhs == rhs 74 | case let (lhs as UInt16, rhs as UInt16): 75 | return lhs == rhs 76 | case let (lhs as UInt32, rhs as UInt32): 77 | return lhs == rhs 78 | case let (lhs as UInt64, rhs as UInt64): 79 | return lhs == rhs 80 | case let (lhs as Float, rhs as Float): 81 | return lhs == rhs 82 | case let (lhs as Double, rhs as Double): 83 | return lhs == rhs 84 | case let (lhs as String, rhs as String): 85 | return lhs == rhs 86 | case let (lhs as [String: AttributeDecodable], rhs as [String: AttributeDecodable]): 87 | return lhs == rhs 88 | case let (lhs as [AttributeDecodable], rhs as [AttributeDecodable]): 89 | return lhs == rhs 90 | default: 91 | return false 92 | } 93 | } 94 | } 95 | 96 | extension AttributeDecodable: CustomStringConvertible { 97 | public var description: String { 98 | switch value { 99 | case is Void: 100 | return String(describing: nil as Any?) 101 | case let value as CustomStringConvertible: 102 | return value.description 103 | default: 104 | return String(describing: value) 105 | } 106 | } 107 | } 108 | 109 | extension AttributeDecodable: CustomDebugStringConvertible { 110 | public var debugDescription: String { 111 | switch value { 112 | case let value as CustomDebugStringConvertible: 113 | return "AttributeDecodable(\(value.debugDescription))" 114 | default: 115 | return "AttributeDecodable(\(description))" 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/HexGrid/Convertor.swift: -------------------------------------------------------------------------------- 1 | /// Allows conversion between several coordinate systems. 2 | /// 3 | /// While cube coordinate system is suitable for most of alghoritms, axial 4 | /// or offset coordinate systems are often better for storage or rendering. 5 | internal struct Convertor { 6 | 7 | /// Converts coordinates from `cube` to `offset` 8 | /// 9 | /// - parameters: 10 | /// - fromCoordinates: Original coordinates to be coverted. 11 | /// - orientation: See `OrientationEnumeration` 12 | /// - offsetLayout: See `OffsetLayoutEnumeration` 13 | static func cubeToOffset ( 14 | from: CubeCoordinates, 15 | orientation: Orientation, 16 | offsetLayout: OffsetLayout) -> OffsetCoordinates { 17 | let column, row: Int 18 | 19 | switch orientation { 20 | case Orientation.pointyOnTop: 21 | // using right shift bit >> by 1 = which is basically division by 2 22 | column = from.x + ((from.z + offsetLayout.rawValue * (abs(from.z) & 1)) >> 1) 23 | row = from.z 24 | return OffsetCoordinates(column: column, row: row, orientation: orientation, offsetLayout: offsetLayout) 25 | 26 | case Orientation.flatOnTop: 27 | column = from.x 28 | // using right shift bit >> by 1 = which is basically division by 2 29 | row = from.z + ((from.x + offsetLayout.rawValue * (abs(from.x) & 1)) >> 1) 30 | return OffsetCoordinates(column: column, row: row, orientation: orientation, offsetLayout: offsetLayout) 31 | } 32 | } 33 | 34 | /// Converts coordinates from `offset` to `cube` 35 | /// 36 | /// - parameters: 37 | /// - fromCoordinates: Original coordinates to be coverted. 38 | /// - throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 39 | static func offsetToCube (from: OffsetCoordinates) throws -> CubeCoordinates { 40 | let x, y, z: Int 41 | 42 | switch from.orientation { 43 | case Orientation.pointyOnTop: 44 | // using right shift bit >> by 1 = which is basically division by 2 45 | x = from.column - ((from.row + from.offsetLayout.rawValue * (abs(from.row) & 1)) >> 1) 46 | z = from.row 47 | y = -x - z 48 | return try CubeCoordinates(x: x, y: y, z: z) 49 | 50 | case Orientation.flatOnTop: 51 | x = from.column 52 | // using right shift bit >> by 1 = which is basically division by 2 53 | z = from.row - ((from.column + from.offsetLayout.rawValue * (abs(from.column) & 1)) >> 1) 54 | y = -x - z 55 | return try CubeCoordinates(x: x, y: y, z: z) 56 | } 57 | } 58 | 59 | /// Converts coordinates from `cube` to `axial` 60 | /// 61 | /// - parameters: 62 | /// - fromCoordinates: Original coordinates to be coverted. 63 | static func cubeToAxial (from: CubeCoordinates) -> AxialCoordinates { 64 | return AxialCoordinates(q: from.x, r: from.z) 65 | } 66 | 67 | /// Converts coordinates from `axial` to `cube` 68 | /// 69 | /// - parameters: 70 | /// - fromCoordinates: Original coordinates to be coverted. 71 | /// - throws: `InvalidArgumentsError` in case sum of underlying cube coordinates initializer propagate the error. 72 | static func axialToCube (from: AxialCoordinates) throws -> CubeCoordinates { 73 | return try CubeCoordinates(x: from.q, y: (-from.q - from.r), z: from.r) 74 | } 75 | 76 | static func cubeToPixel( 77 | from coordinates: CubeCoordinates, 78 | hexSize: HexSize, 79 | gridOrigin: Point, 80 | orientation: Orientation) -> Point { 81 | let axialCoords = coordinates.toAxial() 82 | let orientation = OrientationMatrix(orientation: orientation) 83 | let x = ((orientation.f00 * Double(axialCoords.q)) + (orientation.f10 * Double(axialCoords.r))) * hexSize.width 84 | let y = ((orientation.f01 * Double(axialCoords.q)) + (orientation.f11 * Double(axialCoords.r))) * hexSize.height 85 | 86 | return Point(x: x + gridOrigin.x, y: y + gridOrigin.y) 87 | } 88 | 89 | static func pixelToCube( 90 | from point: Point, 91 | hexSize: HexSize, 92 | origin: Point, 93 | orientation: Orientation) -> CubeCoordinates { 94 | let orientation = OrientationMatrix(orientation: orientation) 95 | let point = (x: (point.x - origin.x) / hexSize.width, y: (point.y - origin.y) / hexSize.height) 96 | let x = orientation.b00 * point.x + orientation.b10 * point.y 97 | let y = orientation.b01 * point.x + orientation.b11 * point.y 98 | return CubeCoordinates(x: x, y: -x-y, z: y) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Attributes/AttributeEncodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type-erased `Encodable` value. 4 | /// 5 | /// The `AttributeEncodable` type forwards encoding responsibilities 6 | /// to an underlying value, hiding its specific underlying type. 7 | /// 8 | /// You can encode mixed-type values in dictionaries 9 | /// and other collections that require `Encodable` conformance 10 | /// by declaring their contained type to be `AttributeEncodable`. 11 | public struct AttributeEncodable: Encodable { 12 | public let value: Any 13 | 14 | public init(_ value: T?) { 15 | self.value = value ?? () 16 | } 17 | } 18 | 19 | @usableFromInline 20 | protocol _AttributeEncodable { 21 | var value: Any { get } 22 | init(_ value: T?) 23 | } 24 | 25 | extension AttributeEncodable: _AttributeEncodable {} 26 | 27 | // MARK: - Encodable 28 | 29 | extension _AttributeEncodable { 30 | public func encode(to encoder: Encoder) throws { 31 | var container = encoder.singleValueContainer() 32 | 33 | switch value { 34 | case is NSNull, is Void: 35 | try container.encodeNil() 36 | case let bool as Bool: 37 | try container.encode(bool) 38 | case let int as Int: 39 | try container.encode(int) 40 | case let int8 as Int8: 41 | try container.encode(int8) 42 | case let int16 as Int16: 43 | try container.encode(int16) 44 | case let int32 as Int32: 45 | try container.encode(int32) 46 | case let int64 as Int64: 47 | try container.encode(int64) 48 | case let uint as UInt: 49 | try container.encode(uint) 50 | case let uint8 as UInt8: 51 | try container.encode(uint8) 52 | case let uint16 as UInt16: 53 | try container.encode(uint16) 54 | case let uint32 as UInt32: 55 | try container.encode(uint32) 56 | case let uint64 as UInt64: 57 | try container.encode(uint64) 58 | case let float as Float: 59 | try container.encode(float) 60 | case let double as Double: 61 | try container.encode(double) 62 | case let string as String: 63 | try container.encode(string) 64 | case let date as Date: 65 | try container.encode(date) 66 | case let url as URL: 67 | try container.encode(url) 68 | case let array as [Any?]: 69 | try container.encode(array.map { Attribute($0) }) 70 | case let dictionary as [String: Any?]: 71 | try container.encode(dictionary.mapValues { Attribute($0) }) 72 | default: 73 | let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "Attribute value cannot be encoded") 74 | throw EncodingError.invalidValue(value, context) 75 | } 76 | } 77 | } 78 | 79 | extension AttributeEncodable: Equatable { 80 | public static func == (lhs: AttributeEncodable, rhs: AttributeEncodable) -> Bool { 81 | switch (lhs.value, rhs.value) { 82 | case is (Void, Void): 83 | return true 84 | case let (lhs as Bool, rhs as Bool): 85 | return lhs == rhs 86 | case let (lhs as Int, rhs as Int): 87 | return lhs == rhs 88 | case let (lhs as Int8, rhs as Int8): 89 | return lhs == rhs 90 | case let (lhs as Int16, rhs as Int16): 91 | return lhs == rhs 92 | case let (lhs as Int32, rhs as Int32): 93 | return lhs == rhs 94 | case let (lhs as Int64, rhs as Int64): 95 | return lhs == rhs 96 | case let (lhs as UInt, rhs as UInt): 97 | return lhs == rhs 98 | case let (lhs as UInt8, rhs as UInt8): 99 | return lhs == rhs 100 | case let (lhs as UInt16, rhs as UInt16): 101 | return lhs == rhs 102 | case let (lhs as UInt32, rhs as UInt32): 103 | return lhs == rhs 104 | case let (lhs as UInt64, rhs as UInt64): 105 | return lhs == rhs 106 | case let (lhs as Float, rhs as Float): 107 | return lhs == rhs 108 | case let (lhs as Double, rhs as Double): 109 | return lhs == rhs 110 | case let (lhs as String, rhs as String): 111 | return lhs == rhs 112 | case let (lhs as [String: AttributeEncodable], rhs as [String: AttributeEncodable]): 113 | return lhs == rhs 114 | case let (lhs as [AttributeEncodable], rhs as [AttributeEncodable]): 115 | return lhs == rhs 116 | default: 117 | return false 118 | } 119 | } 120 | } 121 | 122 | extension AttributeEncodable: CustomStringConvertible { 123 | public var description: String { 124 | switch value { 125 | case is Void: 126 | return String(describing: nil as Any?) 127 | case let value as CustomStringConvertible: 128 | return value.description 129 | default: 130 | return String(describing: value) 131 | } 132 | } 133 | } 134 | 135 | extension AttributeEncodable: CustomDebugStringConvertible { 136 | public var debugDescription: String { 137 | switch value { 138 | case let value as CustomDebugStringConvertible: 139 | return "AttributeEncodable(\(value.debugDescription))" 140 | default: 141 | return "AttributeEncodable(\(description))" 142 | } 143 | } 144 | } 145 | 146 | extension AttributeEncodable: ExpressibleByNilLiteral {} 147 | extension AttributeEncodable: ExpressibleByBooleanLiteral {} 148 | extension AttributeEncodable: ExpressibleByIntegerLiteral {} 149 | extension AttributeEncodable: ExpressibleByFloatLiteral {} 150 | extension AttributeEncodable: ExpressibleByStringLiteral {} 151 | extension AttributeEncodable: ExpressibleByArrayLiteral {} 152 | extension AttributeEncodable: ExpressibleByDictionaryLiteral {} 153 | 154 | extension _AttributeEncodable { 155 | public init(nilLiteral _: ()) { 156 | self.init(nil as Any?) 157 | } 158 | 159 | public init(booleanLiteral value: Bool) { 160 | self.init(value) 161 | } 162 | 163 | public init(integerLiteral value: Int) { 164 | self.init(value) 165 | } 166 | 167 | public init(floatLiteral value: Double) { 168 | self.init(value) 169 | } 170 | 171 | public init(extendedGraphemeClusterLiteral value: String) { 172 | self.init(value) 173 | } 174 | 175 | public init(stringLiteral value: String) { 176 | self.init(value) 177 | } 178 | 179 | public init(arrayLiteral elements: Any...) { 180 | self.init(elements) 181 | } 182 | 183 | public init(dictionaryLiteral elements: (AnyHashable, Any)...) { 184 | self.init([AnyHashable: Any](elements, uniquingKeysWith: { first, _ in first })) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Tests/HexGridTests/CoordinatesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HexGrid 3 | 4 | class CoordinatesTests: XCTestCase { 5 | // Initializer tests 6 | /// create valid cube coordinates 7 | func testCreateValidCubeCoordinates() throws { 8 | let cubeCoordinates = try CubeCoordinates(x: 1, y: -1, z: 0) 9 | XCTAssertEqual(cubeCoordinates.x, 1) 10 | XCTAssertEqual(cubeCoordinates.y, -1) 11 | XCTAssertEqual(cubeCoordinates.z, 0) 12 | } 13 | 14 | /// create invalid cube coordinates (sum of x, y and z is not equal to 0) 15 | func testCreateInalidCubeCoordinates() throws { 16 | XCTAssertThrowsError(try CubeCoordinates(x: -1, y: 0, z: 0)) 17 | XCTAssertThrowsError(try CubeCoordinates(x: 0, y: -1, z: 0)) 18 | XCTAssertThrowsError(try CubeCoordinates(x: 0, y: 0, z: -1)) 19 | } 20 | 21 | /// create valid fractional cube coordinates 22 | func testCreateValidFractionalCubeCoordinates() throws { 23 | let coordinates = try CubeFractionalCoordinates(x: 1.0, y: -1.0, z: 0.0) 24 | XCTAssertEqual(coordinates.x, 1.0) 25 | XCTAssertEqual(coordinates.y, -1.0) 26 | XCTAssertEqual(coordinates.z, 0.0) 27 | } 28 | 29 | /// create invalid fractional cube coordinates (sum of x, y and z is not equal to 0) 30 | func testCreateInalidFractionalCubeCoordinates() throws { 31 | XCTAssertThrowsError(try CubeFractionalCoordinates(x: -1.0, y: 0.0, z: 0.0)) 32 | XCTAssertThrowsError(try CubeFractionalCoordinates(x: 0.0, y: -1.0, z: 0.0)) 33 | XCTAssertThrowsError(try CubeFractionalCoordinates(x: 0.0, y: 0.0, z: -1.0)) 34 | } 35 | 36 | /// create axial coordinates 37 | func testCreateAxialCoordinates() throws { 38 | let axialCoordinates = AxialCoordinates(q: 1, r: -1) 39 | XCTAssertEqual(axialCoordinates.q, 1) 40 | XCTAssertEqual(axialCoordinates.r, -1) 41 | } 42 | 43 | /// Create offset coordinates (odd row + pointy top) 44 | func testCreateOddRowOffsetCoordinates() throws { 45 | // ODD_ROW 46 | let oddRowOffsetCoordinates = OffsetCoordinates(column: 0, row: 0, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.odd) 47 | XCTAssertEqual(oddRowOffsetCoordinates.column, 0) 48 | XCTAssertEqual(oddRowOffsetCoordinates.row, 0) 49 | XCTAssertEqual(oddRowOffsetCoordinates.orientation, Orientation.pointyOnTop) 50 | XCTAssertEqual(oddRowOffsetCoordinates.offsetLayout, OffsetLayout.odd) 51 | } 52 | 53 | /// Create offset coordinates (even row + pointy top) 54 | func testCreateEvenRowOffsetCoordinates() throws { 55 | // EVEN_ROW 56 | let evenRowOffsetCoordinates = OffsetCoordinates(column: 0, row: 0, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.even) 57 | XCTAssertEqual(evenRowOffsetCoordinates.column, 0) 58 | XCTAssertEqual(evenRowOffsetCoordinates.row, 0) 59 | XCTAssertEqual(evenRowOffsetCoordinates.orientation, Orientation.pointyOnTop) 60 | XCTAssertEqual(evenRowOffsetCoordinates.offsetLayout, OffsetLayout.even) 61 | } 62 | 63 | /// Create offset coordinates (odd column + flat top) 64 | func testCreateOddColumnOffsetCoordinates() throws { 65 | // ODD_COLUMN 66 | let oddColumnOffsetCoordinates = OffsetCoordinates(column: 0, row: 0, orientation: Orientation.flatOnTop, offsetLayout: OffsetLayout.odd) 67 | XCTAssertEqual(oddColumnOffsetCoordinates.column, 0) 68 | XCTAssertEqual(oddColumnOffsetCoordinates.row, 0) 69 | XCTAssertEqual(oddColumnOffsetCoordinates.orientation, Orientation.flatOnTop) 70 | XCTAssertEqual(oddColumnOffsetCoordinates.offsetLayout, OffsetLayout.odd) 71 | } 72 | 73 | /// Create offset coordinates (even column + flat top) 74 | func testCreateEvenColumnOffsetCoordinates() throws { 75 | // EVEN_COLUMN 76 | let evenColOffsetCoordinates = OffsetCoordinates(column: 0, row: 0, orientation: Orientation.flatOnTop, offsetLayout: OffsetLayout.even) 77 | XCTAssertEqual(evenColOffsetCoordinates.column, 0) 78 | XCTAssertEqual(evenColOffsetCoordinates.row, 0) 79 | XCTAssertEqual(evenColOffsetCoordinates.orientation, Orientation.flatOnTop) 80 | XCTAssertEqual(evenColOffsetCoordinates.offsetLayout, OffsetLayout.even) 81 | } 82 | 83 | /// Test offset coordinates equatablity 84 | func testOffsetCoordinatesEquatability() throws { 85 | let offsetCoordinatesA = OffsetCoordinates(column: 2, row: 4, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.odd) 86 | let offsetCoordinatesB = OffsetCoordinates(column: 2, row: 4, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.odd) 87 | XCTAssertEqual(offsetCoordinatesA, offsetCoordinatesB) 88 | } 89 | 90 | // conversions 91 | /// convert CubeToAxial 92 | func testCubeToAxial() throws { 93 | let cubeCoordinates = try CubeCoordinates(x: 1, y: 0, z: -1) 94 | let axialCoordinates = cubeCoordinates.toAxial() 95 | XCTAssertEqual(axialCoordinates.q, 1) 96 | XCTAssertEqual(axialCoordinates.r, -1) 97 | } 98 | 99 | /// convert CubeToPixel 100 | func testCubeToPixel() throws { 101 | let rootCubeCoordinates = try CubeCoordinates(x: 0, y: 0, z: 0) 102 | var pixelCoordinates = rootCubeCoordinates.toPixel( 103 | orientation: Orientation.pointyOnTop, 104 | hexSize: HexSize(width: 10.0, height: 10.0), 105 | origin: Point(x: 0.0, y: 0.0)) 106 | XCTAssertEqual(pixelCoordinates.x, 0.0) 107 | XCTAssertEqual(pixelCoordinates.y, 0.0) 108 | 109 | let cubeCoordinates = try CubeCoordinates(x: 2, y: -1, z: -1) 110 | pixelCoordinates = cubeCoordinates.toPixel( 111 | orientation: Orientation.pointyOnTop, 112 | hexSize: HexSize(width: 10.0, height: 10.0), 113 | origin: Point(x: 0.0, y: 0.0)) 114 | XCTAssertEqual(pixelCoordinates.x, 25.98076211353316) 115 | XCTAssertEqual(pixelCoordinates.y, -15.0) 116 | } 117 | 118 | /// Convert coordinates from cube to offset using `pointy on top` orientation and `odd` offset layout 119 | func testConvertCubeToOffsetOddRow () throws { 120 | let cubeCoordinates = try CubeCoordinates(x: 2, y: 0, z: -2) 121 | let offsetCoordinates = cubeCoordinates.toOffset(orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.odd) 122 | XCTAssertEqual(offsetCoordinates.column, 1) 123 | XCTAssertEqual(offsetCoordinates.row, -2) 124 | XCTAssertEqual(offsetCoordinates.orientation, Orientation.pointyOnTop) 125 | XCTAssertEqual(offsetCoordinates.offsetLayout, OffsetLayout.odd) 126 | } 127 | 128 | /// convert Axial coordinates to Cube 129 | func testAxialToCube() throws { 130 | let axialCoordinates = AxialCoordinates(q: 1, r: -1) 131 | let cubeCoordinates = try axialCoordinates.toCube() 132 | XCTAssertEqual(cubeCoordinates.x, 1) 133 | XCTAssertEqual(cubeCoordinates.y, 0) 134 | XCTAssertEqual(cubeCoordinates.z, -1) 135 | } 136 | 137 | /// Test equatability of Axial coordinates 138 | func testAxialEquatable() throws { 139 | let axialCoordinates = AxialCoordinates(q: 1, r: -1) 140 | let validCoordinates = AxialCoordinates(q: 1, r: -1) 141 | let invalidCoordinates = AxialCoordinates(q: 2, r: 0) 142 | XCTAssertTrue(axialCoordinates == validCoordinates) 143 | XCTAssertFalse(axialCoordinates == invalidCoordinates) 144 | } 145 | 146 | static var allTests = [ 147 | ("Create valid cube coordinates", testCreateValidCubeCoordinates), 148 | ("Create inalid cube coordinates", testCreateInalidCubeCoordinates), 149 | ("Create axial coordinates", testCreateAxialCoordinates), 150 | ("Create offset coordinates (odd row + pointy top)", testCreateOddRowOffsetCoordinates), 151 | ("Create offset coordinates (even row + pointy top)", testCreateEvenRowOffsetCoordinates), 152 | ("Create offset coordinates (odd column + flat top)", testCreateOddColumnOffsetCoordinates), 153 | ("Create offset coordinates (even column + flat top)", testCreateEvenColumnOffsetCoordinates), 154 | ("Test offset coordinates equatablity", testOffsetCoordinatesEquatability), 155 | ("Convert cube coordinates to axial", testCubeToAxial), 156 | ("Convert cube coordinates to pixel", testCubeToPixel), 157 | ("Convert cube coordinates to offset", testConvertCubeToOffsetOddRow), 158 | ("Convert axial coordinates to cube", testAxialToCube), 159 | ("Test equatability of Axial coordinates", testAxialEquatable), 160 | ] 161 | } 162 | 163 | -------------------------------------------------------------------------------- /Sources/HexGrid/DataStructures/Heap.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2016 Matthijs Hollemans and contributors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | internal struct Heap { 24 | 25 | /** The array that stores the heap's nodes. */ 26 | var nodes = [T]() 27 | 28 | /** 29 | * Determines how to compare two nodes in the heap. 30 | * Use '>' for a max-heap or '<' for a min-heap, 31 | * or provide a comparing method if the heap is made 32 | * of custom elements, for example tuples. 33 | */ 34 | private var orderCriteria: (T, T) -> Bool 35 | 36 | /** 37 | * Creates an empty heap. 38 | * The sort function determines whether this is a min-heap or max-heap. 39 | * For comparable data types, > makes a max-heap, < makes a min-heap. 40 | */ 41 | init(sort: @escaping (T, T) -> Bool) { 42 | self.orderCriteria = sort 43 | } 44 | 45 | /** 46 | * Creates a heap from an array. The order of the array does not matter; 47 | * the elements are inserted into the heap in the order determined by the 48 | * sort function. For comparable data types, '>' makes a max-heap, 49 | * '<' makes a min-heap. 50 | */ 51 | init(array: [T], sort: @escaping (T, T) -> Bool) { 52 | self.orderCriteria = sort 53 | configureHeap(from: array) 54 | } 55 | 56 | /** 57 | * Configures the max-heap or min-heap from an array, in a bottom-up manner. 58 | * Performance: This runs pretty much in O(n). 59 | */ 60 | private mutating func configureHeap(from array: [T]) { 61 | nodes = array 62 | for i in stride(from: (nodes.count/2-1), through: 0, by: -1) { 63 | shiftDown(i) 64 | } 65 | } 66 | 67 | var isEmpty: Bool { 68 | return nodes.isEmpty 69 | } 70 | 71 | var count: Int { 72 | return nodes.count 73 | } 74 | 75 | /** 76 | * Returns the index of the parent of the element at index i. 77 | * The element at index 0 is the root of the tree and has no parent. 78 | */ 79 | @inline(__always) internal func parentIndex(ofIndex i: Int) -> Int { 80 | // using right shift bit >> by 1 = which is basically division by 2 81 | return (i - 1) >> 1 82 | } 83 | 84 | /** 85 | * Returns the index of the left child of the element at index i. 86 | * Note that this index can be greater than the heap size, in which case 87 | * there is no left child. 88 | */ 89 | @inline(__always) internal func leftChildIndex(ofIndex i: Int) -> Int { 90 | return 2*i + 1 91 | } 92 | 93 | /** 94 | * Returns the index of the right child of the element at index i. 95 | * Note that this index can be greater than the heap size, in which case 96 | * there is no right child. 97 | */ 98 | @inline(__always) internal func rightChildIndex(ofIndex i: Int) -> Int { 99 | return 2*i + 2 100 | } 101 | 102 | /** 103 | * Returns the maximum value in the heap (for a max-heap) or the minimum 104 | * value (for a min-heap). 105 | */ 106 | func peek() -> T? { 107 | return nodes.first 108 | } 109 | 110 | /** 111 | * Adds a new value to the heap. This reorders the heap so that the max-heap 112 | * or min-heap property still holds. Performance: O(log n). 113 | */ 114 | mutating func insert(_ value: T) { 115 | nodes.append(value) 116 | shiftUp(nodes.count - 1) 117 | } 118 | 119 | /** 120 | * Adds a sequence of values to the heap. This reorders the heap so that 121 | * the max-heap or min-heap property still holds. Performance: O(log n). 122 | */ 123 | mutating func insert(_ sequence: S) where S.Iterator.Element == T { 124 | for value in sequence { 125 | insert(value) 126 | } 127 | } 128 | 129 | /** 130 | * Allows you to change an element. This reorders the heap so that 131 | * the max-heap or min-heap property still holds. 132 | */ 133 | mutating func replace(index i: Int, value: T) { 134 | guard i < nodes.count else { return } 135 | 136 | remove(at: i) 137 | insert(value) 138 | } 139 | 140 | /** 141 | * Removes the root node from the heap. For a max-heap, this is the maximum 142 | * value; for a min-heap it is the minimum value. Performance: O(log n). 143 | */ 144 | @discardableResult mutating func remove() -> T? { 145 | guard !nodes.isEmpty else { return nil } 146 | 147 | if nodes.count == 1 { 148 | return nodes.removeLast() 149 | } 150 | // Use the last node to replace the first one, then fix the heap by 151 | // shifting this new first node into its proper position. 152 | let value = nodes[0] 153 | nodes[0] = nodes.removeLast() 154 | shiftDown(0) 155 | return value 156 | } 157 | 158 | /** 159 | * Removes an arbitrary node from the heap. Performance: O(log n). 160 | * Note that you need to know the node's index. 161 | */ 162 | @discardableResult mutating func remove(at index: Int) -> T? { 163 | guard index < nodes.count else { return nil } 164 | 165 | let size = nodes.count - 1 166 | if index != size { 167 | nodes.swapAt(index, size) 168 | shiftDown(from: index, until: size) 169 | shiftUp(index) 170 | } 171 | return nodes.removeLast() 172 | } 173 | 174 | /** 175 | * Takes a child node and looks at its parents; if a parent is not larger 176 | * (max-heap) or not smaller (min-heap) than the child, we exchange them. 177 | */ 178 | internal mutating func shiftUp(_ index: Int) { 179 | var childIndex = index 180 | let child = nodes[childIndex] 181 | var parentIndex = self.parentIndex(ofIndex: childIndex) 182 | 183 | while childIndex > 0 && orderCriteria(child, nodes[parentIndex]) { 184 | nodes[childIndex] = nodes[parentIndex] 185 | childIndex = parentIndex 186 | parentIndex = self.parentIndex(ofIndex: childIndex) 187 | } 188 | 189 | nodes[childIndex] = child 190 | } 191 | 192 | /** 193 | * Looks at a parent node and makes sure it is still larger (max-heap) or 194 | * smaller (min-heap) than its children. 195 | */ 196 | internal mutating func shiftDown(from index: Int, until endIndex: Int) { 197 | let leftChildIndex = self.leftChildIndex(ofIndex: index) 198 | let rightChildIndex = leftChildIndex + 1 199 | 200 | // Figure out which comes first if we order them by the sort function: 201 | // the parent, the left child, or the right child. If the parent comes 202 | // first, we're done. If not, that element is out-of-place and we make 203 | // it "float down" the tree until the heap property is restored. 204 | var first = index 205 | if leftChildIndex < endIndex && orderCriteria(nodes[leftChildIndex], nodes[first]) { 206 | first = leftChildIndex 207 | } 208 | if rightChildIndex < endIndex && orderCriteria(nodes[rightChildIndex], nodes[first]) { 209 | first = rightChildIndex 210 | } 211 | if first == index { return } 212 | 213 | nodes.swapAt(index, first) 214 | shiftDown(from: first, until: endIndex) 215 | } 216 | 217 | internal mutating func shiftDown(_ index: Int) { 218 | shiftDown(from: index, until: nodes.count) 219 | } 220 | 221 | } 222 | 223 | // MARK: - Searching 224 | extension Heap where T: Equatable { 225 | 226 | /** Get the index of a node in the heap. Performance: O(n). */ 227 | func index(of node: T) -> Int? { 228 | return nodes.firstIndex(where: { $0 == node }) 229 | } 230 | 231 | /** Removes the first occurrence of a node from the heap. Performance: O(n log n). */ 232 | @discardableResult mutating func remove(node: T) -> T? { 233 | if let index = index(of: node) { 234 | return remove(at: index) 235 | } 236 | return nil 237 | } 238 | 239 | } 240 | 241 | // MARK: - Sorting 242 | extension Heap { 243 | mutating func sort() -> [T] { 244 | for i in stride(from: (nodes.count - 1), through: 1, by: -1) { 245 | nodes.swapAt(0, i) 246 | shiftDown(from: 0, until: i) 247 | } 248 | return nodes 249 | } 250 | } 251 | 252 | /* 253 | Sorts an array using heap. 254 | Heapsort can be performed in-place, but it is not a stable sort. 255 | */ 256 | func heapsort(_ a: [T], _ sort: @escaping (T, T) -> Bool) -> [T] { 257 | let reverseOrder = { i1, i2 in sort(i2, i1) } 258 | var h = Heap(array: a, sort: reverseOrder) 259 | return h.sort() 260 | } 261 | -------------------------------------------------------------------------------- /Tests/HexGridTests/GridGeneratorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HexGrid 3 | 4 | class GridGeneratorTests: XCTestCase { 5 | 6 | /// Create rectangular grids in all variations 7 | func testCreateRectangleGrid () throws { 8 | var orientation = Orientation.pointyOnTop 9 | var offsetLayout = OffsetLayout.even 10 | let hexSize = HexSize(width: 10.0, height: 10.0) 11 | let origin = Point(x: 0.0, y: 0.0) 12 | var shape = GridShape.rectangle(2, 1) 13 | 14 | let gridPointyEven = HexGrid( 15 | shape: shape, 16 | orientation: orientation, 17 | offsetLayout: offsetLayout, 18 | hexSize: hexSize, 19 | origin: origin) 20 | XCTAssertEqual(gridPointyEven.cells.count, 2) 21 | 22 | shape = GridShape.rectangle(2, 2) 23 | offsetLayout = OffsetLayout.odd 24 | let gridPointyOdd = HexGrid( 25 | shape: shape, 26 | orientation: orientation, 27 | offsetLayout: offsetLayout, 28 | hexSize: hexSize, 29 | origin: origin) 30 | XCTAssertEqual(gridPointyOdd.cells.count, 4) 31 | 32 | shape = GridShape.rectangle(3, 4) 33 | orientation = Orientation.flatOnTop 34 | offsetLayout = OffsetLayout.even 35 | let gridFlatEven = HexGrid( 36 | shape: shape, 37 | orientation: orientation, 38 | offsetLayout: offsetLayout, 39 | hexSize: hexSize, 40 | origin: origin) 41 | XCTAssertEqual(gridFlatEven.cells.count, 12) 42 | 43 | shape = GridShape.rectangle(4, 5) 44 | offsetLayout = OffsetLayout.odd 45 | let gridFlatOdd = HexGrid( 46 | shape: shape, 47 | orientation: orientation, 48 | offsetLayout: offsetLayout) 49 | XCTAssertEqual(gridFlatOdd.cells.count, 20) 50 | } 51 | 52 | /// Create hexagonal grid 53 | func testCreateHexagonGrid () throws { 54 | var orientation = Orientation.pointyOnTop 55 | var offsetLayout = OffsetLayout.even 56 | let hexSize = HexSize(width: 10.0, height: 10.0) 57 | let origin = Point(x: 0.0, y: 0.0) 58 | var shape = GridShape.hexagon(2) 59 | 60 | let gridPointy = HexGrid( 61 | shape: shape, 62 | orientation: orientation, 63 | offsetLayout: offsetLayout, 64 | hexSize: hexSize, 65 | origin: origin) 66 | XCTAssertEqual(gridPointy.cells.count, 7) 67 | 68 | orientation = Orientation.flatOnTop 69 | offsetLayout = OffsetLayout.odd 70 | shape = GridShape.hexagon(3) 71 | let gridFlat = HexGrid( 72 | shape: shape, 73 | orientation: orientation, 74 | offsetLayout: offsetLayout, 75 | hexSize: hexSize, 76 | origin: origin) 77 | XCTAssertEqual(gridFlat.cells.count, 19) 78 | } 79 | 80 | /// Create triangular grids in all variations 81 | func testCreateTriangleGrid () throws { 82 | var orientation = Orientation.pointyOnTop 83 | var offsetLayout = OffsetLayout.even 84 | let hexSize = HexSize(width: 10.0, height: 10.0) 85 | let origin = Point(x: 0.0, y: 0.0) 86 | var shape = GridShape.triangle(4) 87 | 88 | let gridPointy = HexGrid( 89 | shape: shape, 90 | orientation: orientation, 91 | offsetLayout: offsetLayout, 92 | hexSize: hexSize, 93 | origin: origin) 94 | XCTAssertEqual(gridPointy.cells.count, 10) 95 | 96 | shape = GridShape.triangle(5) 97 | orientation = Orientation.flatOnTop 98 | offsetLayout = OffsetLayout.even 99 | let gridFlat = HexGrid( 100 | shape: shape, 101 | orientation: orientation, 102 | offsetLayout: offsetLayout, 103 | hexSize: hexSize, 104 | origin: origin) 105 | XCTAssertEqual(gridFlat.cells.count, 15) 106 | } 107 | 108 | /// Create invalid shape grid 109 | func testCreateInvalidShapeGrid () throws { 110 | let orientation = Orientation.pointyOnTop 111 | let offsetLayout = OffsetLayout.even 112 | let hexSize = HexSize(width: 10.0, height: 10.0) 113 | let origin = Point(x: 0.0, y: 0.0) 114 | 115 | // test for invalid rectangle 116 | var shape = GridShape.rectangle(10, -5) 117 | var result = HexGrid( 118 | shape: shape, 119 | orientation: orientation, 120 | offsetLayout: offsetLayout, 121 | hexSize: hexSize, 122 | origin: origin) 123 | XCTAssertEqual(result.cells.count, 0) 124 | 125 | // test for invalid hexagon 126 | shape = GridShape.hexagon(-3) 127 | result = HexGrid( 128 | shape: shape, 129 | orientation: orientation, 130 | offsetLayout: offsetLayout, 131 | hexSize: hexSize, 132 | origin: origin) 133 | XCTAssertEqual(result.cells.count, 0) 134 | 135 | // test for invalid triangle 136 | shape = GridShape.triangle(-3) 137 | result = HexGrid( 138 | shape: shape, 139 | orientation: orientation, 140 | offsetLayout: offsetLayout, 141 | hexSize: hexSize, 142 | origin: origin) 143 | XCTAssertEqual(result.cells.count, 0) 144 | 145 | // test for invalid parallelogram 146 | shape = GridShape.parallelogram(-3, 5) 147 | result = HexGrid( 148 | shape: shape, 149 | orientation: orientation, 150 | offsetLayout: offsetLayout, 151 | hexSize: hexSize, 152 | origin: origin) 153 | XCTAssertEqual(result.cells.count, 0) 154 | 155 | // test for invalid irregular hexagon 156 | shape = GridShape.irregularHexagon(-3, 5) 157 | result = HexGrid( 158 | shape: shape, 159 | orientation: orientation, 160 | offsetLayout: offsetLayout, 161 | hexSize: hexSize, 162 | origin: origin) 163 | XCTAssertEqual(result.cells.count, 0) 164 | } 165 | 166 | /// Create parallelogram grids 167 | func testCreateParallelogramGrids () throws { 168 | var orientation = Orientation.pointyOnTop 169 | var offsetLayout = OffsetLayout.even 170 | let hexSize = HexSize(width: 10.0, height: 10.0) 171 | let origin = Point(x: 0.0, y: 0.0) 172 | 173 | var shape = GridShape.parallelogram(3, 4) 174 | let gridPointy = HexGrid( 175 | shape: shape, 176 | orientation: orientation, 177 | offsetLayout: offsetLayout, 178 | hexSize: hexSize, 179 | origin: origin) 180 | XCTAssertEqual(gridPointy.cells.count, 12) 181 | 182 | shape = GridShape.parallelogram(10, 20) 183 | orientation = Orientation.flatOnTop 184 | offsetLayout = OffsetLayout.even 185 | let gridFlat = HexGrid( 186 | shape: shape, 187 | orientation: orientation, 188 | offsetLayout: offsetLayout, 189 | hexSize: hexSize, 190 | origin: origin) 191 | XCTAssertEqual(gridFlat.cells.count, 200) 192 | } 193 | 194 | /// Create extended hexagon grids 195 | func testCreateElongatedHexagonGrids () throws { 196 | 197 | var shape = GridShape.elongatedHexagon(3, 4) 198 | var grid = HexGrid( 199 | shape: shape, 200 | orientation: .pointyOnTop) 201 | XCTAssertEqual(grid.cells.count, 30) 202 | 203 | // flat 204 | grid = HexGrid( 205 | shape: shape, 206 | orientation: .flatOnTop) 207 | XCTAssertEqual(grid.cells.count, 24) 208 | 209 | shape = GridShape.elongatedHexagon(2, 3) 210 | grid = HexGrid( 211 | shape: shape, 212 | orientation: .pointyOnTop) 213 | XCTAssertEqual(grid.cells.count, 14) 214 | 215 | grid = HexGrid( 216 | shape: shape, 217 | orientation: .flatOnTop) 218 | XCTAssertEqual(grid.cells.count, 10) 219 | 220 | shape = GridShape.elongatedHexagon(7, 3) 221 | grid = HexGrid( 222 | shape: shape, 223 | orientation: .pointyOnTop) 224 | XCTAssertEqual(grid.cells.count, 39) 225 | 226 | shape = GridShape.elongatedHexagon(3, 7) 227 | grid = HexGrid( 228 | shape: shape, 229 | orientation: .flatOnTop) 230 | XCTAssertEqual(grid.cells.count, 39) 231 | } 232 | 233 | /// Create irregular hexagon grids 234 | func testCreateIrregularHexagonGrids () throws { 235 | var orientation = Orientation.pointyOnTop 236 | var offsetLayout = OffsetLayout.even 237 | let hexSize = HexSize(width: 10.0, height: 10.0) 238 | let origin = Point(x: 0.0, y: 0.0) 239 | 240 | var shape = GridShape.irregularHexagon(3, 4) 241 | let gridPointy = HexGrid( 242 | shape: shape, 243 | orientation: orientation, 244 | offsetLayout: offsetLayout, 245 | hexSize: hexSize, 246 | origin: origin) 247 | XCTAssertEqual(gridPointy.cells.count, 27) 248 | 249 | shape = GridShape.irregularHexagon(2, 3) 250 | orientation = Orientation.flatOnTop 251 | offsetLayout = OffsetLayout.even 252 | let gridFlat = HexGrid( 253 | shape: shape, 254 | orientation: orientation, 255 | offsetLayout: offsetLayout, 256 | hexSize: hexSize, 257 | origin: origin) 258 | XCTAssertEqual(gridFlat.cells.count, 12) 259 | } 260 | 261 | static var allTests = [ 262 | ("Create rectangular grids", testCreateRectangleGrid), 263 | ("Create hexagonal grid", testCreateHexagonGrid), 264 | ("Create triangular grid", testCreateTriangleGrid), 265 | ("Create invalid shape grid", testCreateInvalidShapeGrid), 266 | ("Create parallelogram shape grid", testCreateParallelogramGrids), 267 | ("Create elongated hexagon shape grid", testCreateElongatedHexagonGrids), 268 | ("Create irregular hexagon shape grid", testCreateIrregularHexagonGrids), 269 | ] 270 | 271 | } 272 | -------------------------------------------------------------------------------- /Sources/HexGrid/Generator.swift: -------------------------------------------------------------------------------- 1 | /// Generates grid based on parameters 2 | internal struct Generator { 3 | 4 | /// A Set of `Cell`s based on `GridShape`, `Orientation`, and `OffsetLayout`. 5 | /// - parameters: 6 | /// - shape: The `GridShape` for our `Cell` values. 7 | /// - orientation: An `Orientation` value for the generated set of `Cell` values. 8 | /// - offsetLayout: The `OffsetLayout` for the generated set of `Cell`s. 9 | /// - returns: A `Set`, suitable for use in `HexGrid`. 10 | static func cellsForGridShape( 11 | shape: GridShape, 12 | orientation: Orientation, 13 | offsetLayout: OffsetLayout 14 | ) throws -> Set { 15 | switch shape { 16 | case .hexagon(let sideLength): 17 | return try Set(Generator.createHexagonGrid(sideLength: sideLength).map { Cell($0) }) 18 | case .elongatedHexagon(let side1, let side2): 19 | return try Set(Generator.createElongatedHexGrid( 20 | side1: side1, 21 | side2: side2, 22 | orientation: orientation 23 | ).map { Cell($0) }) 24 | case .irregularHexagon(let side1, let side2): 25 | return try Set(Generator.createIrregularHexGrid( 26 | side1: side1, 27 | side2: side2 28 | ).map { Cell($0) }) 29 | case .parallelogram(let width, let height): 30 | return try Set(Generator.createParallelogramGrid( 31 | width: width, 32 | height: height).map { Cell($0) }) 33 | case .rectangle(let width, let height): 34 | return try Set(Generator.createRectangleGrid( 35 | orientation: orientation, 36 | offsetLayout: offsetLayout, 37 | width: width, 38 | height: height).map { Cell($0) }) 39 | case .triangle(let sideSize): 40 | return try Set(Generator.createTriangleGrid( 41 | orientation: orientation, 42 | sideLength: sideSize).map { Cell($0) }) 43 | } 44 | } 45 | 46 | /// Create grid of hexagonal shape 47 | /// - parameters: 48 | /// - sideLength: side length of desired hexagonal shape (1 -> single tile, 2 -> 7 tiles, 3 -> 19 tiles...) 49 | static func createHexagonGrid( 50 | sideLength: Int 51 | ) throws -> Set { 52 | guard sideLength > 0 else { 53 | throw InvalidArgumentsError(message: "Hexagon side length must be greater than zero.") 54 | } 55 | let side = sideLength - 1 56 | var tiles = Set() 57 | for x in -side...side { 58 | for y in max(-side, -x-side)...min(side, -x+side) { 59 | tiles.insert(try (CubeCoordinates(x: x, y: y, z: -x-y))) 60 | } 61 | } 62 | return tiles 63 | } 64 | 65 | /// Create an elongated hexagon shape (where sides are: length1, length1, length2, length1, length1, length2.) 66 | /// - parameters: 67 | /// - side1: number of hexes on the first side length 68 | /// - side2: number of hexes along the second side length 69 | static func createElongatedHexGrid( 70 | side1: Int, 71 | side2: Int, 72 | orientation: Orientation 73 | ) throws -> Set { 74 | guard side1 > 0, side2 > 0 else { 75 | throw InvalidArgumentsError(message: "Side lengths must both be greater than zero.") 76 | } 77 | guard side1 != side2 else { 78 | return try createHexagonGrid(sideLength: side1) 79 | } 80 | let total = side1 + side2 - 1 81 | var tiles = Set() 82 | var rLimit1: Int 83 | var rLimit2: Int 84 | switch orientation { 85 | case .pointyOnTop: 86 | rLimit1 = side2 87 | rLimit2 = side1 88 | case .flatOnTop: 89 | rLimit1 = side1 90 | rLimit2 = side2 91 | } 92 | // first half 93 | for r in 0..(tiles) 113 | tiles.removeAll(keepingCapacity: true) 114 | for coordinate in rotatedTiles { 115 | tiles.insert(try Math.rotateRight(coordinates: coordinate)) 116 | } 117 | } 118 | return tiles 119 | } 120 | 121 | /// Create an irregular hexagon shape (where sides alternate length1, length2, etc.) 122 | /// - parameters: 123 | /// - side1: number of hexes on the first side 124 | /// - side2: number of hexes along the second side 125 | static func createIrregularHexGrid( 126 | side1: Int, 127 | side2: Int 128 | ) throws -> Set { 129 | guard side1 > 0, side2 > 0 else { 130 | throw InvalidArgumentsError(message: "Side lengths must both be greater than zero.") 131 | } 132 | guard side1 != side2 else { 133 | return try createHexagonGrid(sideLength: side1) 134 | } 135 | let total = side1 + side2 - 1 136 | var tiles = Set() 137 | // first half 138 | for r in 0.. Set { 163 | guard width > 0, height > 0 else { 164 | throw InvalidArgumentsError(message: "Parallelogram width and height must be greater than zero.") 165 | } 166 | var tiles = Set() 167 | for row in 0.. Set { 187 | guard width > 0, height > 0 else { 188 | throw InvalidArgumentsError(message: "Rectangle width and height must be greater than zero.") 189 | } 190 | var tiles = Set() 191 | switch orientation { 192 | case Orientation.pointyOnTop: 193 | for r in 0..>1 198 | case OffsetLayout.odd: 199 | offset = (r)>>1 200 | } 201 | for q in -offset..>1 211 | case OffsetLayout.odd: 212 | offset = (q)>>1 213 | } 214 | for r in -offset.. Set { 230 | guard sideLength > 0 else { 231 | throw InvalidArgumentsError(message: "Triangle side length must be greater than zero.") 232 | } 233 | var tiles = Set() 234 | let side = sideLength - 1 235 | switch orientation { 236 | case Orientation.pointyOnTop: 237 | for x in 0...side { 238 | for y in 0...side-x { 239 | tiles.insert(try (CubeCoordinates(x: x, y: y, z: -x-y))) 240 | } 241 | } 242 | case Orientation.flatOnTop: 243 | for x in 0...side { 244 | for y in side-x...side { 245 | tiles.insert(try (CubeCoordinates(x: x, y: y, z: -x-y))) 246 | } 247 | } 248 | } 249 | return tiles 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Tests/HexGridTests/ConvertorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HexGrid 3 | 4 | class ConvertorTests: XCTestCase { 5 | 6 | // Coordinates conversion tests 7 | /// Convert coordinates from cube to axial 8 | func testConvertCubeToAxial () throws { 9 | let cubeCoordinates = try CubeCoordinates(x: 1, y: 0, z: -1) 10 | let axialCoordinates = Convertor.cubeToAxial(from: cubeCoordinates) 11 | XCTAssertEqual(axialCoordinates.q, 1) 12 | XCTAssertEqual(axialCoordinates.r, -1) 13 | } 14 | 15 | /// Convert coordinates from cube to offset using `pointy on top` orientation and `odd` offset layout 16 | func testConvertCubeToOffsetOddRow () throws { 17 | let cubeCoordinates = try CubeCoordinates(x: 2, y: 0, z: -2) 18 | let offsetCoordinates = Convertor.cubeToOffset(from: cubeCoordinates, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.odd) 19 | XCTAssertEqual(offsetCoordinates.column, 1) 20 | XCTAssertEqual(offsetCoordinates.row, -2) 21 | XCTAssertEqual(offsetCoordinates.orientation, Orientation.pointyOnTop) 22 | XCTAssertEqual(offsetCoordinates.offsetLayout, OffsetLayout.odd) 23 | } 24 | 25 | /// Convert coordinates from cube to offset using `pointy on top` orientation, `odd` offset layout and `alternate` row 26 | func testConvertCubeToOffsetOddRowAlt () throws { 27 | // test conversion for alterante row 28 | let cubeCoordinatesAlt = try CubeCoordinates(x: 2, y: -1, z: -1) 29 | let offsetCoordinatesAlt = Convertor.cubeToOffset(from: cubeCoordinatesAlt, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.odd) 30 | XCTAssertEqual(offsetCoordinatesAlt.column, 1) 31 | XCTAssertEqual(offsetCoordinatesAlt.row, -1) 32 | XCTAssertEqual(offsetCoordinatesAlt.orientation, Orientation.pointyOnTop) 33 | XCTAssertEqual(offsetCoordinatesAlt.offsetLayout, OffsetLayout.odd) 34 | } 35 | 36 | /// Convert coordinates from cube to offset using `pointy on top` orientation and `even` offset layout 37 | func testConvertCubeToOffsetEvenRow () throws { 38 | // test conversion for opposite row 39 | let cubeCoordinates = try CubeCoordinates(x: 2, y: 0, z: -2) 40 | let offsetCoordinates = Convertor.cubeToOffset(from: cubeCoordinates, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.even) 41 | XCTAssertEqual(offsetCoordinates.column, 1) 42 | XCTAssertEqual(offsetCoordinates.row, -2) 43 | XCTAssertEqual(offsetCoordinates.orientation, Orientation.pointyOnTop) 44 | XCTAssertEqual(offsetCoordinates.offsetLayout, OffsetLayout.even) 45 | } 46 | 47 | /// Convert coordinates from cube to offset using `pointy on top` orientation, `even` offset layout and `alternate` row 48 | func testConvertCubeToOffsetEvenRowAlt () throws { 49 | // test conversion for alterante row 50 | let cubeCoordinatesAlt = try CubeCoordinates(x: 2, y: -1, z: -1) 51 | let offsetCoordinatesAlt = Convertor.cubeToOffset(from: cubeCoordinatesAlt, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.even) 52 | XCTAssertEqual(offsetCoordinatesAlt.column, 2) 53 | XCTAssertEqual(offsetCoordinatesAlt.row, -1) 54 | XCTAssertEqual(offsetCoordinatesAlt.orientation, Orientation.pointyOnTop) 55 | XCTAssertEqual(offsetCoordinatesAlt.offsetLayout, OffsetLayout.even) 56 | } 57 | 58 | /// Convert coordinates from cube to offset using `flat on top` orientation and `odd` offset layout 59 | func testConvertCubeToOffsetOddColumn () throws { 60 | // test conversion for opposite row 61 | let cubeCoordinates = try CubeCoordinates(x: 2, y: 0, z: -2) 62 | let offsetCoordinates = Convertor.cubeToOffset(from: cubeCoordinates, orientation: Orientation.flatOnTop, offsetLayout: OffsetLayout.odd) 63 | XCTAssertEqual(offsetCoordinates.column, 2) 64 | XCTAssertEqual(offsetCoordinates.row, -1) 65 | XCTAssertEqual(offsetCoordinates.orientation, Orientation.flatOnTop) 66 | XCTAssertEqual(offsetCoordinates.offsetLayout, OffsetLayout.odd) 67 | } 68 | 69 | /// Convert coordinates from cube to offset using `flat on top` orientation, `odd` offset layout and `alternate` column 70 | func testConvertCubeToOffsetOddColumnAlt () throws { 71 | // test conversion for alterante column 72 | let cubeCoordinatesAlt = try CubeCoordinates(x: 1, y: 0, z: -1) 73 | let offsetCoordinatesAlt = Convertor.cubeToOffset(from: cubeCoordinatesAlt, orientation: Orientation.flatOnTop, offsetLayout: OffsetLayout.odd) 74 | XCTAssertEqual(offsetCoordinatesAlt.column, 1) 75 | XCTAssertEqual(offsetCoordinatesAlt.row, -1) 76 | XCTAssertEqual(offsetCoordinatesAlt.orientation, Orientation.flatOnTop) 77 | XCTAssertEqual(offsetCoordinatesAlt.offsetLayout, OffsetLayout.odd) 78 | } 79 | 80 | /// Convert coordinates from cube to offset using `flat on top` orientation and `even` offset layout 81 | func testConvertCubeToOffsetEvenColumn () throws { 82 | // test conversion for opposite row 83 | let cubeCoordinates = try CubeCoordinates(x: 2, y: 0, z: -2) 84 | let offsetCoordinates = Convertor.cubeToOffset(from: cubeCoordinates, orientation: Orientation.flatOnTop, offsetLayout: OffsetLayout.even) 85 | XCTAssertEqual(offsetCoordinates.column, 2) 86 | XCTAssertEqual(offsetCoordinates.row, -1) 87 | XCTAssertEqual(offsetCoordinates.orientation, Orientation.flatOnTop) 88 | XCTAssertEqual(offsetCoordinates.offsetLayout, OffsetLayout.even) 89 | } 90 | 91 | /// Convert coordinates from cube to offset using `flat on top` orientation, `even` offset layout and `alternate` column 92 | func testConvertCubeToOffsetEvenColumnAlt () throws { 93 | // test conversion for alterante column 94 | let cubeCoordinatesAlt = try CubeCoordinates(x: 1, y: 0, z: -1) 95 | let offsetCoordinatesAlt = Convertor.cubeToOffset(from: cubeCoordinatesAlt, orientation: Orientation.flatOnTop, offsetLayout: OffsetLayout.even) 96 | XCTAssertEqual(offsetCoordinatesAlt.column, 1) 97 | XCTAssertEqual(offsetCoordinatesAlt.row, 0) 98 | XCTAssertEqual(offsetCoordinatesAlt.orientation, Orientation.flatOnTop) 99 | XCTAssertEqual(offsetCoordinatesAlt.offsetLayout, OffsetLayout.even) 100 | } 101 | 102 | // Convert coordinates from offset to cube 103 | func testConvertOffsetToCube() throws { 104 | let offsetCoordinatesPointyEven = OffsetCoordinates(column: 2, row: -1, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.even) 105 | var cubeCoordinates = try offsetCoordinatesPointyEven.toCube() 106 | XCTAssertEqual(cubeCoordinates.x, 2) 107 | XCTAssertEqual(cubeCoordinates.y, -1) 108 | XCTAssertEqual(cubeCoordinates.z, -1) 109 | 110 | let offsetCoordinatesPointyOdd = OffsetCoordinates(column: 2, row: -1, orientation: Orientation.pointyOnTop, offsetLayout: OffsetLayout.odd) 111 | cubeCoordinates = try offsetCoordinatesPointyOdd.toCube() 112 | XCTAssertEqual(cubeCoordinates.x, 3) 113 | XCTAssertEqual(cubeCoordinates.y, -2) 114 | XCTAssertEqual(cubeCoordinates.z, -1) 115 | 116 | let offsetCoordinatesFlatEven = OffsetCoordinates(column: -1, row: 2, orientation: Orientation.flatOnTop, offsetLayout: OffsetLayout.even) 117 | cubeCoordinates = try offsetCoordinatesFlatEven.toCube() 118 | XCTAssertEqual(cubeCoordinates.x, -1) 119 | XCTAssertEqual(cubeCoordinates.y, -1) 120 | XCTAssertEqual(cubeCoordinates.z, 2) 121 | 122 | let offsetCoordinatesFlatOdd = OffsetCoordinates(column: -1, row: 2, orientation: Orientation.flatOnTop, offsetLayout: OffsetLayout.odd) 123 | cubeCoordinates = try offsetCoordinatesFlatOdd.toCube() 124 | XCTAssertEqual(cubeCoordinates.x, -1) 125 | XCTAssertEqual(cubeCoordinates.y, -2) 126 | XCTAssertEqual(cubeCoordinates.z, 3) 127 | } 128 | 129 | // Convert coordinates from pixel to cube 130 | func testConvertPixelToCube() throws { 131 | let point1 = Convertor.pixelToCube( 132 | from: Point(x: 3.2, y: 4.5), 133 | hexSize: HexSize(width: 10.0, height: 10.0), 134 | origin: Point(x: 0.0, y: 0.0), 135 | orientation: Orientation.pointyOnTop) 136 | let point2 = Convertor.pixelToCube( 137 | from: Point(x: 22.2, y: 4.5), 138 | hexSize: HexSize(width: 10.0, height: 10.0), 139 | origin: Point(x: 0.0, y: 0.0), 140 | orientation: Orientation.pointyOnTop) 141 | let point3 = Convertor.pixelToCube( 142 | from: Point(x: 0.0, y: -22.5), 143 | hexSize: HexSize(width: 10.0, height: 10.0), 144 | origin: Point(x: 0.0, y: 0.0), 145 | orientation: Orientation.pointyOnTop) 146 | 147 | let expectedCube1 = try CubeCoordinates(x: 0, y: 0, z: 0) 148 | let expectedCube2 = try CubeCoordinates(x: 1, y: -1, z: 0) 149 | let expectedCube3 = try CubeCoordinates(x: 1, y: 1, z: -2) 150 | 151 | XCTAssertEqual(expectedCube1, point1) 152 | XCTAssertEqual(expectedCube2, point2) 153 | XCTAssertEqual(expectedCube3, point3) 154 | } 155 | 156 | 157 | 158 | static var allTests = [ 159 | ("Convert coordinates from cube to axial", testConvertCubeToAxial), 160 | ("Convert coordinates from cube to offset (odd row + pointy top)", testConvertCubeToOffsetOddRow), 161 | ("Convert coordinates from cube to offset (odd row + pointy top + alternate row)", testConvertCubeToOffsetOddRowAlt), 162 | ("Convert coordinates from cube to offset (even row + pointy top)", testConvertCubeToOffsetEvenRow), 163 | ("Convert coordinates from cube to offset (even row + pointy top + alternate row)", testConvertCubeToOffsetEvenRowAlt), 164 | ("Convert coordinates from cube to offset (odd column + flat top)", testConvertCubeToOffsetOddColumn), 165 | ("Convert coordinates from cube to offset (odd column + flat top + alternate column)", testConvertCubeToOffsetOddColumnAlt), 166 | ("Convert coordinates from cube to offset (even column + flat top)", testConvertCubeToOffsetEvenColumn), 167 | ("Convert coordinates from cube to offset (even column + flat top + alternate column)", testConvertCubeToOffsetEvenColumnAlt), 168 | ("Convert coordinates from offset to cube)", testConvertOffsetToCube), 169 | ("Convert coordinates from pixel to cube)", testConvertPixelToCube) 170 | ] 171 | 172 | } 173 | -------------------------------------------------------------------------------- /Tests/HexGridTests/HeapTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HexGrid 3 | 4 | class HeapTests: XCTestCase { 5 | fileprivate func verifyMaxHeap(_ h: Heap) -> Bool { 6 | for i in 0.. 0 && h.nodes[parent] < h.nodes[i] { return false } 13 | } 14 | return true 15 | } 16 | 17 | fileprivate func verifyMinHeap(_ h: Heap) -> Bool { 18 | for i in 0.. h.nodes[left] { return false } 23 | if right < h.count && h.nodes[i] > h.nodes[right] { return false } 24 | if i > 0 && h.nodes[parent] > h.nodes[i] { return false } 25 | } 26 | return true 27 | } 28 | 29 | fileprivate func isPermutation(_ array1: [Int], _ array2: [Int]) -> Bool { 30 | var a1 = array1 31 | var a2 = array2 32 | if a1.count != a2.count { return false } 33 | while a1.count > 0 { 34 | if let i = a2.firstIndex(of: a1[0]) { 35 | a1.remove(at: 0) 36 | a2.remove(at: i) 37 | } else { 38 | return false 39 | } 40 | } 41 | return a2.count == 0 42 | } 43 | 44 | func testEmptyHeap() { 45 | var heap = Heap(sort: <) 46 | XCTAssertTrue(heap.isEmpty) 47 | XCTAssertEqual(heap.count, 0) 48 | XCTAssertNil(heap.peek()) 49 | XCTAssertNil(heap.remove()) 50 | } 51 | 52 | func testIsEmpty() { 53 | var heap = Heap(sort: >) 54 | XCTAssertTrue(heap.isEmpty) 55 | heap.insert(1) 56 | XCTAssertFalse(heap.isEmpty) 57 | heap.remove() 58 | XCTAssertTrue(heap.isEmpty) 59 | } 60 | 61 | func testCount() { 62 | var heap = Heap(sort: >) 63 | XCTAssertEqual(0, heap.count) 64 | heap.insert(1) 65 | XCTAssertEqual(1, heap.count) 66 | } 67 | 68 | func testMaxHeapOneElement() { 69 | let heap = Heap(array: [10], sort: >) 70 | XCTAssertTrue(verifyMaxHeap(heap)) 71 | XCTAssertTrue(verifyMinHeap(heap)) 72 | XCTAssertFalse(heap.isEmpty) 73 | XCTAssertEqual(heap.count, 1) 74 | XCTAssertEqual(heap.peek()!, 10) 75 | } 76 | 77 | func testCreateMaxHeap() { 78 | let h1 = Heap(array: [1, 2, 3, 4, 5, 6, 7], sort: >) 79 | XCTAssertTrue(verifyMaxHeap(h1)) 80 | XCTAssertFalse(verifyMinHeap(h1)) 81 | XCTAssertEqual(h1.nodes, [7, 5, 6, 4, 2, 1, 3]) 82 | XCTAssertFalse(h1.isEmpty) 83 | XCTAssertEqual(h1.count, 7) 84 | XCTAssertEqual(h1.peek()!, 7) 85 | 86 | let h2 = Heap(array: [7, 6, 5, 4, 3, 2, 1], sort: >) 87 | XCTAssertTrue(verifyMaxHeap(h2)) 88 | XCTAssertFalse(verifyMinHeap(h2)) 89 | XCTAssertEqual(h2.nodes, [7, 6, 5, 4, 3, 2, 1]) 90 | XCTAssertFalse(h2.isEmpty) 91 | XCTAssertEqual(h2.count, 7) 92 | XCTAssertEqual(h2.peek()!, 7) 93 | 94 | let h3 = Heap(array: [4, 1, 3, 2, 16, 9, 10, 14, 8, 7], sort: >) 95 | XCTAssertTrue(verifyMaxHeap(h3)) 96 | XCTAssertFalse(verifyMinHeap(h3)) 97 | XCTAssertEqual(h3.nodes, [16, 14, 10, 8, 7, 9, 3, 2, 4, 1]) 98 | XCTAssertFalse(h3.isEmpty) 99 | XCTAssertEqual(h3.count, 10) 100 | XCTAssertEqual(h3.peek()!, 16) 101 | 102 | let h4 = Heap(array: [27, 17, 3, 16, 13, 10, 1, 5, 7, 12, 4, 8, 9, 0], sort: >) 103 | XCTAssertTrue(verifyMaxHeap(h4)) 104 | XCTAssertFalse(verifyMinHeap(h4)) 105 | XCTAssertEqual(h4.nodes, [27, 17, 10, 16, 13, 9, 1, 5, 7, 12, 4, 8, 3, 0]) 106 | XCTAssertFalse(h4.isEmpty) 107 | XCTAssertEqual(h4.count, 14) 108 | XCTAssertEqual(h4.peek()!, 27) 109 | } 110 | 111 | func testCreateMinHeap() { 112 | let h1 = Heap(array: [1, 2, 3, 4, 5, 6, 7], sort: <) 113 | XCTAssertTrue(verifyMinHeap(h1)) 114 | XCTAssertFalse(verifyMaxHeap(h1)) 115 | XCTAssertEqual(h1.nodes, [1, 2, 3, 4, 5, 6, 7]) 116 | XCTAssertFalse(h1.isEmpty) 117 | XCTAssertEqual(h1.count, 7) 118 | XCTAssertEqual(h1.peek()!, 1) 119 | 120 | let h2 = Heap(array: [7, 6, 5, 4, 3, 2, 1], sort: <) 121 | XCTAssertTrue(verifyMinHeap(h2)) 122 | XCTAssertFalse(verifyMaxHeap(h2)) 123 | XCTAssertEqual(h2.nodes, [1, 3, 2, 4, 6, 7, 5]) 124 | XCTAssertFalse(h2.isEmpty) 125 | XCTAssertEqual(h2.count, 7) 126 | XCTAssertEqual(h2.peek()!, 1) 127 | 128 | let h3 = Heap(array: [4, 1, 3, 2, 16, 9, 10, 14, 8, 7], sort: <) 129 | XCTAssertTrue(verifyMinHeap(h3)) 130 | XCTAssertFalse(verifyMaxHeap(h3)) 131 | XCTAssertEqual(h3.nodes, [1, 2, 3, 4, 7, 9, 10, 14, 8, 16]) 132 | XCTAssertFalse(h3.isEmpty) 133 | XCTAssertEqual(h3.count, 10) 134 | XCTAssertEqual(h3.peek()!, 1) 135 | 136 | let h4 = Heap(array: [27, 17, 3, 16, 13, 10, 1, 5, 7, 12, 4, 8, 9, 0], sort: <) 137 | XCTAssertTrue(verifyMinHeap(h4)) 138 | XCTAssertFalse(verifyMaxHeap(h4)) 139 | XCTAssertEqual(h4.nodes, [0, 4, 1, 5, 12, 8, 3, 16, 7, 17, 13, 10, 9, 27]) 140 | XCTAssertFalse(h4.isEmpty) 141 | XCTAssertEqual(h4.count, 14) 142 | XCTAssertEqual(h4.peek()!, 0) 143 | } 144 | 145 | func testCreateMaxHeapEqualnodes() { 146 | let heap = Heap(array: [1, 1, 1, 1, 1], sort: >) 147 | XCTAssertTrue(verifyMaxHeap(heap)) 148 | XCTAssertTrue(verifyMinHeap(heap)) 149 | XCTAssertEqual(heap.nodes, [1, 1, 1, 1, 1]) 150 | } 151 | 152 | func testCreateMinHeapEqualnodes() { 153 | let heap = Heap(array: [1, 1, 1, 1, 1], sort: <) 154 | XCTAssertTrue(verifyMinHeap(heap)) 155 | XCTAssertTrue(verifyMaxHeap(heap)) 156 | XCTAssertEqual(heap.nodes, [1, 1, 1, 1, 1]) 157 | } 158 | 159 | fileprivate func randomArray(_ n: Int) -> [Int] { 160 | var a = [Int]() 161 | for _ in 0..) 171 | XCTAssertTrue(verifyMaxHeap(h)) 172 | XCTAssertFalse(h.isEmpty) 173 | XCTAssertEqual(h.count, n) 174 | XCTAssertTrue(isPermutation(a, h.nodes)) 175 | } 176 | } 177 | 178 | func testCreateRandomMinHeap() { 179 | for n in 1...40 { 180 | let a = randomArray(n) 181 | let h = Heap(array: a, sort: <) 182 | XCTAssertTrue(verifyMinHeap(h)) 183 | XCTAssertFalse(h.isEmpty) 184 | XCTAssertEqual(h.count, n) 185 | XCTAssertTrue(isPermutation(a, h.nodes)) 186 | } 187 | } 188 | 189 | func testRemoving() { 190 | var h = Heap(array: [100, 50, 70, 10, 20, 60, 65], sort: >) 191 | XCTAssertTrue(verifyMaxHeap(h)) 192 | XCTAssertEqual(h.nodes, [100, 50, 70, 10, 20, 60, 65]) 193 | 194 | //test index out of bounds 195 | let v = h.remove(at: 10) 196 | XCTAssertEqual(v, nil) 197 | XCTAssertTrue(verifyMaxHeap(h)) 198 | XCTAssertEqual(h.nodes, [100, 50, 70, 10, 20, 60, 65]) 199 | 200 | let v1 = h.remove(at: 5) 201 | XCTAssertEqual(v1, 60) 202 | XCTAssertTrue(verifyMaxHeap(h)) 203 | XCTAssertEqual(h.nodes, [100, 50, 70, 10, 20, 65]) 204 | 205 | let v2 = h.remove(at: 4) 206 | XCTAssertEqual(v2, 20) 207 | XCTAssertTrue(verifyMaxHeap(h)) 208 | XCTAssertEqual(h.nodes, [100, 65, 70, 10, 50]) 209 | 210 | let v3 = h.remove(at: 4) 211 | XCTAssertEqual(v3, 50) 212 | XCTAssertTrue(verifyMaxHeap(h)) 213 | XCTAssertEqual(h.nodes, [100, 65, 70, 10]) 214 | 215 | let v4 = h.remove(at: 0) 216 | XCTAssertEqual(v4, 100) 217 | XCTAssertTrue(verifyMaxHeap(h)) 218 | XCTAssertEqual(h.nodes, [70, 65, 10]) 219 | 220 | XCTAssertEqual(h.peek()!, 70) 221 | let v5 = h.remove() 222 | XCTAssertEqual(v5, 70) 223 | XCTAssertTrue(verifyMaxHeap(h)) 224 | XCTAssertEqual(h.nodes, [65, 10]) 225 | 226 | XCTAssertEqual(h.peek()!, 65) 227 | let v6 = h.remove() 228 | XCTAssertEqual(v6, 65) 229 | XCTAssertTrue(verifyMaxHeap(h)) 230 | XCTAssertEqual(h.nodes, [10]) 231 | 232 | XCTAssertEqual(h.peek()!, 10) 233 | let v7 = h.remove() 234 | XCTAssertEqual(v7, 10) 235 | XCTAssertTrue(verifyMaxHeap(h)) 236 | XCTAssertEqual(h.nodes, []) 237 | 238 | XCTAssertNil(h.peek()) 239 | } 240 | 241 | func testRemoveEmpty() { 242 | var heap = Heap(sort: >) 243 | let removed = heap.remove() 244 | XCTAssertNil(removed) 245 | } 246 | 247 | func testRemoveRoot() { 248 | var h = Heap(array: [15, 13, 9, 5, 12, 8, 7, 4, 0, 6, 2, 1], sort: >) 249 | XCTAssertTrue(verifyMaxHeap(h)) 250 | XCTAssertEqual(h.nodes, [15, 13, 9, 5, 12, 8, 7, 4, 0, 6, 2, 1]) 251 | XCTAssertEqual(h.peek()!, 15) 252 | let v = h.remove() 253 | XCTAssertEqual(v, 15) 254 | XCTAssertTrue(verifyMaxHeap(h)) 255 | XCTAssertEqual(h.nodes, [13, 12, 9, 5, 6, 8, 7, 4, 0, 1, 2]) 256 | } 257 | 258 | func testRemoveRandomItems() { 259 | for n in 1...40 { 260 | var a = randomArray(n) 261 | var h = Heap(array: a, sort: >) 262 | XCTAssertTrue(verifyMaxHeap(h)) 263 | XCTAssertTrue(isPermutation(a, h.nodes)) 264 | 265 | let m = (n + 1)/2 266 | for k in 1...m { 267 | let i = Int.random(in: 0..<(n - k + 1)) 268 | let v = h.remove(at: i)! 269 | let j = a.firstIndex(of: v)! 270 | a.remove(at: j) 271 | 272 | XCTAssertTrue(verifyMaxHeap(h)) 273 | XCTAssertEqual(h.count, a.count) 274 | XCTAssertEqual(h.count, n - k) 275 | XCTAssertTrue(isPermutation(a, h.nodes)) 276 | } 277 | } 278 | } 279 | 280 | func testInsert() { 281 | var h = Heap(array: [15, 13, 9, 5, 12, 8, 7, 4, 0, 6, 2, 1], sort: >) 282 | XCTAssertTrue(verifyMaxHeap(h)) 283 | XCTAssertEqual(h.nodes, [15, 13, 9, 5, 12, 8, 7, 4, 0, 6, 2, 1]) 284 | 285 | h.insert(10) 286 | XCTAssertTrue(verifyMaxHeap(h)) 287 | XCTAssertEqual(h.nodes, [15, 13, 10, 5, 12, 9, 7, 4, 0, 6, 2, 1, 8]) 288 | } 289 | 290 | func testInsertArrayAndRemove() { 291 | var heap = Heap(sort: >) 292 | heap.insert([1, 3, 2, 7, 5, 9]) 293 | XCTAssertEqual(heap.nodes, [9, 5, 7, 1, 3, 2]) 294 | 295 | XCTAssertEqual(9, heap.remove()) 296 | XCTAssertEqual(7, heap.remove()) 297 | XCTAssertEqual(5, heap.remove()) 298 | XCTAssertEqual(3, heap.remove()) 299 | XCTAssertEqual(2, heap.remove()) 300 | XCTAssertEqual(1, heap.remove()) 301 | XCTAssertNil(heap.remove()) 302 | 303 | heap.insert(1) 304 | heap.remove(node: 1) 305 | XCTAssertNil(heap.peek()) 306 | XCTAssertNil(heap.remove(node: 1)) 307 | } 308 | 309 | func testReplace() { 310 | var h = Heap(array: [16, 14, 10, 8, 7, 9, 3, 2, 4, 1], sort: >) 311 | XCTAssertTrue(verifyMaxHeap(h)) 312 | 313 | h.replace(index: 5, value: 13) 314 | XCTAssertTrue(verifyMaxHeap(h)) 315 | 316 | //test index out of bounds 317 | h.replace(index: 20, value: 2) 318 | XCTAssertTrue(verifyMaxHeap(h)) 319 | } 320 | 321 | func testSort() { 322 | var heap = Heap(sort: >) 323 | heap.insert([1, 3, 2, 7, 5, 9]) 324 | XCTAssertEqual(heap.sort(), [1, 2, 3, 5, 7, 9]) 325 | } 326 | 327 | func testHeapsort() { 328 | let array = [1, 3, 2, 7, 5, 9] 329 | XCTAssertEqual(heapsort(array, <), [1, 2, 3, 5, 7, 9]) 330 | } 331 | 332 | static var allTests = [ 333 | ("Test empty - heap", testEmptyHeap), 334 | ("Test is empty - heap", testIsEmpty), 335 | ("Test count - heap", testCount), 336 | ("Test max one element - heap", testMaxHeapOneElement), 337 | ("Test max - heap", testCreateMaxHeap), 338 | ("Test min - heap", testCreateMinHeap), 339 | ("Test max equal nodes - heap", testCreateMaxHeapEqualnodes), 340 | ("Test min equal nodes - heap", testCreateMinHeapEqualnodes), 341 | ("Test random max - heap", testCreateRandomMaxHeap), 342 | ("Test random min - heap", testCreateRandomMinHeap), 343 | ("Test removing - heap", testRemoving), 344 | ("Test remove empty - heap", testRemoveEmpty), 345 | ("Test remove root - heap", testRemoveRoot), 346 | ("Test remove random - heap", testRemoveRandomItems), 347 | ("Test insert - heap", testInsert), 348 | ("Test insert array remove - heap", testInsertArrayAndRemove), 349 | ("Test replace - heap", testReplace), 350 | ("Test sort - heap", testSort), 351 | ("Test heapsort", testHeapsort) 352 | ] 353 | } 354 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HexGrid 2 |

3 | 4 | Swift 5.6+ 5 | 6 | 7 | Swift Package Manager Compatible 8 | 9 | 10 | MIT License 11 | 12 | 13 | Documentation 14 | 15 |

16 | 17 | Development in progress! 18 | 19 | API might change without further notice until first major release 1.x.x. 20 | 21 | The `HexGrid` library provides an easy and intuitive way of working with hexagonal grids. Under the hood it handles all the math so you can focus on more important stuff. 22 | 23 | - Support for grids of any shape, including irregular shapes, or grids with holes in them. 24 | 25 | The library is meant for generic backend use. Therefore it doesn't perform any UI or rendering. However, it provides the calculations that will be needed for rendering. 26 | 27 | ## Features 28 | 29 | - [x] Create or generate a grid of hexagonal cells. 30 | - [x] Various coordinate systems (cube, axial, offset), and conversion between them. 31 | - [x] Rotation, Manhattan distance, linear interpolation. 32 | - [x] Get Neighbors or diagonal neighbors. 33 | - [x] Get Line (get all hexes making a line from A to B). 34 | - [x] Get Ring (get all hexes making a ring from an origin coordinates in specified radius). 35 | - [x] Get Filled Ring (get all hexes making a filled ring from an origin coordinates in specified radius). 36 | - [x] Find reachable hexes within `n` steps (Breath First Search). 37 | - [x] Find the shortest path from A to B (optimized A* search algorithm). 38 | - [x] FieldOfView algorithm (`ShadowCasting` designed for hexagonal grids). 39 | - [x] Hexagon rendering related functions (e.g. get polygon corners). 40 | - [x] Code inline documentation (quick help). 41 | - [x] Solid unit tests coverage. 42 | - [x] Automated documentation generator (SwiftDoc + GitHub Actions -> hosted on repo GitHub Pages). 43 | - [x] Demo with visualization. 44 | 45 | ## What's coming next? 46 | 47 | - [ ] We are done for the moment. Any feature requests or ideas are welcome. 48 | 49 | ## HexGrid in action 50 | 51 | HexGrid [demo app](https://github.com/mgrider/SKHexGrid) using SpriteKit. (Also available [in the App Store](https://apps.apple.com/us/app/hexagon-grid-generator/id6445898427).) 52 | HexGrid [demo app](https://github.com/fananek/HexGrid-SwiftUI-Demo) using SwiftUI. 53 | 54 | ## Getting Started 55 | 56 | ### Integrating HexGrid to your project 57 | 58 | Add HexGrid as a dependency to your `Package.swift` file. 59 | 60 | ```swift 61 | import PackageDescription 62 | 63 | let package = Package( 64 | name: "MyApp", 65 | dependencies: [ 66 | ... 67 | // Add HexGrid package here 68 | .package(url: "https://github.com/fananek/hex-grid.git", from: "0.4.11") 69 | ], 70 | ... 71 | targets: [ 72 | .target(name: "App", dependencies: [ 73 | .product(name: "HexGrid", package: "hex-grid"), 74 | ... 75 | ``` 76 | 77 | Import HexGrid package to your code. 78 | 79 | ```swift 80 | import HexGrid 81 | ... 82 | // your code goes here 83 | ``` 84 | 85 | ## Using 86 | 87 | ### Creating a grid 88 | 89 | Grids can be initialized either with a set of cell coordinates, or HexGrid can generate some standard shaped grids for you. 90 | 91 | #### Standard shape grids 92 | 93 | For example: 94 | 95 | ```swift 96 | ... 97 | // create grid of hexagonal shape 98 | var grid = HexGrid(shape: GridShape.hexagon(10)) 99 | // or rectangular shape 100 | var grid = HexGrid(shape: GridShape.rectangle(8, 12)) 101 | // or triangular shape 102 | var grid = HexGrid(shape: GridShape.triangle(6)) 103 | ``` 104 | 105 | See the section on `GridShape` below for more details, and a full list of the available grid shapes. 106 | 107 | #### Custom grids 108 | 109 | Example: 110 | 111 | ```swift 112 | ... 113 | // create new HexGrid 114 | 115 | let gridCells: Set = try [ 116 | Cell(CubeCoordinates(x: 2, y: -2, z: 0)), 117 | Cell(CubeCoordinates(x: 0, y: -1, z: 1)), 118 | Cell(CubeCoordinates(x: -1, y: 1, z: 0)), 119 | Cell(CubeCoordinates(x: 0, y: 2, z: -2)) 120 | ] 121 | var grid = HexGrid(cells: gridCells) 122 | ... 123 | ``` 124 | 125 | #### Initializers and drawing 126 | 127 | Note that, assuming you want to draw your grid, you'll want to think about whether to pass a size for each cell (`hexSize`) or a size for the entire Grid (`pixelSize`) to the initializer. (See "Drawing the Grid" below.) 128 | 129 | #### HexGrid <-> JSON 130 | 131 | HexGrid conforms to swift `Codable` protocol so it can be easily encoded to or decoded from JSON. 132 | 133 | Example: 134 | 135 | ```swift 136 | // encode (grid to JSON) 137 | let grid = HexGrid(shape: GridShape.hexagon(5) ) 138 | let encoder = JSONEncoder() 139 | let data = try encoder.encode(grid) 140 | ``` 141 | 142 | ```swift 143 | // decode (JSON to grid) 144 | let decoder = JSONDecoder() 145 | let grid = try decoder.decode(HexGrid.self, from: data) 146 | ``` 147 | 148 | ### Grid operations examples 149 | 150 | Almost all functions have two variants. One that works with `Cell` and the other one works with `CubeCoordinates`. Use those which better fulfill your needs. 151 | 152 | #### Get Cell at coordinates 153 | 154 | ```swift 155 | let cell = grid.cellAt(try CubeCoordinates(x: 1, y: 0, z: -1)) 156 | ``` 157 | 158 | #### Validate coordinates 159 | 160 | Check whether a coordinate is valid (meaning it has a corresponding `Cell` in the grid's `cells` array). 161 | 162 | ```swift 163 | // returns Bool 164 | isValidCoordinates(try CubeCoordinates(x: 2, y: 4, z: -6)) 165 | ``` 166 | 167 | #### Get blocked or non blocked Cells 168 | 169 | ```swift 170 | let blockedCells = grid.blockedCells() 171 | // or 172 | let nonBlockedCells = grid.nonBlockedCells() 173 | ``` 174 | 175 | #### Get single neighbor 176 | 177 | ```swift 178 | // get neighbor for a specific Cell 179 | let neighbor = try grid.neighbor( 180 | for: someCell, 181 | at: Direction.Pointy.northEast.rawValue) 182 | 183 | // get just neighbor coordinates 184 | let neighborCoordinates = try grid.neighborCoordinates( 185 | for: someCoordinates, 186 | at: Direction.Pointy.northEast.rawValue) 187 | ``` 188 | 189 | #### Get all neighbors 190 | 191 | ```swift 192 | // get all neighbors for a specific Cell 193 | let neighbors = try grid.neighbors(for: someCell) 194 | 195 | // get only coordinates of all neighbors 196 | let neighborsCoords = try grid.neighbors(for: someCoordinates) 197 | ``` 198 | 199 | #### Get line from A to B 200 | 201 | ```swift 202 | // returns nil in case line doesn't exist 203 | let line = try grid.line(from: originCell, to: targetCell) 204 | ``` 205 | 206 | #### Get ring 207 | 208 | ```swift 209 | // returns all cells making a ring from origin cell in radius 210 | let ring = try grid.ring(from: originCell, in: 2) 211 | ``` 212 | 213 | #### Get filled ring 214 | 215 | ```swift 216 | // returns all cells making a filled ring from origin cell in radius 217 | let ring = try grid.filledRing(from: originCell, in: 2) 218 | ``` 219 | 220 | #### Find reachable cells 221 | 222 | ```swift 223 | // find all reachable cells (max. 4 steps away from origin) 224 | let reachableCells = try grid.findReachable(from: origin, in: 4) 225 | ``` 226 | 227 | #### Find shortest path 228 | 229 | ```swift 230 | // returns nil in case path doesn't exist at all 231 | let path = try grid.findPath(from: originCell, to: targetCell) 232 | ``` 233 | 234 | #### Calculate field of view (FOV) 235 | 236 | `Cell` has an attribute called `isOpaque`. Its value can be `true` or `false`. Based on this information it's possible to calculate so called **field of view**. It means all cells visible from specific position on grid, considering all opaque obstacles. 237 | 238 | ```swift 239 | // set cell as opaque 240 | obstacleCell.isOpaque = true 241 | ``` 242 | 243 | In order to get field of view, simply call following function. 244 | 245 | ```swift 246 | // find all hexes visible in radius 4 from origin cell 247 | let visibleHexes = try grid.fieldOfView(from: originCell, in: 4) 248 | ``` 249 | 250 | By default cell is considered visible as soon as its center is visible from the origin cell. If you want to include partially visible cells as well, use optional paramter `includePartiallyVisible`. 251 | 252 | ```swift 253 | // find all hexes even partially visible in radius 4 from origin cell 254 | let visibleHexesIncludingPartials = try grid.fieldOfView(from: originCell, in: 4, includePartiallyVisible: true) 255 | ``` 256 | 257 | ### Drawing the Grid 258 | 259 | Internally, `HexGrid` calculates all "pixel coordinates" using one of two methods: 260 | 261 | 1. Using a size for each `Cell` (stored in the `hexSize` property). 262 | 263 | *...or...* 264 | 265 | 2. Using a size for the entire Grid (stored in the `pixelSize` property). 266 | 267 | > *Note that both the `hexSize` and `pixelSize` properties are stored internally as `HexSize` structures. Try not to get confused by this! `HexSize` is just a convenient way to store `width` and `height` values.* 268 | 269 | Which flavor of `HexGrid` initializer you want to use will depend on which of these methods best applies to your use case. When specifying the `hexSize`, the `pixelSize` is calculated for you, and when specifying the `pixelSize`, the `hexSize` is likewise set automatically. 270 | 271 | While it is not possible to modify the `hexSize` or `pixelSize` properties directly (after initialization), you can set the grid's `pixelSize` (and re-calculate `hexSize` from it) at any time using the `fitGrid(in size: HexSize)` function. Note that this also resets the `origin` property. 272 | 273 | #### The origin property 274 | 275 | You can think of the `HexGrid`'s' `origin` property as the center point of the `Cell` at `CubeCoordinate` `0,0,0`. 276 | 277 | > *Note that you can specify the `origin` at initialization, but only when using the `cellSize` method. When specifying `pixelSize`, the origin is set for you, so the grid "fits" inside the specified width & height.* 278 | 279 | It will be important to change the `origin` property any time you want to change the pixel coordinates for your Grid. *Changing the `origin` will modify the return values of all pixel-calculating functions.* You can use this to apply an offset for your grid, or "re-center" it later. 280 | 281 | #### Corner Pixel Coordinates 282 | 283 | Usually, when drawing hexagons, you will want the screen coordinates of the polygon corners for each `Cell`. 284 | 285 | ```swift 286 | let corners = grid.polygonCorners(for: someCell) 287 | ``` 288 | 289 | #### Center Pixel Coordinates 290 | 291 | This function returns a `Point` struct (x: and y: values) for the center of a `Cell`. 292 | 293 | ```swift 294 | let screenCoords = grid.pixelCoordinates(for: someCell) 295 | ``` 296 | 297 | #### Finding a Cell at screen coordinates 298 | 299 | ```swift 300 | // return cell for specified screen coordinates (or nil if such cell doesn't exists) 301 | let cell = try grid.cellAt(point) 302 | ``` 303 | 304 | ## Implementation fundamentals 305 | 306 | For detailed information see complete [documentation](https://fananek.github.io/hex-grid/) 307 | 308 | ### Data structures you should know 309 | 310 | #### HexGrid 311 | 312 | Represents the grid itself as well as it's an entry point of the HexGrid library. 313 | 314 | `HexGrid` is defined by set of `Cells` and few other properties. All together makes a grid setup. In other words it put grid cells into a meaningful context. Therefore most of available operations are being called directly on a grid instance because it make sense only with such context (grid setup). 315 | 316 | Properties: 317 | 318 | - cells: `Set` - grid cells 319 | - orientation: `Orientation` - see [Orientation enumeration](#Orientation) 320 | - offsetLayout: `OffsetLayout` - see [OffsetLayout enumeration](#OffsetLayout) 321 | - hexSize: `HexSize` - width and height of a hexagon 322 | - origin: `Point` - 'display coordinates' (x, y) of a grid origin 323 | - pixelSize: `HexSize` - pixel width and height of the entire grid 324 | - attributes: `[String: Attribute]` - dictionary of custom attributes (most primitive types are supported as well as nesting) 325 | 326 | #### Cell 327 | 328 | Cell is a building block of a grid. 329 | 330 | Properties: 331 | 332 | - coordinates: `CubeCoordinates` - cell placement on a grid coordinate system 333 | - attributes: `[String: Attribute]` - dictionary of custom attributes (most primitive types are supported as well as nesting) 334 | - isBlocked: `Bool` - used by algorithms (reachableCells, pathfinding etc.) 335 | - isOpaque: `Bool` - used by fieldOfView algorithm 336 | - cost: `Float` - used by pathfinding algorithm. For the sake of simplicity let's put graph theory aside. You can imagine cost as an amount of energy needed to pass a cell. Pathfinding algorithm then search for path requiring the less effort. 337 | 338 | #### CubeCoordinates 339 | 340 | The most common coordinates used within HexGrid library is cube coordinate system. This type of coordinates has three axis x, y and z. The only condition is that sum of its all values has to be equal zero. 341 | 342 | ```swift 343 | // valid cube coordinates 344 | CubeCoordinates(x: 1, y: 0, z: -1) -> sum = 0 345 | 346 | // invalid cube coordinates 347 | CubeCoordinates(x: 1, y: 1, z: -1) -> sum = 1 -> throws error 348 | ``` 349 | 350 | For more details check [Amit Patel's explanation](https://www.redblobgames.com/grids/hexagons/#coordinates-cube). 351 | 352 | #### Enumerations 353 | 354 | ##### Orientation 355 | 356 | Options: 357 | 358 | - `pointyOnTop` - ⬢ 359 | - `flatOnTop` - ⬣ 360 | 361 | ##### OffsetLayout 362 | 363 | OffsetLayout is used primarily for rectangular shaped grids. It has two options but their meaning can differ based on grid orientation. 364 | 365 | Options: 366 | 367 | - `odd` 368 | - `even` 369 | 370 | There are four offset types depending on orientation of hexagons. The “row” types are used with with pointy top hexagons and the “column” types are used with flat top. 371 | 372 | - Pointy on top orientation 373 | - odd-row (slide alternate rows right) 374 | - even-row (slide alternate rows left) 375 | - Flat on top orientation 376 | - odd-column (slide alternate columns up) 377 | - even-column (slide alternate columns down) 378 | 379 | ##### GridShape 380 | 381 | Cases from this enumeration can be passed into the `HexGrid` constructor to generate grids of various shapes and sizes. 382 | 383 | Options: 384 | 385 | - `rectangle(Int, Int)` - Associated values are column width and row height of the generated grid. Sides of the grid will be essentially parallel to the edges of the screen. 386 | - `parallelogram(Int, Int)` - Associated values are both side-lengths. 387 | - `hexagon(Int)` - This generates a regular hexagon shaped grid, with all sides the length of the associated value. 388 | - `elongatedHexagon(Int, Int)` - This is used to generate a grid shaped like a regular hexagon that has been stretched or elongated in one dimension. The associated values are side lengths. 389 | - `irregularHexagon(Int, Int)` - This is used to generate a grid shaped like a hexagon where every other side is the length of the two associated values. 390 | - `triangle(Int)` - Generates an equilateral triangle with all sides the length of the associated value. 391 | 392 | ##### Rotation 393 | 394 | Options: 395 | 396 | - `left` 397 | - `right` 398 | 399 | ##### Direction 400 | 401 | Direction enumeration is consistent and human recognizable direction naming. Using direction enumeration is not only much more convenient but it also help to avoid errors. It's better to say just *"Hey, I'm on cell X and want to go north."* than think *"What the hack was that index of north direction?"*, isn't it? 402 | 403 | Options: 404 | 405 | - `Direction.Flat` 406 | - `north` 407 | - `northEast` 408 | - `southEast` 409 | - `south` 410 | - `southWest` 411 | - `northWest` 412 | - `Direction.Pointy` 413 | - `northEast` 414 | - `east` 415 | - `southEast` 416 | - `southWest` 417 | - `west` 418 | - `northWest` 419 | 420 | There is actually separate set of directions for each grid orientation. It's because of two reasons. First, some directions are valid only for one or another orientation. Second, direction raw values are shifted based on orientation. 421 | 422 | ## Authors 423 | 424 | - [@fananek](https://github.com/fananek) 425 | - [@mlhovka](https://github.com/mlhovka) 426 | 427 | See also the list of [contributors](https://github.com/fananek/HexGrid/contributors) who participated in this project. 428 | 429 | ## License 430 | 431 | All code contained in the HexGrid package is under the [MIT](https://github.com/fananek/HexGrid/blob/master/LICENSE.md) license agreement. 432 | -------------------------------------------------------------------------------- /Tests/HexGridTests/MathTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HexGrid 3 | 4 | class MathTests: XCTestCase { 5 | 6 | // Coordinates Math test 7 | /// Test add operation 8 | func testAddOperation() throws { 9 | let aCoord = try CubeCoordinates(x: 1, y: 1, z: -2) 10 | let bCoord = try CubeCoordinates(x: 2, y: 0, z: -2) 11 | let result = try Math.add(a: aCoord, b: bCoord) 12 | XCTAssertEqual(result.x, 3) 13 | XCTAssertEqual(result.y, 1) 14 | XCTAssertEqual(result.z, -4) 15 | } 16 | 17 | /// Test subtract operation 18 | func testSubtractOperation() throws { 19 | let aCoord = try CubeCoordinates(x: 1, y: 1, z: -2) 20 | let bCoord = try CubeCoordinates(x: 2, y: 0, z: -2) 21 | let result = try Math.subtract(a: aCoord, b: bCoord) 22 | XCTAssertEqual(result.x, -1) 23 | XCTAssertEqual(result.y, 1) 24 | XCTAssertEqual(result.z, 0) 25 | } 26 | 27 | /// Test scale operation 28 | func testScaleOperation() throws { 29 | let coord = try CubeCoordinates(x: 1, y: 0, z: -1) 30 | let result = try Math.scale(a: coord, c: 3) 31 | XCTAssertEqual(result.x, 3) 32 | XCTAssertEqual(result.y, 0) 33 | XCTAssertEqual(result.z, -3) 34 | } 35 | 36 | /// Test length 37 | func testLenght() throws { 38 | let coord = try CubeCoordinates(x: 1, y: 1, z: -2) 39 | let distance = Math.length(coordinates: coord) 40 | XCTAssertEqual(distance, 2) 41 | } 42 | 43 | /// Test distance 44 | func testDistance() throws { 45 | let aCoord = try CubeCoordinates(x: 2, y: -1, z: -1) 46 | let bCoord = try CubeCoordinates(x: 0, y: 4, z: -4) 47 | let distance = try Math.distance(from: aCoord, to: bCoord) 48 | XCTAssertEqual(distance, 5) 49 | } 50 | 51 | /// Test direction vector 52 | func testDirection() throws { 53 | var index = 0 54 | var direction = Math.direction(at: index) 55 | XCTAssertEqual(direction.x, 1) 56 | XCTAssertEqual(direction.y, 0) 57 | XCTAssertEqual(direction.z, -1) 58 | 59 | // test negative index outside the boundries 60 | index = -1 61 | direction = Math.direction(at: index) 62 | XCTAssertEqual(direction.x, 0) 63 | XCTAssertEqual(direction.y, 1) 64 | XCTAssertEqual(direction.z, -1) 65 | } 66 | 67 | /// Test neighbor 68 | func testNeighbor() throws { 69 | var coord = try CubeCoordinates(x: 0, y: 0, z: 0) 70 | var neighbor = try Math.neighbor(at: 0, origin: coord) 71 | XCTAssertEqual(neighbor.x, 1) 72 | XCTAssertEqual(neighbor.y, 0) 73 | XCTAssertEqual(neighbor.z, -1) 74 | 75 | coord = try CubeCoordinates(x: 1, y: -1, z: 0) 76 | neighbor = try Math.neighbor(at: 3, origin: coord) 77 | XCTAssertEqual(neighbor.x, 0) 78 | XCTAssertEqual(neighbor.y, -1) 79 | XCTAssertEqual(neighbor.z, 1) 80 | 81 | coord = try CubeCoordinates(x: 1, y: -1, z: 0) 82 | neighbor = try Math.neighbor(at: 1, origin: coord) 83 | XCTAssertEqual(neighbor.x, 2) 84 | XCTAssertEqual(neighbor.y, -2) 85 | XCTAssertEqual(neighbor.z, 0) 86 | } 87 | 88 | /// Test diagonal neighbor 89 | func testDiagonalNeighbor() throws { 90 | let coord = try CubeCoordinates(x: 0, y: 0, z: 0) 91 | let neighbor = try Math.diagonalNeighbor(at: 0, origin: coord) 92 | XCTAssertEqual(neighbor.x, 2) 93 | XCTAssertEqual(neighbor.y, -1) 94 | XCTAssertEqual(neighbor.z, -1) 95 | } 96 | 97 | /// Test left rotation 98 | func testRotateLeft() throws { 99 | let coord = try CubeCoordinates(x: 1, y: 0, z: -1) 100 | let result = try Math.rotateLeft(coordinates: coord) 101 | XCTAssertEqual(result.x, 0) 102 | XCTAssertEqual(result.y, 1) 103 | XCTAssertEqual(result.z, -1) 104 | } 105 | 106 | /// Test right rotation 107 | func testRotateRight() throws { 108 | let coord = try CubeCoordinates(x: 1, y: 0, z: -1) 109 | let result = try Math.rotateRight(coordinates: coord) 110 | XCTAssertEqual(result.x, 1) 111 | XCTAssertEqual(result.y, -1) 112 | XCTAssertEqual(result.z, 0) 113 | } 114 | 115 | /// Test rounding and linear interpolation 116 | func testRound() throws { 117 | let a = try CubeFractionalCoordinates(x: 0.0, y: 0.0, z: 0.0); 118 | let b = try CubeFractionalCoordinates(x: 1.0, y: -1.0, z: 0.0); 119 | let c = try CubeFractionalCoordinates(x: 0.0, y: -1.0, z: 1.0); 120 | let d = try CubeFractionalCoordinates(x: a.x * 0.4 + b.x * 0.3 + c.x * 0.3, y: a.y * 0.4 + b.y * 0.3 + c.y * 0.3, z: a.z * 0.4 + b.z * 0.3 + c.z * 0.3) 121 | let e = try CubeFractionalCoordinates(x: a.x * 0.3 + b.x * 0.3 + c.x * 0.4, y: a.y * 0.4 + b.y * 0.3 + c.y * 0.4, z: a.z * 0.4 + b.z * 0.3 + c.z * 0.4) 122 | XCTAssertEqual(try Math.lerpCube(a: CubeFractionalCoordinates(x: 0.0, y: 0.0, z: 0.0), b: CubeFractionalCoordinates(x: 10.0, y: -20.0, z: 10.0), f: 0.5), try CubeCoordinates(x: 5, y: -10, z: 5)) 123 | XCTAssertEqual(Math.lerpCube(a: a, b: b, f: 0.499), CubeCoordinates(x: a.x, y: a.y, z: a.z)) 124 | XCTAssertEqual(Math.lerpCube(a: a, b: b, f: 0.501), CubeCoordinates(x: b.x, y: b.y, z: b.z)) 125 | XCTAssertEqual(CubeCoordinates(x: d.x, y: d.y, z: d.z), CubeCoordinates(x: a.x, y: a.y, z: a.z)) 126 | XCTAssertEqual(CubeCoordinates(x: e.x, y: e.y, z: e.z), CubeCoordinates(x: c.x, y: c.y, z: c.z)) 127 | } 128 | 129 | /// Test calculation of hexagon corners 130 | func testHexCorners() throws { 131 | let hex = try CubeCoordinates(x: 0, y: 0, z: 0) 132 | let hexSize = HexSize(width: 10.0, height: 10.0) 133 | let gridOrigin = Point(x: 0.0, y: 0.0) 134 | let cornersForPointy = Math.hexCorners( 135 | coordinates: hex, 136 | hexSize: hexSize, 137 | origin: gridOrigin, 138 | orientation: Orientation.pointyOnTop) 139 | let expectedCornersForPointy: [Point] = [ 140 | Point(x: 8.6602540378443873, y: 4.9999999999999991), 141 | Point(x: 0.00000000000000061232339957367663, y: 10.0), 142 | Point(x: -8.6602540378443873, y: 4.9999999999999991), 143 | Point(x: -8.660254037844389, y: -4.9999999999999982), 144 | Point(x: -0.0000000000000018369701987210296, y: -10.0), 145 | Point(x: 8.6602540378443837, y: -5.0000000000000044) 146 | ] 147 | XCTAssertEqual(cornersForPointy[0].x, expectedCornersForPointy[0].x, accuracy: 0.0001) 148 | XCTAssertEqual(cornersForPointy[0].y, expectedCornersForPointy[0].y, accuracy: 0.0001) 149 | XCTAssertEqual(cornersForPointy[1].x, expectedCornersForPointy[1].x, accuracy: 0.0001) 150 | XCTAssertEqual(cornersForPointy[1].y, expectedCornersForPointy[1].y, accuracy: 0.0001) 151 | XCTAssertEqual(cornersForPointy[2].x, expectedCornersForPointy[2].x, accuracy: 0.0001) 152 | XCTAssertEqual(cornersForPointy[2].y, expectedCornersForPointy[2].y, accuracy: 0.0001) 153 | XCTAssertEqual(cornersForPointy[3].x, expectedCornersForPointy[3].x, accuracy: 0.0001) 154 | XCTAssertEqual(cornersForPointy[3].y, expectedCornersForPointy[3].y, accuracy: 0.0001) 155 | XCTAssertEqual(cornersForPointy[4].x, expectedCornersForPointy[4].x, accuracy: 0.0001) 156 | XCTAssertEqual(cornersForPointy[4].y, expectedCornersForPointy[4].y, accuracy: 0.0001) 157 | XCTAssertEqual(cornersForPointy[5].x, expectedCornersForPointy[5].x, accuracy: 0.0001) 158 | XCTAssertEqual(cornersForPointy[5].y, expectedCornersForPointy[5].y, accuracy: 0.0001) 159 | 160 | let cornersForFlat = Math.hexCorners( 161 | coordinates: hex, 162 | hexSize: hexSize, 163 | origin: gridOrigin, 164 | orientation: Orientation.flatOnTop) 165 | let expectedCornersForFlat: [Point] = [ 166 | Point(x: 10.0, y: 0.0), 167 | Point(x: 5.0000000000000009, y: 8.6602540378443855), 168 | Point(x: -4.9999999999999982, y: 8.660254037844389), 169 | Point(x: -10.0, y: 0.0000000000000012246467991473533), 170 | Point(x: -5.0000000000000044, y: -8.6602540378443837), 171 | Point(x: 5.0, y: -8.6602540378443855) 172 | ] 173 | XCTAssertEqual(cornersForFlat[0].x, expectedCornersForFlat[0].x, accuracy: 0.0001) 174 | XCTAssertEqual(cornersForFlat[0].y, expectedCornersForFlat[0].y, accuracy: 0.0001) 175 | XCTAssertEqual(cornersForFlat[1].x, expectedCornersForFlat[1].x, accuracy: 0.0001) 176 | XCTAssertEqual(cornersForFlat[1].y, expectedCornersForFlat[1].y, accuracy: 0.0001) 177 | XCTAssertEqual(cornersForFlat[2].x, expectedCornersForFlat[2].x, accuracy: 0.0001) 178 | XCTAssertEqual(cornersForFlat[2].y, expectedCornersForFlat[2].y, accuracy: 0.0001) 179 | XCTAssertEqual(cornersForFlat[3].x, expectedCornersForFlat[3].x, accuracy: 0.0001) 180 | XCTAssertEqual(cornersForFlat[3].y, expectedCornersForFlat[3].y, accuracy: 0.0001) 181 | XCTAssertEqual(cornersForFlat[4].x, expectedCornersForFlat[4].x, accuracy: 0.0001) 182 | XCTAssertEqual(cornersForFlat[4].y, expectedCornersForFlat[4].y, accuracy: 0.0001) 183 | XCTAssertEqual(cornersForFlat[5].x, expectedCornersForFlat[5].x, accuracy: 0.0001) 184 | XCTAssertEqual(cornersForFlat[5].y, expectedCornersForFlat[5].y, accuracy: 0.0001) 185 | } 186 | 187 | /// Test line drawing 188 | func testLinedraw() throws { 189 | let line = try Math.line(from: CubeCoordinates(x: 0, y: 0, z: 0), to: CubeCoordinates(x: 1, y: -5, z: 4)) 190 | let testSet: Set = try [ 191 | CubeCoordinates(x: 0, y: 0, z: 0), 192 | CubeCoordinates(x: 0, y: -1, z: 1), 193 | CubeCoordinates(x: 0, y: -2, z: 2), 194 | CubeCoordinates(x: 1, y: -3, z: 2), 195 | CubeCoordinates(x: 1, y: -4, z: 3), 196 | CubeCoordinates(x: 1, y: -5, z: 4) 197 | ] 198 | XCTAssertEqual(line, testSet) 199 | } 200 | 201 | /// Test ring 202 | func testRing() throws { 203 | let ring1 = try Math.ring(from: CubeCoordinates(x: 1, y: -1, z: 0), in: 1) 204 | let testSet1: Set = try [ 205 | CubeCoordinates(x: 0, y: -1, z: 1), 206 | CubeCoordinates(x: 0, y: 0, z: 0), 207 | CubeCoordinates(x: 1, y: 0, z: -1), 208 | CubeCoordinates(x: 2, y: -1, z: -1), 209 | CubeCoordinates(x: 2, y: -2, z: 0), 210 | CubeCoordinates(x: 1, y: -2, z: 1) 211 | ] 212 | XCTAssertEqual(ring1, testSet1) 213 | 214 | let ring2 = try Math.ring(from: CubeCoordinates(x: 1, y: -1, z: 0), in: 2) 215 | let testSet2: Set = try [ 216 | CubeCoordinates(x: -1, y: -1, z: 2), 217 | CubeCoordinates(x: 0, y: -2, z: 2), 218 | CubeCoordinates(x: 1, y: -3, z: 2), 219 | CubeCoordinates(x: 2, y: -3, z: 1), 220 | CubeCoordinates(x: 3, y: -3, z: 0), 221 | CubeCoordinates(x: 3, y: -2, z: -1), 222 | CubeCoordinates(x: 3, y: -1, z: -2), 223 | CubeCoordinates(x: 2, y: 0, z: -2), 224 | CubeCoordinates(x: 1, y: 1, z: -2), 225 | CubeCoordinates(x: 0, y: 1, z: -1), 226 | CubeCoordinates(x: -1, y: 1, z: 0), 227 | CubeCoordinates(x: -1, y: 0, z: 1) 228 | ] 229 | XCTAssertEqual(ring2, testSet2) 230 | } 231 | 232 | /// Test zero ring 233 | func testZeroRing() throws { 234 | let ring = try Math.ring(from: CubeCoordinates(x: 1, y: -1, z: 0), in: 0) 235 | let testSet: Set = try [ 236 | CubeCoordinates(x: 1, y: -1, z: 0), 237 | ] 238 | XCTAssertEqual(ring, testSet) 239 | } 240 | 241 | /// Test invalid ring 242 | func testInvalidRing() throws { 243 | XCTAssertThrowsError(try Math.ring(from: CubeCoordinates(x: 1, y: -1, z: 0), in: -1)) 244 | } 245 | 246 | /// Test filled ring 247 | func testFilledRing() throws { 248 | let filledRing = try Math.filledRing(from: CubeCoordinates(x: 0, y: 0, z: 0), in: 1) 249 | let testSet: Set = try [ 250 | CubeCoordinates(x: 0, y: 0, z: 0), 251 | CubeCoordinates(x: 1, y: 0, z: -1), 252 | CubeCoordinates(x: 1, y: -1, z: 0), 253 | CubeCoordinates(x: 0, y: -1, z: 1), 254 | CubeCoordinates(x: -1, y: 0, z: 1), 255 | CubeCoordinates(x: -1, y: 1, z: 0), 256 | CubeCoordinates(x: 0, y: 1, z: -1) 257 | ] 258 | XCTAssertEqual(filledRing, testSet) 259 | } 260 | 261 | // Test invalid filled ring 262 | func testInvalidFilledRing() throws { 263 | XCTAssertThrowsError(try Math.filledRing(from: CubeCoordinates(x: 0, y: 0, z: 0), in: -5)) 264 | } 265 | 266 | func testFieldOfView () throws { 267 | let grid = HexGrid(shape: GridShape.hexagon(3)) 268 | grid.cellAt(try CubeCoordinates(x: -1, y: 1, z: 0))?.isOpaque = true 269 | grid.cellAt(try CubeCoordinates(x: -1, y: 0, z: 1))?.isOpaque = true 270 | grid.cellAt(try CubeCoordinates(x: 1, y: -1, z: 0))?.isOpaque = true 271 | grid.cellAt(try CubeCoordinates(x: 1, y: 1, z: -2))?.isOpaque = true 272 | let fovSet = try Math.calculateFieldOfView(from: CubeCoordinates(x: 0, y: 0, z: 0), in: 3, on: grid) 273 | let testSet: Set = try [ 274 | // origin/center 275 | CubeCoordinates(x: 0, y: 0, z: 0), 276 | // 1st ring 277 | CubeCoordinates(x: -1, y: 1, z: 0), // isOpauqe 278 | CubeCoordinates(x: 0, y: 1, z: -1), 279 | CubeCoordinates(x: 1, y: 0, z: -1), 280 | CubeCoordinates(x: 1, y: -1, z: 0), // isOpaque 281 | CubeCoordinates(x: 0, y: -1, z: 1), 282 | CubeCoordinates(x: -1, y: 0, z: 1), // isOpauqe 283 | 284 | // 2nd ring 285 | // CubeCoordinates(x: -2, y: 2, z: 0), // is shaded = true 286 | // CubeCoordinates(x: -1, y: 2, z: -1), // is shaded = true 287 | CubeCoordinates(x: 0, y: 2, z: -2), 288 | CubeCoordinates(x: 1, y: 1, z: -2), 289 | CubeCoordinates(x: 2, y: 0, z: -2), 290 | // CubeCoordinates(x: 2, y: -1, z: -1), // is shaded = true 291 | // CubeCoordinates(x: 2, y: -2, z: 0), // is shaded = true 292 | // CubeCoordinates(x: 1, y: -2, z: 1), // is shaded = true 293 | CubeCoordinates(x: 0, y: -2, z: 2), 294 | // CubeCoordinates(x: -1, y: -1, z: 2), // is shaded = true 295 | // CubeCoordinates(x: -2, y: 0, z: 2), // is shaded = true 296 | // CubeCoordinates(x: -2, y: 1, z: 1), // is shaded = true 297 | 298 | // 3rd ring 299 | // CubeCoordinates(x: -3, y: 3, z: 0), // is shaded = true 300 | // CubeCoordinates(x: -2, y: 3, z: -1), // is shaded = true 301 | CubeCoordinates(x: -1, y: 3, z: -2), 302 | CubeCoordinates(x: 0, y: 3, z: -3), 303 | // CubeCoordinates(x: 1, y: 2, z: -3), // is shaded = true 304 | // CubeCoordinates(x: 2, y: 1, z: -3), // is shaded = true 305 | CubeCoordinates(x: 3, y: 0, z: -3), 306 | CubeCoordinates(x: 3, y: -1, z: -2), 307 | // CubeCoordinates(x: 3, y: -2, z: -1), // is shaded = true 308 | // CubeCoordinates(x: 3, y: -3, z: 0), // is shaded = true 309 | // CubeCoordinates(x: 2, y: -3, z: 1), // is shaded = true 310 | CubeCoordinates(x: 1, y: -3, z: 2), 311 | CubeCoordinates(x: 0, y: -3, z: 3), 312 | CubeCoordinates(x: -1, y: -2, z: 3), 313 | // CubeCoordinates(x: -2, y: -1, z: 3), // is shaded = true 314 | // CubeCoordinates(x: -3, y: 0, z: 3), // is shaded = true 315 | // CubeCoordinates(x: -3, y: 1, z: 2), // is shaded = true 316 | // CubeCoordinates(x: -3, y: 2, z: 1) // is shaded = true 317 | ] 318 | XCTAssertEqual(testSet, fovSet) 319 | XCTAssertThrowsError(try Math.calculateFieldOfView(from: CubeCoordinates(x: 0, y: 0, z: 0), in: -5, on: grid)) 320 | } 321 | 322 | func testFieldOfViewIncludingPartials () throws { 323 | let grid = HexGrid(shape: GridShape.hexagon(3)) 324 | grid.cellAt(try CubeCoordinates(x: -1, y: 1, z: 0))?.isOpaque = true 325 | grid.cellAt(try CubeCoordinates(x: -1, y: 0, z: 1))?.isOpaque = true 326 | grid.cellAt(try CubeCoordinates(x: 1, y: -1, z: 0))?.isOpaque = true 327 | grid.cellAt(try CubeCoordinates(x: 1, y: 1, z: -2))?.isOpaque = true 328 | let fovSet = try Math.calculateFieldOfView(from: CubeCoordinates(x: 0, y: 0, z: 0), in: 3, on: grid, includePartiallyVisible: true) 329 | let testSet: Set = try [ 330 | // origin/center 331 | CubeCoordinates(x: 0, y: 0, z: 0), 332 | // 1st ring 333 | CubeCoordinates(x: -1, y: 1, z: 0), // isOpauqe 334 | CubeCoordinates(x: 0, y: 1, z: -1), 335 | CubeCoordinates(x: 1, y: 0, z: -1), 336 | CubeCoordinates(x: 1, y: -1, z: 0), // isOpaque 337 | CubeCoordinates(x: 0, y: -1, z: 1), 338 | CubeCoordinates(x: -1, y: 0, z: 1), // isOpauqe 339 | 340 | // 2nd ring 341 | // CubeCoordinates(x: -2, y: 2, z: 0), // is shaded = true 342 | CubeCoordinates(x: -1, y: 2, z: -1), 343 | CubeCoordinates(x: 0, y: 2, z: -2), 344 | CubeCoordinates(x: 1, y: 1, z: -2), 345 | CubeCoordinates(x: 2, y: 0, z: -2), 346 | CubeCoordinates(x: 2, y: -1, z: -1), 347 | // CubeCoordinates(x: 2, y: -2, z: 0), // is shaded = true 348 | CubeCoordinates(x: 1, y: -2, z: 1), 349 | CubeCoordinates(x: 0, y: -2, z: 2), 350 | CubeCoordinates(x: -1, y: -1, z: 2), 351 | // CubeCoordinates(x: -2, y: 0, z: 2), // is shaded = true 352 | // CubeCoordinates(x: -2, y: 1, z: 1), // is shaded = true 353 | 354 | // 3rd ring 355 | // CubeCoordinates(x: -3, y: 3, z: 0), // is shaded = true 356 | // CubeCoordinates(x: -2, y: 3, z: -1), // is shaded = true 357 | CubeCoordinates(x: -1, y: 3, z: -2), 358 | CubeCoordinates(x: 0, y: 3, z: -3), 359 | CubeCoordinates(x: 1, y: 2, z: -3), 360 | CubeCoordinates(x: 2, y: 1, z: -3), 361 | CubeCoordinates(x: 3, y: 0, z: -3), 362 | CubeCoordinates(x: 3, y: -1, z: -2), 363 | // CubeCoordinates(x: 3, y: -2, z: -1), // is shaded = true 364 | // CubeCoordinates(x: 3, y: -3, z: 0), // is shaded = true 365 | // CubeCoordinates(x: 2, y: -3, z: 1), // is shaded = true 366 | CubeCoordinates(x: 1, y: -3, z: 2), 367 | CubeCoordinates(x: 0, y: -3, z: 3), 368 | CubeCoordinates(x: -1, y: -2, z: 3), 369 | // CubeCoordinates(x: -2, y: -1, z: 3), // is shaded = true 370 | // CubeCoordinates(x: -3, y: 0, z: 3), // is shaded = true 371 | // CubeCoordinates(x: -3, y: 1, z: 2), // is shaded = true 372 | // CubeCoordinates(x: -3, y: 2, z: 1) // is shaded = true 373 | ] 374 | XCTAssertEqual(testSet, fovSet) 375 | XCTAssertThrowsError(try Math.calculateFieldOfView(from: CubeCoordinates(x: 0, y: 0, z: 0), in: -5, on: grid)) 376 | } 377 | 378 | /// Test breadthFirstSearch algorithm 379 | func testBreadthFirstSearch() throws { 380 | let grid = HexGrid(shape: GridShape.hexagon(3)) 381 | grid.cellAt(try CubeCoordinates(x: 1, y: 0, z: -1))?.isBlocked = true 382 | grid.cellAt(try CubeCoordinates(x: 0, y: 1, z: -1))?.isBlocked = true 383 | let searchResultSet = try Math.breadthFirstSearch(from: CubeCoordinates(x: 0, y: 0, z: 0), in: 2, on: grid) 384 | let testSet: Set = try [ 385 | CubeCoordinates(x: 0, y: 0, z: 0), 386 | // CubeCoordinates(x: 1, y: 0, z: -1), // Blocked hex 387 | CubeCoordinates(x: 1, y: -1, z: 0), 388 | CubeCoordinates(x: 0, y: -1, z: 1), 389 | CubeCoordinates(x: -1, y: 0, z: 1), 390 | CubeCoordinates(x: -1, y: 1, z: 0), 391 | // CubeCoordinates(x: 0, y: 1, z: -1), // Blocked hex 392 | // CubeCoordinates(x: 2, y: 0, z: -2), // Unreachable hex 393 | CubeCoordinates(x: 2, y: -1, z: -1), 394 | CubeCoordinates(x: 2, y: -2, z: 0), 395 | CubeCoordinates(x: 1, y: -2, z: 1), 396 | CubeCoordinates(x: 0, y: -2, z: 2), 397 | CubeCoordinates(x: -1, y: -1, z: 2), 398 | CubeCoordinates(x: -2, y: 0, z: 2), 399 | CubeCoordinates(x: -2, y: 1, z: 1), 400 | CubeCoordinates(x: -2, y: 2, z: 0), 401 | CubeCoordinates(x: -1, y: 2, z: -1), 402 | // CubeCoordinates(x: 0, y: 2, z: -2), // Unreachable hex 403 | // CubeCoordinates(x: 1, y: 1, z: -2) // Unreachable hex 404 | ] 405 | XCTAssertEqual(testSet, searchResultSet) 406 | XCTAssertThrowsError(try Math.breadthFirstSearch(from: CubeCoordinates(x: 0, y: 0, z: 0), in: -5, on: grid)) 407 | } 408 | 409 | /// Test aStar algorithm 410 | func testAStarPath() throws { 411 | let grid = HexGrid( 412 | shape: GridShape.hexagon(3)) 413 | grid.cellAt(try CubeCoordinates(x: 1, y: 0, z: -1))?.isBlocked = true 414 | grid.cellAt(try CubeCoordinates(x: 0, y: 1, z: -1))?.isBlocked = true 415 | if let path = try Math.aStarPath(from: CubeCoordinates(x: 0, y: 0, z: 0), to: CubeCoordinates(x: 2, y: 0, z: -2), on: grid) { 416 | let testSet: Set = try [ 417 | CubeCoordinates(x: 0, y: 0, z: 0), 418 | CubeCoordinates(x: 1, y: -1, z: 0), 419 | CubeCoordinates(x: 2, y: -1, z: -1), 420 | CubeCoordinates(x: 2, y: 0, z: -2) 421 | ] 422 | XCTAssertEqual(path.count, testSet.count) 423 | for pathItem in path { 424 | XCTAssertTrue(testSet.contains(pathItem)) 425 | } 426 | } else { 427 | XCTFail("Path not found.") 428 | } 429 | } 430 | 431 | // Test aStar algorithm for case when path doesn't exist 432 | func testAStarNoPath() throws { 433 | let grid = HexGrid( 434 | shape: GridShape.hexagon(2)) 435 | grid.cellAt(try CubeCoordinates(x: 1, y: 0, z: -1))?.isBlocked = true 436 | grid.cellAt(try CubeCoordinates(x: 1, y: 1, z: -2))?.isBlocked = true 437 | grid.cellAt(try CubeCoordinates(x: 2, y: -1, z: -1))?.isBlocked = true 438 | XCTAssertNil(try Math.aStarPath( 439 | from: CubeCoordinates(x: 0, y: 0, z: 0), 440 | to: CubeCoordinates(x: 2, y: 0, z: -2), 441 | on: grid)) 442 | } 443 | 444 | 445 | static var allTests = [ 446 | ("Test Add operation", testAddOperation), 447 | ("Test Subtract operation", testSubtractOperation), 448 | ("Test Scale operation", testScaleOperation), 449 | ("Test Length", testLenght), 450 | ("Test Distance", testDistance), 451 | ("Test Direction vector", testDirection), 452 | ("Generate all neighbors for provided coordinates", testNeighbor), 453 | ("Generate all diagonal neighbors for provided coordinates", testDiagonalNeighbor), 454 | ("Test Rotate left", testRotateLeft), 455 | ("Test Rotate right", testRotateRight), 456 | ("Test calculation of hexagon corners", testHexCorners), 457 | ("Test rounding and linear interpolation", testRound), 458 | ("Test linedraw", testLinedraw), 459 | ("Test ring", testRing), 460 | ("Test zero ring", testZeroRing), 461 | ("Test invalid ring", testInvalidRing), 462 | ("Test filled ring", testFilledRing), 463 | ("Test invalid filled ring", testInvalidFilledRing), 464 | ("Test breadthFirstSearch algorithm", testBreadthFirstSearch), 465 | ("Test aStar algorithm", testAStarPath), 466 | ("Test aStar algorithm for case when path doesn't exist", testAStarNoPath) 467 | ] 468 | 469 | } 470 | -------------------------------------------------------------------------------- /Sources/HexGrid/Math.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Math operations for hexagon related calculations. 4 | internal struct Math { 5 | 6 | // Basic arithmetic operations 7 | /// Add operation 8 | /// 9 | /// - Parameters: 10 | /// - a: Coordinates of a first hexagon. 11 | /// - b: Coordinates of a second hexagon. 12 | /// - Returns: Result coordinates 13 | static func add (a: CubeCoordinates, b: CubeCoordinates) throws -> CubeCoordinates { 14 | return try CubeCoordinates(x: a.x + b.x, y: a.y + b.y, z: a.z + b.z) 15 | } 16 | 17 | /// Subtract operation 18 | /// 19 | /// - Parameters: 20 | /// - a: Coordinates of minuend. 21 | /// - b: Coordinates of subtrahend. 22 | /// - Returns: Result coordinates 23 | static func subtract (a: CubeCoordinates, b: CubeCoordinates) throws -> CubeCoordinates { 24 | return try CubeCoordinates(x: a.x - b.x, y: a.y - b.y, z: a.z - b.z) 25 | } 26 | 27 | /// Scale operation for direction vectors 28 | /// 29 | /// - Parameters: 30 | /// - a: Coordinates of factor. 31 | /// - c: Coefficient. 32 | /// - Returns: Result coordinates 33 | /// - Note: 34 | /// This function works only with vectors coordinates! See `CoordinateMath.directions` for more details. 35 | /// Using other than vector coordinates as an argument `a` might produce invalid coordinates. 36 | /// 37 | /// Result coordinates must fulfill the same condition `sum(x+y+z)`as any cube coordinates used in this library. 38 | static func scale (a: CubeCoordinates, c: Int) throws -> CubeCoordinates { 39 | return try CubeCoordinates(x: a.x * c, y: a.y * c, z: a.z * c) 40 | } 41 | 42 | // distance functions 43 | /// Hexagon length in grid units 44 | /// 45 | /// - Parameters: 46 | /// - coordinates: Coordinates of a hexagon. 47 | /// - Returns: Length 48 | static func length (coordinates: CubeCoordinates) -> Int { 49 | // using right shift bit >> by 1 = which is basically division by 2 50 | return (abs(coordinates.x) + abs(coordinates.y) + abs(coordinates.z)) >> 1 51 | } 52 | 53 | /// Distance between `from`and `to` hexagons in grid units 54 | /// 55 | /// - Parameters: 56 | /// - from: Coordinates of the initial hexagon. 57 | /// - to: Coordinates of the destination hexagon. 58 | /// - Returns: Distance between two coordinates 59 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 60 | static func distance (from: CubeCoordinates, to: CubeCoordinates) throws -> Int { 61 | return try length(coordinates: subtract(a: from, b: to)) 62 | } 63 | 64 | // neighbors 65 | /// Predefined neighbor coordinates 66 | fileprivate static let directions = try! [ 67 | CubeCoordinates(x: 1, y: 0, z: -1), 68 | CubeCoordinates(x: 1, y: -1, z: 0), 69 | CubeCoordinates(x: 0, y: -1, z: 1), 70 | CubeCoordinates(x: -1, y: 0, z: 1), 71 | CubeCoordinates(x: -1, y: 1, z: 0), 72 | CubeCoordinates(x: 0, y: 1, z: -1) 73 | ] 74 | 75 | /// Get one of the predefined neighbor coordinates based on index 76 | /// 77 | /// - Parameters: 78 | /// - at: index of desired direction (0...5). 79 | /// - Returns: Coordinates of a specified direction 80 | /// - Note: 81 | /// Parameter `at`works with index outside of 0...5. It works also with negative numbers. 82 | /// 83 | /// This makes `directions` a closed loop. When an index overflow the array boundary it continues on the other side. 84 | /// 85 | /// examples: 86 | /// 87 | /// index `6` will return the same direction as index `0` 88 | /// 89 | /// index `-1` will return the same direction as index `5` 90 | /// 91 | /// index `13` will return the same direction as index `1` 92 | static func direction(at index: Int) -> CubeCoordinates { 93 | return directions[numberInCyclingRange(index, in: 6)] 94 | } 95 | 96 | /// Get coordinates of a neighbor based on specified direction 97 | /// 98 | /// - Parameters: 99 | /// - at: index of desired direction (0...5). 100 | /// - origin: Coordinates of the origin hexagon. 101 | /// - Returns: Coordinates of a neighbor at specified direction 102 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 103 | static func neighbor(at index: Int, origin: CubeCoordinates) throws -> CubeCoordinates { 104 | return try add(a: origin, b: direction(at: index)) 105 | } 106 | 107 | /// Get coordinates of all neighbors 108 | /// 109 | /// - Parameters: 110 | /// - coordinates: Coordinates of the origin hexagon. 111 | /// - Returns: Set of CubeCoordinates 112 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 113 | static func neighbors(for coordinates: CubeCoordinates) throws -> Set { 114 | var allNeighbors: Set = Set() 115 | for index in 0...5 { 116 | allNeighbors.insert(try neighbor(at: index, origin: coordinates)) 117 | } 118 | return allNeighbors 119 | } 120 | 121 | /// Predefined diagonal coordinates 122 | fileprivate static let diagonalDirections: [CubeCoordinates] = try! [ 123 | CubeCoordinates(x: 2, y: -1, z: -1), 124 | CubeCoordinates(x: 1, y: 1, z: -2), 125 | CubeCoordinates(x: -1, y: 2, z: -1), 126 | CubeCoordinates(x: -2, y: 1, z: 1), 127 | CubeCoordinates(x: -1, y: -1, z: 2), 128 | CubeCoordinates(x: 1, y: -2, z: 1) 129 | ] 130 | 131 | /// Get one of the predefined diagonal neighbor coordinates based on index 132 | /// 133 | /// - Parameters: 134 | /// - at: index of desired direction (0...5). 135 | /// - Returns: Coordinates of a specified diagonal direction 136 | /// - Note: 137 | /// Parameter `at`works with index outside of 0...5. It works also with negative numbers. 138 | /// 139 | /// This makes `diagonalDirections` a closed loop. When index overflow an array boundary it continues on the other side. 140 | /// 141 | /// examples: 142 | /// 143 | /// index `6` will return the same direction as index `0` 144 | /// 145 | /// index `-1` will return the same direction as index `5` 146 | /// 147 | /// index `13` will return the same direction as index `1` 148 | static func diagonalDirection(at index: Int) -> CubeCoordinates { 149 | return diagonalDirections[numberInCyclingRange(index, in: 6)] 150 | } 151 | 152 | /// Get coordinates of a diagonal neighbor based on specified direction 153 | /// 154 | /// - Parameters: 155 | /// - at: index of desired direction (0...5). 156 | /// - origin: Coordinates of the origin hexagon. 157 | /// - Returns: Coordinates of a diagonal neighbor at specified direction 158 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 159 | static func diagonalNeighbor(at index: Int, origin: CubeCoordinates) throws -> CubeCoordinates { 160 | return try add(a: origin, b: diagonalDirection(at: index)) 161 | } 162 | 163 | /// Get coordinates of all diagonal neighbors 164 | /// 165 | /// - Parameters: 166 | /// - coordinates: Coordinates of the origin hexagon. 167 | /// - Returns: Set of CubeCoordinates 168 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 169 | static func diagonalNeighbors(for coordinates: CubeCoordinates) throws -> Set { 170 | var allNeighbors: Set = Set() 171 | for index in 0...5 { 172 | allNeighbors.insert(try diagonalNeighbor(at: index, origin: coordinates)) 173 | } 174 | return allNeighbors 175 | } 176 | 177 | // Rotation 178 | /// Rotate Left 179 | /// - Parameters: 180 | /// - coordinates: coordinates to be rotated 181 | /// - Returns: rotated coordinates 182 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 183 | static func rotateLeft(coordinates: CubeCoordinates) throws -> CubeCoordinates 184 | { 185 | return try CubeCoordinates(x: -coordinates.y, y: -coordinates.z, z: -coordinates.x) 186 | } 187 | 188 | /// Rotate Right 189 | /// - Parameters: 190 | /// - coordinates: coordinates to be rotated 191 | /// - Returns: rotated coordinates 192 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 193 | static func rotateRight(coordinates: CubeCoordinates) throws -> CubeCoordinates 194 | { 195 | return try CubeCoordinates(x: -coordinates.z, y: -coordinates.x, z: -coordinates.y) 196 | } 197 | 198 | // Screen coordinates related math 199 | 200 | // Helper function for hex corner offset calculation 201 | fileprivate static func hexCornerOffset(at index: Int, hexSize: HexSize, orientation: Orientation) -> Point { 202 | let angle = 2.0 * Double.pi * ( 203 | OrientationMatrix(orientation: orientation).startAngle + Double(index)) / 6 204 | return Point(x: hexSize.width * cos(angle), y: hexSize.height * sin(angle)) 205 | } 206 | 207 | /// Hex Corners 208 | /// - Parameters: 209 | /// - coordinates: coordinates of a hex 210 | /// - hexSize: represents display height and width of a hex 211 | /// - gridOrigin: display coordinates of a grid origin 212 | /// - orientation: orientation of a grid 213 | /// - Returns: Array of six display coordinates (polygon vertices). 214 | static func hexCorners( 215 | coordinates: CubeCoordinates, 216 | hexSize: HexSize, 217 | origin: Point, 218 | orientation: Orientation) -> [Point] { 219 | var corners = [Point]() 220 | let center = Convertor.cubeToPixel(from: coordinates, hexSize: hexSize, gridOrigin: origin, orientation: orientation) 221 | for i in 0...5 { 222 | let offset = hexCornerOffset(at: i, hexSize: hexSize, orientation: orientation) 223 | corners.append(Point(x: center.x + offset.x, y: center.y + offset.y)) 224 | } 225 | return corners 226 | } 227 | 228 | // Algorithms 229 | /// Linear interpolation between two hexes (fractional cube coordinates) 230 | /// - Parameters: 231 | /// - a: first coordinates 232 | /// - b: second coordinates 233 | /// - Returns: Interpolated coordinates 234 | static func lerpCube(a: CubeFractionalCoordinates, b: CubeFractionalCoordinates, f: Double) -> CubeCoordinates 235 | { 236 | return CubeCoordinates( 237 | x: a.x * (1.0 - f) + b.x * f, 238 | y: a.y * (1.0 - f) + b.y * f, 239 | z: a.z * (1.0 - f) + b.z * f) 240 | } 241 | 242 | /// Line between two hexes 243 | /// - Parameters: 244 | /// - from: origin coordinates 245 | /// - to: target coordinates 246 | /// - Returns: Set of all coordinates making a line from coordinate `a` to coordinate `b` 247 | static func line(from a: CubeCoordinates, to b: CubeCoordinates) throws -> Set { 248 | let n = try distance(from: a, to: b) 249 | let aNudge = try CubeFractionalCoordinates( 250 | x: Double(a.x) + 1e-06, 251 | y: Double(a.y) + 1e-06, 252 | z: Double(a.z) - 2e-06) 253 | let bNudge = try CubeFractionalCoordinates( 254 | x: Double(b.x) + 1e-06, 255 | y: Double(b.y) + 1e-06, 256 | z: Double(b.z) - 2e-06) 257 | var results = Set() 258 | let step = 1.0 / Double(max(n, 1)) 259 | for i in 0...n { 260 | results.insert(lerpCube(a: aNudge, b: bNudge, f: step * Double(i))) 261 | } 262 | return results 263 | } 264 | 265 | /// Ring algorithm 266 | /// - Parameters: 267 | /// - from: coordinates of a ring center 268 | /// - in: ring radius 269 | /// - Returns: Set of all coordinates making a ring from hex `origin` on `radius` 270 | static func ring(from origin: CubeCoordinates, in radius: Int) throws -> Set { 271 | var results = Set() 272 | switch radius { 273 | case Int.min..<0: 274 | throw InvalidArgumentsError(message: "Radius can't be less than zero.") 275 | case 0: 276 | results.insert(origin) 277 | default: 278 | var h = try add(a: origin, b: scale(a: direction(at: 4), c: radius)) 279 | for side in 0..<6 { 280 | for _ in 0.. Set { 294 | var results = Set() 295 | if radius < 0 { 296 | throw InvalidArgumentsError(message: "Radius can't be less than zero.") 297 | } 298 | results.insert(origin) 299 | for step in 1...radius { 300 | try results = results.union(ring(from: origin, in: step)) 301 | } 302 | return results 303 | } 304 | 305 | /// Breadth First Search algorithm (flood algorithm) 306 | /// - from: coordinates of a search origin 307 | /// - in: search radius 308 | /// - on: grid used for pathfinding 309 | /// - blocked: set of blocked coordinates 310 | /// - Returns: Set of all reachable coordinates within specified `radius`, considering obstacles 311 | static func breadthFirstSearch( 312 | from origin: CubeCoordinates, 313 | in steps: Int, 314 | on grid: HexGrid) throws -> Set { 315 | if steps < 0 { 316 | throw InvalidArgumentsError(message: "Radius can't be less than zero.") 317 | } 318 | let blocked = grid.blockedCellsCoordinates() 319 | var results = Set() 320 | results.insert(origin) 321 | var fringes = [Set]() 322 | fringes.append([origin]) 323 | var neighborCoordinates: CubeCoordinates 324 | 325 | for k in 1...steps { 326 | fringes.append([]) 327 | for coord in fringes[k-1] { 328 | for direction in 0..<6 { 329 | neighborCoordinates = try neighbor(at: direction, origin: coord) 330 | if !(blocked.contains(neighborCoordinates) || results.contains(neighborCoordinates) || !grid.isValidCoordinates(neighborCoordinates)) { 331 | results.insert(neighborCoordinates) 332 | fringes[k].insert(neighborCoordinates) 333 | } 334 | } 335 | } 336 | } 337 | return results 338 | } 339 | 340 | 341 | /// Field of view (shadowcasting) 342 | /// - Parameters: 343 | /// - origin: viewers position 344 | /// - radius: maximum radius distance from origin 345 | /// - grid: grid used for field of view calculation 346 | /// - includePartiallyVisible: include coordinates which are at least partially visible. 347 | /// Default value is `false` which means that center of cooridnates has to be visible in order to include it in a result set. 348 | /// - Throws: propagates error (usually might be caused by invalid CubeCoordinates) 349 | /// - Returns: `Set` 350 | static func calculateFieldOfView ( 351 | from origin: CubeCoordinates, 352 | in radius: Int, 353 | on grid: HexGrid, 354 | includePartiallyVisible: Bool = false) throws -> Set { 355 | var results = Set() 356 | let opaque = grid.opaqueCellsCoordinates() 357 | var shadows = Set() 358 | results.insert(origin) 359 | 360 | switch radius { 361 | case Int.min..<0: 362 | throw InvalidArgumentsError(message: "Radius can't be less than zero.") 363 | default: 364 | for step in 1...radius { 365 | var hexIndex = 1 366 | var h = try add(a: origin, b: scale(a: direction(at: 4), c: step)) 367 | for side in 0..<6 { 368 | for _ in 0..= shadow.minAngle 379 | && numberInCyclingRange(maxAngle, in: 360) <= shadow.maxAngle { 380 | isShaded = true 381 | break 382 | } 383 | default: 384 | if numberInCyclingRange(centerAngle, in: 360) >= shadow.minAngle 385 | && numberInCyclingRange(centerAngle, in: 360) <= shadow.maxAngle { 386 | isShaded = true 387 | break 388 | } 389 | } 390 | } 391 | if !isShaded { 392 | results.insert(h) 393 | } 394 | if opaque.contains(h){ 395 | var newShadows = Set() 396 | if minAngle < 0 { 397 | newShadows.insert(Shadow(minAngle: 360.0 + minAngle, maxAngle: 360.0)) 398 | newShadows.insert(Shadow(minAngle: 0.0, maxAngle: maxAngle)) 399 | } else { 400 | newShadows.insert(Shadow(minAngle: minAngle, maxAngle: maxAngle)) 401 | } 402 | 403 | for var newShadow in newShadows { 404 | for shadow in shadows { 405 | let newShadowRange = newShadow.minAngle...newShadow.maxAngle 406 | let shadowRange = shadow.minAngle...shadow.maxAngle 407 | if newShadowRange.overlaps(shadowRange) { 408 | newShadow.minAngle = min(newShadow.minAngle, shadow.minAngle) 409 | newShadow.maxAngle = max(newShadow.maxAngle, shadow.maxAngle) 410 | shadows.remove(shadow) 411 | } 412 | } 413 | shadows.insert(Shadow(minAngle: newShadow.minAngle, maxAngle: newShadow.maxAngle)) 414 | } 415 | 416 | } 417 | h = try neighbor(at: side, origin: h) 418 | hexIndex += 1 419 | } 420 | } 421 | } 422 | } 423 | return results 424 | } 425 | 426 | 427 | /// Reconstruct path based on the target node found by an aStar search algorithm 428 | /// 429 | /// - parameter targetNode: The target node found by aStarPath function. 430 | /// - Returns: An array representing path from target to origin. 431 | fileprivate static func backtrack(_ targetNode: Node) -> [T] { 432 | var result: [T] = [] 433 | var node = targetNode 434 | 435 | while let parent = node.parent { 436 | result.append(node.coordinates) 437 | node = parent 438 | } 439 | result.append(node.coordinates) // add origin node as well 440 | 441 | return result.reversed() 442 | } 443 | 444 | /// A* pathfinding algorithm. Search for the shortest available path. 445 | /// - from: start coordinates 446 | /// - to: target coordinates 447 | /// - on: HexGrid on which is pathfinding performed 448 | /// - Returns: Path (Sorted Array) of consequent coordinates from start to target coordinates or `nil` in case a path doesn't exist. 449 | static func aStarPath( 450 | from: CubeCoordinates, 451 | to: CubeCoordinates, 452 | on grid: HexGrid) throws -> [CubeCoordinates]? { 453 | let origin = Node(coordinates: from, parent: nil, costScore: 0, heuristicScore: 0) 454 | 455 | // Store potential candidates for next step in prority queue. 456 | // Priority is based on movement cost + heuristic function. 457 | var frontier = PriorityQueue>(sort: < ) 458 | frontier.enqueue(origin) 459 | 460 | // Store already explored coordinates together with "cost so far". 461 | var exploredNodes = Dictionary() 462 | exploredNodes[origin.coordinates] = 0 463 | 464 | while let currentNode = frontier.dequeue() { 465 | 466 | // We are done, return the path 467 | if currentNode.coordinates == to { 468 | return backtrack(currentNode) 469 | } 470 | 471 | // Get all non blocked neighbors for current Node 472 | for nextCoords in try grid.neighborsCoordinates(for: currentNode.coordinates) 473 | .subtracting(grid.blockedCellsCoordinates()) { 474 | let newCost = (exploredNodes[currentNode.coordinates] ?? 0) 475 | + (grid.cellAt(nextCoords)?.cost ?? 0) + 10 476 | let nextNode = Node( 477 | coordinates: nextCoords, 478 | parent: currentNode, 479 | costScore: newCost, 480 | // Manhattan distance seems to have a good performance/accuracy balance 481 | heuristicScore: Double(try distance(from: nextCoords, to: to)) 482 | ) 483 | // If a neighbor has not been visited or we found a better path, 484 | // enqueue it in priority queue and store or update record in exploredNodes. 485 | if let exploredNextNodeCost = exploredNodes[nextCoords], newCost > exploredNextNodeCost { 486 | continue 487 | } else { 488 | exploredNodes[nextCoords] = nextNode.costScore 489 | frontier.enqueue(nextNode) 490 | } 491 | } 492 | } 493 | // There is no valid path 494 | return nil 495 | } 496 | } 497 | 498 | extension Math { 499 | /// Support structure 500 | /// `minAngle` - minimal angle related to origin position 501 | /// `maxAngle` - maximal angle related to origin position 502 | fileprivate struct Shadow: Hashable { 503 | var minAngle: Double 504 | var maxAngle: Double 505 | } 506 | 507 | 508 | /// Convert number into number within range `0.0...upperBound` 509 | /// - Parameter input: `Double` - input number 510 | /// - Parameter upperBound: `Double`- upper bound of range 511 | /// - Returns: `Double` output number 512 | /// - Note: 513 | /// Using formula `(upperBound + (input % upperBound)) % upperBound` allow number to cycle within `0.0...upperBound` range in both directions 514 | /// angles greater than `upperBound` simply starts from beginning 515 | /// angles lower then 0 are coverted into substracted from `upperBound` 516 | /// Example: 517 | /// `upperBound` = 360.0 518 | /// `-30.0` becomes `330.0` 519 | /// `370.0` becomes `10.0` 520 | fileprivate static func numberInCyclingRange(_ input: Double, in upperBound: Double ) -> Double { 521 | return (upperBound + (input.truncatingRemainder(dividingBy: upperBound))).truncatingRemainder(dividingBy: upperBound) 522 | } 523 | 524 | /// Convert number into number within range `0...upperBound` 525 | /// - Parameter input: `Int` - input number 526 | /// - Parameter upperBound: `Int` - upper bound of range 527 | /// - Returns: `Int` output number 528 | /// - Note: 529 | /// Using formula `(upperBound + (input % upperBound)) % upperBound` allow number to cycle within `0...upperBound` range in both directions 530 | /// angles greater than `upperBound` simply starts from beginning 531 | /// angles lower then 0 are coverted into substracted from `upperBound` 532 | /// Example: 533 | /// `upperBound` = 360 534 | /// `-30` becomes `330` 535 | /// `370` becomes `10` 536 | fileprivate static func numberInCyclingRange(_ input: Int, in upperBound: Int ) -> Int { 537 | return (upperBound + (input % upperBound)) % upperBound 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /Sources/HexGrid/HexGrid.swift: -------------------------------------------------------------------------------- 1 | /// HexGrid is an entry point of the package. It represents a grid of hexagonal cells 2 | public class HexGrid: Codable { 3 | 4 | // MARK: Properties 5 | 6 | /// The `Orientation` of all the hexagons in the grid. 7 | public var orientation: Orientation 8 | 9 | /// The `OffsetLayout` option for the grid. 10 | public var offsetLayout: OffsetLayout 11 | 12 | /// A radius size for each individual hexagon cell in the grid. This describes 13 | /// the center of the hexagon to one of its points. 14 | /// - Note For regular hexagons, `hexSize` should be a square, meaning 15 | /// the `width` and `height` values are the same. Changing one of these values 16 | /// to be out of sync with the other will result in hexagons that are "squished". 17 | private(set) public var hexSize: HexSize 18 | 19 | /// The point of origin for generating pixel coordinates and various drawing-related values. 20 | public var origin: Point 21 | 22 | /// All the cells in the grid. 23 | public var cells: Set { 24 | didSet { 25 | updatePixelDimensions() 26 | } 27 | } 28 | 29 | /// User attributes stored on the grid itself. 30 | public var attributes: [String: Attribute] 31 | 32 | /// Width and height of the entire grid in pixel dimensions. 33 | /// - Note: This value is calculated at based on the `hexSize` property. 34 | private(set) public var pixelSize = HexSize(width: 0.0, height: 0.0) 35 | 36 | // MARK: Initializers 37 | 38 | /// Default initializer using a `Set` filled with `Cell` objects. 39 | /// - Parameters: 40 | /// - cells: grid cells `Set` 41 | /// - orientation: Grid `Orientation` enumeration 42 | /// - offsetLayout: `OffsetLayout` enumeration (used to specify row or column offset for rectangular grids) 43 | /// - hexSize: `HexSize` 44 | /// - origin: Grid origin coordinates (used for drawing related math) 45 | /// - attributes: A dictionary of attributes for the entire grid. 46 | public init( 47 | cells: Set, 48 | orientation: Orientation = Orientation.pointyOnTop, 49 | offsetLayout: OffsetLayout = OffsetLayout.even, 50 | hexSize: HexSize = HexSize(width: 10.0, height: 10.0), 51 | origin: Point = Point(x: 0, y: 0), 52 | attributes: [String: Attribute] = [String: Attribute]() 53 | ) { 54 | self.cells = cells 55 | self.orientation = orientation 56 | self.offsetLayout = offsetLayout 57 | self.hexSize = hexSize 58 | self.origin = origin 59 | self.attributes = attributes 60 | updatePixelDimensions() 61 | } 62 | 63 | /// Initializer with generated grid shape, including a `HexSize` (width/height) for the 64 | /// pixel dimensions of a single hexagon, and an origin `Point` for drawing. 65 | /// 66 | /// - Parameters: 67 | /// - shape: The `GridShape` to use for generated cells. 68 | /// - orientation: Grid `Orientation` enumeration 69 | /// - offsetLayout: `OffsetLayout` enumeration (used to specify row or column offset for rectangular grids) 70 | /// - hexSize: `HexSize` 71 | /// - origin: Grid origin coordinates (used for drawing related math) 72 | /// - attributes: A dictionary of attributes for the entire grid. 73 | public init( 74 | shape: GridShape, 75 | orientation: Orientation = Orientation.pointyOnTop, 76 | offsetLayout: OffsetLayout = OffsetLayout.even, 77 | hexSize: HexSize = HexSize(width: 10.0, height: 10.0), 78 | origin: Point = Point(x: 0, y: 0), 79 | attributes: [String: Attribute] = [String: Attribute]() 80 | ) { 81 | self.orientation = orientation 82 | self.offsetLayout = offsetLayout 83 | self.hexSize = hexSize 84 | self.origin = origin 85 | self.attributes = attributes 86 | 87 | do { 88 | try self.cells = Generator.cellsForGridShape( 89 | shape: shape, 90 | orientation: orientation, 91 | offsetLayout: offsetLayout) 92 | } catch { 93 | self.cells = Set() 94 | } 95 | updatePixelDimensions() 96 | } 97 | 98 | /// Initializer with generated grid shape and `pixelSize` for the entire grid. 99 | /// Each hexagon in the grid will "fit" within that size by default. 100 | /// - Parameters: 101 | /// - shape: Shape to be generated 102 | /// - pixelSize: A `HexSize` corresponding to the size for the entire grid. 103 | /// - orientation: Grid `Orientation` enumeration 104 | /// - offsetLayout: `OffsetLayout` enumeration (used to specify row or column offset for rectangular grids) 105 | /// - attributes: A dictionary of attributes for the entire grid. 106 | public init( 107 | shape: GridShape, 108 | pixelSize: HexSize, 109 | orientation: Orientation = Orientation.pointyOnTop, 110 | offsetLayout: OffsetLayout = OffsetLayout.even, 111 | attributes: [String: Attribute] = [String: Attribute]() 112 | ) { 113 | self.orientation = orientation 114 | self.offsetLayout = offsetLayout 115 | self.pixelSize = pixelSize 116 | self.attributes = attributes 117 | do { 118 | try self.cells = Generator.cellsForGridShape( 119 | shape: shape, 120 | orientation: orientation, 121 | offsetLayout: offsetLayout) 122 | } catch { 123 | self.cells = Set() 124 | } 125 | // these values will be overwritten shortly 126 | self.hexSize = HexSize(width: 100.0, height: 100.0) 127 | self.origin = Point(x: 0, y: 0) 128 | updatePixelDimensions() 129 | fitGrid(in: pixelSize) 130 | } 131 | 132 | // MARK: Coordinates 133 | 134 | /// Coordinates of all available grid cells 135 | /// - Returns: `Set` 136 | public func allCellsCoordinates() -> Set { 137 | return Set(self.cells.map { $0.coordinates }) 138 | } 139 | 140 | /// All non-blocked grid cells 141 | /// - Returns: `Set` 142 | public func nonBlockedCells() -> Set { 143 | return self.cells.filter { !$0.isBlocked } 144 | } 145 | 146 | /// All non-blocked grid cells coordinates 147 | /// - Returns: `Set` 148 | public func nonBlockedCellsCoordinates() -> Set { 149 | return Set(nonBlockedCells().map { $0.coordinates }) 150 | } 151 | 152 | /// All blocked grid cells 153 | /// - Returns: `Set` 154 | public func blockedCells() -> Set { 155 | return self.cells.filter { $0.isBlocked } 156 | } 157 | 158 | /// All blocked grid cells coordinates 159 | /// - Returns: `Set` 160 | public func blockedCellsCoordinates() -> Set { 161 | return Set(blockedCells().map { $0.coordinates }) 162 | } 163 | 164 | /// All opaque grid cells 165 | /// - Returns: `Set` 166 | public func opaqueCells() -> Set { 167 | return self.cells.filter { $0.isOpaque } 168 | } 169 | 170 | /// All opaque grid cells coordinates 171 | /// - Returns: `Set` 172 | public func opaqueCellsCoordinates() -> Set { 173 | return Set(opaqueCells().map { $0.coordinates }) 174 | } 175 | 176 | /// Tries to get a grid cells for specified coordinates 177 | /// - Parameter coordinates: `CubeCoordinates` 178 | /// - Returns: `Cell` or `nil` in case cell with provided coordinates doesn't exist 179 | public func cellAt(_ coordinates: CubeCoordinates) -> Cell? { 180 | return self.cells.first(where: { $0.coordinates == coordinates }) 181 | } 182 | 183 | /// Check whether grid cells with specified coordinates exists 184 | /// - Parameter coordinates: `CubeCoordinates` 185 | /// - Returns: `Bool` 186 | public func isValidCoordinates(_ coordinates: CubeCoordinates) -> Bool { 187 | return self.allCellsCoordinates().contains(coordinates) 188 | } 189 | 190 | // MARK: Cell relations (neighbors and ranges) 191 | 192 | /// Search for a cell neighbor at specified direction 193 | /// - Parameters: 194 | /// - cell: `cell` 195 | /// - direction: `Int` index of direction 196 | /// - Returns: `Cell` or `nil` in case neighbor cell doesn't exist on the grid 197 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 198 | /// - Note: 199 | /// Consider using `Direction` enumeration instead of an Integer value. 200 | /// 201 | /// Example: `Direction.Pointy.northEast.rawValue` 202 | public func neighbor(for cell: Cell, at direction: Int) throws -> Cell? { 203 | return try cellAt(Math.neighbor(at: direction, origin: cell.coordinates)) 204 | } 205 | 206 | /// Search for a neighbor coordinates at specified direction 207 | /// - Parameters: 208 | /// - coordinates: `CubeCoordinates` 209 | /// - direction: `Int` index of direction 210 | /// - Returns: `CubeCoordinates` or `nil` in case neighbor coordinates are not valid 211 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 212 | /// - Note: 213 | /// Consider using `Direction` enumeration instead of an Integer value. 214 | /// 215 | /// Example: `Direction.Pointy.northEast.rawValue` 216 | public func neighborCoordinates( 217 | for coordinates: CubeCoordinates, 218 | at direction: Int) throws -> CubeCoordinates? { 219 | let neighborCoords = try Math.neighbor(at: direction, origin: coordinates) 220 | return isValidCoordinates(neighborCoords) ? neighborCoords : nil 221 | } 222 | 223 | /// Get all available neighbor cells for specified cell 224 | /// - Parameter cell: `Cell` 225 | /// - Returns: `Set` 226 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 227 | public func neighbors(for cell: Cell) throws -> Set { 228 | let neighborsCoordinates = try Math.neighbors( 229 | for: cell.coordinates).filter { isValidCoordinates($0) } 230 | return Set(neighborsCoordinates.compactMap { cellAt($0) }) 231 | } 232 | 233 | /// Get all available neighbor coordinates for specified coordinates 234 | /// - Parameter coordinates: `CubeCoordinates` 235 | /// - Returns: `Set` 236 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 237 | public func neighborsCoordinates(for coordinates: CubeCoordinates) throws -> Set { 238 | return try Math.neighbors(for: coordinates).filter { isValidCoordinates($0) } 239 | } 240 | 241 | /// Search for a cell neighbor at specified diagonal direction 242 | /// - Parameters: 243 | /// - cell: `cell` 244 | /// - direction: `Int` index of direction 245 | /// - Returns: `Cell` or `nil` in case neighbor cell doesn't exist on the grid 246 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 247 | /// - Note: 248 | /// Consider using `Direction` enumeration instead of an Integer value. 249 | /// 250 | /// Example: `Direction.Pointy.northEast.rawValue` 251 | public func diagonalNeighbor(for cell: Cell, at direction: Int) throws -> Cell? { 252 | return try cellAt(Math.diagonalNeighbor(at: direction, origin: cell.coordinates)) 253 | } 254 | 255 | /// Search for a neighbor coordinates at specified diagonal direction 256 | /// - Parameters: 257 | /// - coordinates: `CubeCoordinates` 258 | /// - direction: `Int` index of direction 259 | /// - Returns: `CubeCoordinates` or `nil` in case neighbor coordinates are not valid 260 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 261 | /// - Note: 262 | /// Consider using `Direction` enumeration instead of an Integer value. 263 | /// 264 | /// Example: `Direction.Pointy.northEast.rawValue` 265 | public func diagonalNeighborCoordinates( 266 | for coordinates: CubeCoordinates, 267 | at direction: Int) throws -> CubeCoordinates? { 268 | let neighborCoords = try Math.diagonalNeighbor(at: direction, origin: coordinates) 269 | return isValidCoordinates(neighborCoords) ? neighborCoords : nil 270 | } 271 | 272 | /// Get all available diagonal neighbor cells for specified cell 273 | /// - Parameter cell: `Cell` 274 | /// - Returns: `Set` 275 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 276 | public func diagonalNeighbors(for cell: Cell) throws -> Set { 277 | let neighborsCoordinates = try Math.diagonalNeighbors( 278 | for: cell.coordinates).filter { isValidCoordinates($0) } 279 | return Set(neighborsCoordinates.compactMap { cellAt($0) }) 280 | } 281 | 282 | /// Get all available diagonal neighbor coordinates for specified coordinates 283 | /// - Parameter coordinates: `CubeCoordinates` 284 | /// - Returns: `Set` 285 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 286 | public func diagonalNeighborsCoordinates(for coordinates: CubeCoordinates) throws -> Set { 287 | return try Math.diagonalNeighbors(for: coordinates).filter { isValidCoordinates($0) } 288 | } 289 | 290 | /// Search for coordinates making a line from origin to target coordinates 291 | /// - Parameters: 292 | /// - origin: `CubeCoordinates` 293 | /// - target: `CubeCoordinates` 294 | /// - Returns: `Set` or `nil` in case valid line doesn't exist 295 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 296 | public func lineCoordinates( 297 | from origin: CubeCoordinates, 298 | to target: CubeCoordinates) throws -> Set? { 299 | let mathLine = try Math.line(from: origin, to: target) 300 | return mathLine.allSatisfy({ isValidCoordinates($0) }) ? mathLine : nil 301 | } 302 | 303 | /// Search for cells making a line from origin to target cell 304 | /// - Parameters: 305 | /// - origin: `Cell` 306 | /// - target: `Cell` 307 | /// - Returns: `Set` or `nil` in case valid line doesn't exist 308 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 309 | public func line(from origin: Cell, to target: Cell) throws -> Set? { 310 | if let lineCoords = try lineCoordinates(from: origin.coordinates, to: target.coordinates) { 311 | return Set(lineCoords.compactMap { self.cellAt($0) }) 312 | } 313 | return nil 314 | } 315 | 316 | /// Search for coordinates making a ring from origin coordinates in specified radius 317 | /// - Parameters: 318 | /// - coordinates: `CubeCoordinates` 319 | /// - radius: `Int` 320 | /// - includingBlocked: optional `Bool` 321 | /// - Returns: `Set` 322 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 323 | /// - Note: Coordinates of blocked cells are excluded by default. 324 | public func ringCoordinates( 325 | from coordinates: CubeCoordinates, 326 | in radius: Int, 327 | includingBlocked: Bool = false) throws -> Set { 328 | let ring = try Math.ring(from: coordinates, in: radius) 329 | if includingBlocked { 330 | return ring.intersection(self.allCellsCoordinates()) 331 | } 332 | return ring.intersection(self.nonBlockedCellsCoordinates()) 333 | } 334 | 335 | /// Search for cells making a ring from origin cell in specified radius 336 | /// - Parameters: 337 | /// - cell: `Cell` 338 | /// - radius: `Int` 339 | /// - includingBlocked: optional `Bool` 340 | /// - Returns: `Set` 341 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 342 | /// - Note: Blocked cells are excluded by default. 343 | public func ring(from cell: Cell, in radius: Int, includingBlocked: Bool = false) throws -> Set { 344 | return Set(try self.ringCoordinates( 345 | from: cell.coordinates, 346 | in: radius, 347 | includingBlocked: includingBlocked ).compactMap { self.cellAt($0) }) 348 | } 349 | 350 | /// Search for coordinates making a filled ring from origin coordinates in specified radius 351 | /// - Parameters: 352 | /// - coordinates: `CubeCoordinates` 353 | /// - radius: `Int` 354 | /// - includingBlocked: optional `Bool` 355 | /// - Returns: `Set` 356 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 357 | /// - Note: Coordinates of blocked cells are excluded by default. 358 | public func filledRingCoordinates(from coordinates: CubeCoordinates, in radius: Int, includingBlocked: Bool = false) throws -> Set { 359 | let filledRing = try Math.filledRing(from: coordinates, in: radius) 360 | if includingBlocked { 361 | return filledRing.intersection(allCellsCoordinates()) 362 | } 363 | return filledRing.intersection(nonBlockedCellsCoordinates()) 364 | } 365 | 366 | /// Search for cells making a filled ring from origin cell in specified radius 367 | /// - Parameters: 368 | /// - cell: `Cell` 369 | /// - radius: `Int` 370 | /// - includingBlocked: optional `Bool` 371 | /// - Returns: `Set` 372 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 373 | /// - Note: Blocked cells are excluded by default. 374 | public func filledRing(from cell: Cell, in radius: Int, includingBlocked: Bool = false) throws -> Set { 375 | return Set(try filledRingCoordinates( 376 | from: cell.coordinates, 377 | in: radius, 378 | includingBlocked: includingBlocked).compactMap{ self.cellAt($0) }) 379 | 380 | } 381 | 382 | // MARK: Grid searching (flood search and pathfinding) 383 | 384 | /// Search for all coordinates reachable from origin coordinates within specified number of steps 385 | /// - Parameters: 386 | /// - coordinates: `CubeCoordinates` origin 387 | /// - steps: maximum number of steps 388 | /// - Returns: `Set` 389 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 390 | /// - Note: This function internally use Breadth First Search algorithm. 391 | public func findReachableCoordinates(from coordinates: CubeCoordinates, in steps: Int) throws -> Set { 392 | return try Math.breadthFirstSearch(from: coordinates, in: steps, on: self) 393 | } 394 | 395 | /// Search for all cells reachable from origin cell within specified number of steps 396 | /// - Parameters: 397 | /// - cell: `Cell` origin 398 | /// - steps: maximum number of steps 399 | /// - Returns: `Set` 400 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 401 | /// - Note: This function internally use Breadth First Search algorithm. 402 | public func findReachable(from cell: Cell, in steps: Int) throws -> Set { 403 | return Set(try self.findReachableCoordinates( 404 | from: cell.coordinates, 405 | in: steps).compactMap{ self.cellAt($0) }) 406 | } 407 | 408 | /// Search for the all coordinates visible from origin coordinates 409 | /// - Parameters: 410 | /// - origin: `CubeCoordinates` viewers position 411 | /// - radius: `Int` radius from origin coordinates 412 | /// - includePartiallyVisible: include coordinates which are at least partially visible. 413 | /// Default value is `false` which means that center of cooridnates has to be visible in order to include it in a result set. 414 | /// - Returns: `Set` set of cooridnates visible from origin coordinates within specified radius 415 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 416 | /// - Note: This function internally uses shadowcasting algorithm designed for hexagonal grids. 417 | public func fieldOfViewCoordinates(from origin: CubeCoordinates, in radius: Int, includePartiallyVisible: Bool = false) throws -> Set { 418 | return try Math.calculateFieldOfView( 419 | from: origin, in: radius, 420 | on: self, 421 | includePartiallyVisible: includePartiallyVisible) 422 | } 423 | 424 | /// Search for the all cells visible from origin cell 425 | /// - Parameters: 426 | /// - origin: `Cell` viewers position 427 | /// - radius: `Int` radius from origin cell 428 | /// - includePartiallyVisible: include cells which are at least partially visible. 429 | /// Default value is `false` which means that center of a cell has to be visible in order to include it in result set. 430 | /// - Returns: `Set` set of cells visible from origin cell within specified radius 431 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 432 | /// - Note: This function internally uses shadowcasting algorithm designed for hexagonal grids. 433 | public func fieldOfView(from origin: Cell, in radius: Int, includePartiallyVisible: Bool = false) throws -> Set { 434 | return Set(try self.fieldOfViewCoordinates( 435 | from: origin.coordinates, 436 | in: radius, 437 | includePartiallyVisible: includePartiallyVisible).compactMap{ self.cellAt($0) }) 438 | } 439 | 440 | /// Search for the shortest path from origin to target coordinates 441 | /// - Parameters: 442 | /// - origin: `CubeCoordinates` starting position 443 | /// - target: `CubeCoordinates` target position 444 | /// - Returns: `[CubeCoordinates]` sorted array of adjacent coordinates or `nil` in case valid path doesn't exist 445 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 446 | /// - Note: This function internally use A* algorithm. 447 | public func findPathCoordinates(from origin: CubeCoordinates, to target: CubeCoordinates) throws -> [CubeCoordinates]? { 448 | return try Math.aStarPath(from: origin, to: target, on: self) 449 | } 450 | 451 | /// Search for the shortest path from origin to target cell 452 | /// - Parameters: 453 | /// - origin: `Cell` starting position 454 | /// - target: `Cell` target position 455 | /// - Returns: `[Cell]` sorted array of adjacent cells or `nil` in case valid path doesn't exist 456 | /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error. 457 | /// - Note: This function internally use A* algorithm. 458 | public func findPath(from origin: Cell, to target: Cell) throws -> [Cell]? { 459 | if let path = try Math.aStarPath(from: origin.coordinates, to: target.coordinates, on: self) { 460 | return path.compactMap{ self.cellAt($0) } 461 | } 462 | return nil 463 | } 464 | 465 | // MARK: UI and Drawing 466 | 467 | /// Calculate screen coordinates for corners all hexagon 468 | /// - Parameter cell: `Cell` 469 | /// - Returns: `[Point]` Array of points (x, y - screen coordinates). Each point represents one of polygon (hexagon) corners. 470 | /// - Note: This function take into account grid orientation as well as its origin screen coordinates. 471 | public func polygonCorners(for cell: Cell) -> [Point] { 472 | return Math.hexCorners( 473 | coordinates: cell.coordinates, 474 | hexSize: self.hexSize, 475 | origin: self.origin, 476 | orientation: self.orientation) 477 | } 478 | 479 | /// Calculate screen coordinates for center of a hexagon 480 | /// - Parameter cell: `Cell` 481 | /// - Returns: `Point` (x, y - screen coordinates) of a cell center 482 | /// - Note: This function take into account grid orientation as well as its origin screen coordinates. 483 | public func pixelCoordinates(for cell: Cell) -> Point { 484 | return cell.coordinates.toPixel(orientation: self.orientation, hexSize: self.hexSize, origin: self.origin) 485 | } 486 | 487 | /// Get cell for specified screen coordinates 488 | /// - Parameter pixelCoordinates: `Point` (x, y - screen coordinates) 489 | /// - Returns: `Cell?` or `nil` if cell doesn't exist for provided screen coordinates 490 | /// - Note: This function take into account grid orientation as well as its origin screen coordinates. 491 | public func cellAt(_ pixelCoordinates: Point) throws -> Cell? { 492 | let coords = Convertor.pixelToCube( 493 | from: pixelCoordinates, 494 | hexSize: self.hexSize, 495 | origin: self.origin, 496 | orientation: self.orientation) 497 | return cellAt(coords) 498 | } 499 | 500 | /// Fit the entire grid inside a given width/height. 501 | /// - Note: This will potentially change the `hexSize`, `origin`, and `pixelSize` properties. 502 | public func fitGrid(in size: HexSize) { 503 | guard size != pixelSize else { return } 504 | let widthRatio = size.width / pixelSize.width 505 | let heightRatio = size.height / pixelSize.height 506 | // a new size that "fits" in the expected size 507 | var newGridSize = HexSize(width: 0.0, height: 0.0) 508 | var newHexSize = HexSize(width: 0.0, height: 0.0) 509 | var difference = HexSize(width: 0.0, height: 0.0) 510 | if widthRatio <= heightRatio { 511 | newGridSize.height = pixelSize.height * widthRatio 512 | newGridSize.width = pixelSize.width * widthRatio 513 | difference.height = (size.height - newGridSize.height) / 2.0 514 | newHexSize.height = hexSize.height * widthRatio 515 | newHexSize.width = hexSize.width * widthRatio 516 | } else { 517 | newGridSize.height = pixelSize.height * heightRatio 518 | newGridSize.width = pixelSize.width * heightRatio 519 | difference.width = (size.width - newGridSize.width) / 2.0 520 | newHexSize.height = hexSize.height * heightRatio 521 | newHexSize.width = hexSize.width * heightRatio 522 | } 523 | pixelSize = newGridSize 524 | self.hexSize = newHexSize 525 | setOriginToFitPixelSize(offset: difference) 526 | } 527 | 528 | /// Set the origin to the middle of the grid. 529 | public func setOriginToFitPixelSize(offset: HexSize) { 530 | var minX: Double = .greatestFiniteMagnitude 531 | var maxX: Double = -.greatestFiniteMagnitude 532 | var minY: Double = .greatestFiniteMagnitude 533 | var maxY: Double = -.greatestFiniteMagnitude 534 | 535 | for cell in cells { 536 | let pixelCoords = pixelCoordinates(for: cell) 537 | minX = min(minX, pixelCoords.x) 538 | maxX = max(maxX, pixelCoords.x) 539 | minY = min(minY, pixelCoords.y) 540 | maxY = max(maxY, pixelCoords.y) 541 | } 542 | 543 | var cellPixelWidth: Double 544 | var cellPixelHeight: Double 545 | switch orientation { 546 | case .pointyOnTop: 547 | cellPixelWidth = (3.0).squareRoot() * hexSize.width 548 | cellPixelHeight = 2.0 * hexSize.height 549 | case .flatOnTop: 550 | cellPixelWidth = 2.0 * hexSize.width 551 | cellPixelHeight = (3.0).squareRoot() * hexSize.height 552 | } 553 | 554 | origin = Point( 555 | x: origin.x-minX+(cellPixelWidth/2.0) + offset.width, 556 | y: origin.y-minY+(cellPixelHeight/2.0) + offset.height 557 | ) 558 | } 559 | 560 | // MARK: Internal functions 561 | 562 | /// Function keeps grid dimensions updated using property observer 563 | fileprivate func updatePixelDimensions() -> Void { 564 | var minX: Double = .greatestFiniteMagnitude 565 | var maxX: Double = -.greatestFiniteMagnitude 566 | var minY: Double = .greatestFiniteMagnitude 567 | var maxY: Double = -.greatestFiniteMagnitude 568 | 569 | for cell in cells { 570 | let pixelCoords = pixelCoordinates(for: cell) 571 | minX = min(minX, pixelCoords.x) 572 | maxX = max(maxX, pixelCoords.x) 573 | minY = min(minY, pixelCoords.y) 574 | maxY = max(maxY, pixelCoords.y) 575 | } 576 | var cellPixelWidth: Double 577 | var cellPixelHeight: Double 578 | switch orientation { 579 | case .pointyOnTop: 580 | cellPixelWidth = (3.0).squareRoot() * hexSize.width 581 | cellPixelHeight = 2.0 * hexSize.height 582 | case .flatOnTop: 583 | cellPixelWidth = 2.0 * hexSize.width 584 | cellPixelHeight = (3.0).squareRoot() * hexSize.height 585 | } 586 | pixelSize.width = (maxX - minX) + cellPixelWidth 587 | pixelSize.height = (maxY - minY) + cellPixelHeight 588 | } 589 | } 590 | --------------------------------------------------------------------------------