From f17bde65b1cdf8a85008d819dddec227f29af31b Mon Sep 17 00:00:00 2001 From: Anton Suprunchuk Date: Sat, 4 Jul 2020 16:38:13 +0300 Subject: [PATCH] Documentation (#1) Created documentation and refactored interface --- .gitignore | 2 + CONTRIBUTING.md | 13 + Docs/README.md | 150 +++++++ .../DataStructures/Coordinate2D.Test.cs | 11 +- .../DataStructures/Coordinate3D.Test.cs | 2 +- HexCore.Tests/Fixtures/MovementTypes.Mock.cs | 26 +- .../{AStar => HexGraph}/AStarSearch.Test.cs | 328 ++++++++------- HexCore.Tests/HexGraph/Graph.Test.cs | 73 +++- HexCore.Tests/HexGraph/GraphFactory.Test.cs | 3 +- HexCore.Tests/HexGraph/GraphUtils.Test.cs | 5 +- HexCore.Tests/HexGraph/MovementTypes.Test.cs | 245 ++++++----- HexCore.Tests/QuickStart.cs | 64 +++ HexCore.Tests/Quickstart.Test.cs | 82 ++++ HexCore.sln | 5 +- HexCore/AStar/README.md | 9 - HexCore/{AStar => }/AStarSearch.cs | 16 +- HexCore/CellState.cs | 29 ++ HexCore/{DataStructures => }/Coordinate2D.cs | 12 +- HexCore/{DataStructures => }/Coordinate3D.cs | 4 +- HexCore/DataStructures/README.md | 18 - HexCore/Graph.cs | 390 ++++++++++++++++++ HexCore/{HexGraph => }/GraphFactory.cs | 8 +- HexCore/{HexGraph => }/GraphUtils.cs | 13 +- HexCore/HexCore.csproj | 4 +- HexCore/HexGraph/CellState.cs | 23 -- HexCore/HexGraph/GRAPH_FACTORY_README.md | 0 HexCore/HexGraph/Graph.cs | 224 ---------- HexCore/HexGraph/MovementTypes.cs | 95 ----- HexCore/HexGraph/README.md | 53 --- HexCore/{HexGraph => }/IMovementType.cs | 2 +- HexCore/ITerrainType.cs | 8 + HexCore/{AStar => }/IWeightedGraph.cs | 6 +- HexCore/{HexGraph => }/MovementType.cs | 4 +- HexCore/MovementTypes.cs | 118 ++++++ HexCore/{DataStructures => }/OffsetTypes.cs | 2 +- HexCore/{DataStructures => }/PriorityQueue.cs | 2 +- HexCore/TerrainType.cs | 25 ++ NuGet.config | 14 + README.md | 105 ++++- TODO.md | 10 - publish.sh | 9 +- 41 files changed, 1420 insertions(+), 792 deletions(-) create mode 100644 CONTRIBUTING.md rename HexCore.Tests/{AStar => HexGraph}/AStarSearch.Test.cs (67%) create mode 100644 HexCore.Tests/QuickStart.cs create mode 100644 HexCore.Tests/Quickstart.Test.cs delete mode 100644 HexCore/AStar/README.md rename HexCore/{AStar => }/AStarSearch.cs (88%) create mode 100644 HexCore/CellState.cs rename HexCore/{DataStructures => }/Coordinate2D.cs (86%) rename HexCore/{DataStructures => }/Coordinate3D.cs (96%) delete mode 100644 HexCore/DataStructures/README.md create mode 100644 HexCore/Graph.cs rename HexCore/{HexGraph => }/GraphFactory.cs (74%) rename HexCore/{HexGraph => }/GraphUtils.cs (89%) delete mode 100644 HexCore/HexGraph/CellState.cs delete mode 100644 HexCore/HexGraph/GRAPH_FACTORY_README.md delete mode 100644 HexCore/HexGraph/Graph.cs delete mode 100644 HexCore/HexGraph/MovementTypes.cs delete mode 100644 HexCore/HexGraph/README.md rename HexCore/{HexGraph => }/IMovementType.cs (84%) create mode 100644 HexCore/ITerrainType.cs rename HexCore/{AStar => }/IWeightedGraph.cs (51%) rename HexCore/{HexGraph => }/MovementType.cs (85%) create mode 100644 HexCore/MovementTypes.cs rename HexCore/{DataStructures => }/OffsetTypes.cs (79%) rename HexCore/{DataStructures => }/PriorityQueue.cs (95%) create mode 100644 HexCore/TerrainType.cs create mode 100644 NuGet.config delete mode 100644 TODO.md diff --git a/.gitignore b/.gitignore index f396414..9e3bb98 100644 --- a/.gitignore +++ b/.gitignore @@ -341,3 +341,5 @@ gen /coverage.report /nunit-results.xml /output.mlpd +/.vscode/ +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..376f8bf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. + +## Pull Request Process + + - Ensure any install or build dependencies are removed before the end of the layer when doing a build. + - Please update the [tests](./HexCore.Tests) to reflect the changes. + - Update the [docs](./Docs) with details of changes to the interface. + - Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is SemVer. + - You may merge the Pull Request in once you have the sign-off of the repo maintainer, or if you do not have permission to do that, you may request the second reviewer to merge it for you. + + Thank you! diff --git a/Docs/README.md b/Docs/README.md index e69de29..05dd11b 100644 --- a/Docs/README.md +++ b/Docs/README.md @@ -0,0 +1,150 @@ +# HexCore Documentation + +Welcome to the HexCore documentation! HexCore is a library to perform various operations with a hexagonal grid, such as finding shortest paths from one cell to another, managing terrains and movement types, maintaining the grid state, finding various ranges, neighbors, coordinate systems and converters, and some more stuff you may want to do with a hex grid. This is an in-depth doc, if you're looking for a quick start guide, please take a look here: [Quickstart](../README.md#quickstart) + +## Table of contents + +- [Introduction](#core-principles) +- [Terrain and movement types](#terrain-and-movement-types) +- [Cell states](#cell-state) +- [Coordinate systems](#coordinates) +- [Finding the shortest path](#finding-the-shortest-path) +- [Get the pawn's movement range](#get-movement-range) +- [Other ranges](#other-ranges) +- [Interacting with cell states](#interacting-with-cell-states) + - [Block cells for movement](#blockunblock-cells-for-movement) + - [Changing the cell's terrain type](#changing-the-cells-terrain-type) +- [Utility methods](#utility-methods) + - [graph.Contains()](#graphcontains) + - [graph.GetAllCells()](#graphgetallcells) + +## Core principles + +The main class is `HexCore.Graph`. In order to work, the graph needs some data: Cell states and movement types. Cell state keeps all the info about a particular cell: its coordinate, whether its occupied or not, and its terrain type. Terrain types are needed to calculate movement range for a pawn on a particular terrain (for example, a pawn with the movement type "walking" should receive a penalty when swimming through "water"). + +### Terrain and movement types + +As explained above, movement types are essential for the graph. There's a helper class to help you maintain your terrain and movement types. It's called `MovementTypes`. It maintains a table of your movement and terrain types. It's mandatory to specify movement types for a graph. Movement types can be created this way: + +*Note: If you want to have just the same constant movement cost across the whole map, create only one terrain and movement type with a movement cost of 1. One terrain type with a movement cost of 1 is not a default option to ensure that this is a deliberate choice and not a mistake.* + +```c# +var ground = new TerrainType(1, "Ground"); +var water = new TerrainType(2, "Water"); + +var walkingType = new MovementType(1, "Walking"); +var swimmingType = new MovementType(2, "Swimming"); + +var movementTypes = new MovementTypes( + new ITerrainType[] { ground, water }, + new Dictionary> + { + [walkingType] = new Dictionary + { + [ground] = 1, + [water] = 2 + }, + [swimmingType] = new Dictionary + { + [ground] = 2, + [water] = 1 + } + } +); +``` + +In this example, walking movement types spends two movement points to swim and one to walk, so his movement range in water is limited. In the example above the vice versa also would be true for a swimming type. + +### Cell State + +Cells have a state. The state contains info about the cell coordinate, whether it's occupied or not, and its terrain type. In order to create a graph, you need to have a list of cell states. You can create it like this: + +_Note: This example uses `Coordinate2D` to make it easier to understand what's going on, but it's recommended to use `Coordinate3D` instead. You can read more about coordinate systems in the [coordinate systems section](#coordinates)._ +```c# +var graph = new Graph(new CellState[] { + new CellState(false, new Coordinate2D(0,0, OffsetTypes.OddRowsRight), ground), + new CellState(false, new Coordinate2D(0,1, OffsetTypes.OddRowsRight), ground), + new CellState(true, new Coordinate2D(1,0, OffsetTypes.OddRowsRight), water), + new CellState(false, new Coordinate2D(1,1, OffsetTypes.OddRowsRight), water), + new CellState(false, new Coordinate2D(1,2, OffsetTypes.OddRowsRight), ground) +}, movementTypes); +``` +The resulting graph would look like this: +``` + ⬡⬡ +⬡⬡⬡ +``` +The two cells on top would have terrain type "ground", the two cells in the second row would represent our first lake, and the first water cell would be not passable - we can imagine that there's a sharp rock in the lake at that spot. + +### Coordinates + +There are currently two coordinate systems implemented: offset coordinate type and 3D coordinate type. Offset coordinate type is easy to read for humans, as it's simple `x: rowNumber, y: columnNumber`. The Coordinate2D, however, requires some additional information to work properly - the offset type. It's also not easy to write algorithms for the `Coordinate2D`. The `Coordinate3D`, on the other hand, is not very human-readable, but very easy to operate with code. The library uses `Coordinate3D` internally everywhere, but it's easily possible to convert them into each other: +```c# +new Coordinate2D(0,0, OffsetTypes.OddRowsRight).To3D(); +new Coordinate3D(0,0,0).To2D(OffsetTypes.OddRowsRight); +``` + +Both structures also have static method to convert an enumerable: +```c# +Coordinate2D.To3D(new [] { + Coordinate2D(0,0, OffsetTypes.OddRowsRight), + Coordinate2D(0,1, OffsetTypes.OddRowsRight) +}); +Coordinate3D.To2D(new [] { + Coordinate3D(0,0,0), + Coordinate3D(0,1,-1) +}, OffsetTypes.OddRowsRight) +``` + +For the `Coordinate3D`, it's also possible to perform addition and substraction of two coordinates and multiplication by a scalar: +```c# +var a = new Coordinate3D(-1,1,0); +var b = new Coordinate3D(0,1,-1); + +var c = a + b; // c == -1, 2, -1 +var d = c * 2; // d == -2, 4, -2 +``` + +All `Graph` methods accept both offset coordinates and 3d coordinates. The result will be returned with the same coordinate type that was passed to the method. + +## Finding the shortest path + +After the graph is initialized, simply call `graph.GetShortestPath(start, goal, pawnMovementType)` method on it. It accepts three parameters: coordinate from, coordinate to and, your pawn's movement type. If there's a path from a to b, it will return a sorted list containing the path excluding the starting point. For example for the 3x3 graph, where top the top left is `(0,0)` and the bottom right is `(2,2)`, the path from the bottom right to the top left would be `{(1,1),(1,0),(0,0)}`. + +If there's no path from a to b, an empty list will be returned. + +## Get movement range + +To get pawn's movement range, call `graph.GetMovementRange(Coordinate3D startPosition, int movementPoints, IMovementType movementType)`. The first parameter should be the current pawn's position, the second parameter is the amount of "movement points" the pawn can use to move, and the last one is the pawn's movement type. The graph will calculate the area in which the pawn can move. The method returns a list of coordinates to which the pawn currently can move. Note that this area is not necessarily will be round in shape, as if you specified several of terrain types, moving through some terrain types can take more movement points. + +## Other ranges + +Sometimes you may need a range that is not dependent on terrain types, such as an ability or an attack range. For this there's `graph.GetRange(start, radius)` method. It returns all cells within the radius from the starting point. + +## Interacting with cell states + +### Changing cell state + +Cell state consists of two properties: if the cell can be used to move to/through, and cell's terrain type. Both of them can be changed. + +#### Block/unblock cells for movement + +`graph.IsCellBlocked(Coordintate3D)` can be used to determine whether the cell is available for movement or not, for example, occupied by the pawn or some object. + +`graph.BlockCells()` can be used to block cells. It has two variants: one is `graph.BlockCells(IEnumerable)` to block a range of cells at once, or `BlockCells(Coordinate3D)` to block one cell. + +`graph.UnblockCells()` works the same way as `graph.BlockCells()` described above, except it marks cells as passable. + +#### Changing the cell's terrain type + +Cell's terrain type can be changed using `graph.SetCellsTerrainType()`. Just as with blocking/unblocking cells, there are two variants: `SetCellsTerrainType(IEnumerable coordinates, ITerrainType terrainType)` and `SetCellsTerrainType(Coordinate3D coordinates, ITerrainType terrainType)`. + +## Utility methods + +### graph.Contains + +`graph.Contains(Coordinate3D)` returns `true` if graph contains given coordinate, `false` otherwise. + +### graph.GetAllCells + +Returns the list with all cells and their states. Please don't modify anything on the returned cell states and use `graph.BlockCells()`, `graph.UnblockCells()` and `graph.SetCellsTerrainType()` instead, as graph caches some data internally to speed up the computations, and modifying the cell state directly will lead to errors. \ No newline at end of file diff --git a/HexCore.Tests/DataStructures/Coordinate2D.Test.cs b/HexCore.Tests/DataStructures/Coordinate2D.Test.cs index cf43e59..69526a9 100644 --- a/HexCore.Tests/DataStructures/Coordinate2D.Test.cs +++ b/HexCore.Tests/DataStructures/Coordinate2D.Test.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; -using HexCore.DataStructures; -using HexCore.HexGraph; +using HexCore; using HexCoreTests.Fixtures; using NUnit.Framework; @@ -19,7 +18,7 @@ public void ConvertsOffsetCoordinatesToCubeCoordinatesCorrectly() //Down and right: Y - 1, Z + 1 //Down and left: X - 1, Z + 1 //Right: X + 1, Y - 1; - var expectedCubeCoordinates = new List + var expectedCubeCoordinates = new [] { new Coordinate3D(0, 0, 0), // Down right: @@ -39,10 +38,8 @@ public void ConvertsOffsetCoordinatesToCubeCoordinatesCorrectly() // Down and left: new Coordinate3D(1, -3, 2) }; - - Assert.That(cubeCoordinates.Count, Is.EqualTo(expectedCubeCoordinates.Count)); - for (var index = 0; index < expectedCubeCoordinates.Count; index++) - Assert.That(cubeCoordinates[index], Is.EqualTo(expectedCubeCoordinates[index])); + + Assert.That(cubeCoordinates, Is.EqualTo(expectedCubeCoordinates)); } [Test] diff --git a/HexCore.Tests/DataStructures/Coordinate3D.Test.cs b/HexCore.Tests/DataStructures/Coordinate3D.Test.cs index fcf3b83..2eab015 100644 --- a/HexCore.Tests/DataStructures/Coordinate3D.Test.cs +++ b/HexCore.Tests/DataStructures/Coordinate3D.Test.cs @@ -1,5 +1,5 @@ using System; -using HexCore.DataStructures; +using HexCore; using NUnit.Framework; namespace HexCoreTests.DataStructures diff --git a/HexCore.Tests/Fixtures/MovementTypes.Mock.cs b/HexCore.Tests/Fixtures/MovementTypes.Mock.cs index 61f7ffc..61b9942 100644 --- a/HexCore.Tests/Fixtures/MovementTypes.Mock.cs +++ b/HexCore.Tests/Fixtures/MovementTypes.Mock.cs @@ -1,39 +1,43 @@ using System.Collections.Generic; -using HexCore.HexGraph; +using HexCore; namespace HexCoreTests.Fixtures { public static class MovementTypesFixture { - public static readonly MovementType Ground = new MovementType(1, "Ground"); - public static readonly MovementType Water = new MovementType(2, "Water"); - public static readonly MovementType Air = new MovementType(3, "Air"); + public static readonly MovementType Walking = new MovementType(1, " Walking"); + public static readonly MovementType Swimming = new MovementType(2, "Swimming"); + public static readonly MovementType Flying = new MovementType(3, "Flying"); + + public static readonly TerrainType Ground = new TerrainType(1, "Ground"); + public static readonly TerrainType Water = new TerrainType(2, "Water"); + public static readonly TerrainType Air = new TerrainType(3, "Air"); public static MovementTypes GetMovementTypes() { - var movementTypes = new MovementTypes( - new Dictionary> + ITerrainType[] terrainTypes = {Ground, Water, Air}; + var movementTypes = new MovementTypes(terrainTypes, + new Dictionary> { - [Ground] = new Dictionary + [Walking] = new Dictionary { [Ground] = 1, [Water] = 2, [Air] = 999 }, - [Water] = new Dictionary + [Swimming] = new Dictionary { [Ground] = 2, [Water] = 1, [Air] = 999 }, - [Air] = new Dictionary + [Flying] = new Dictionary { [Ground] = 1, [Water] = 1, [Air] = 1 } - } - ); + }); return movementTypes; } } diff --git a/HexCore.Tests/AStar/AStarSearch.Test.cs b/HexCore.Tests/HexGraph/AStarSearch.Test.cs similarity index 67% rename from HexCore.Tests/AStar/AStarSearch.Test.cs rename to HexCore.Tests/HexGraph/AStarSearch.Test.cs index a17eb54..fb5822e 100644 --- a/HexCore.Tests/AStar/AStarSearch.Test.cs +++ b/HexCore.Tests/HexGraph/AStarSearch.Test.cs @@ -1,56 +1,128 @@ using System.Collections.Generic; -using HexCore.AStar; -using HexCore.DataStructures; -using HexCore.HexGraph; +using HexCore; using HexCoreTests.Fixtures; using NUnit.Framework; -namespace HexCoreTests.AStar +namespace HexCoreTests.HexGraph { [TestFixture] public class AStarSearchTest { + [Test] + public void FindShortestPath_ShouldFindShortestPath_OnAGraphWithDifferentTerrainTypes() + { + // Everything is like before, but now instead of blocking 1,1 let's make it water to apply some penalties + // to our ground moving type + var graph = GraphFactory.CreateRectangularGraph(3, 3, MovementTypesFixture.GetMovementTypes(), + MovementTypesFixture.Ground); + + graph.SetCellsTerrainType(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight).To3D(), + MovementTypesFixture.Water); + // And we expect to achieve same result - even through 1,1 is not blocked + + var start = new Coordinate2D(2, 2, OffsetTypes.OddRowsRight).To3D(); + var goal = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight).To3D(); + + var expectedPath = new[] + { + // But this time we can't go to 1,1, since there is movement penalty. Instead, we are going to the left - 1,2 first + new Coordinate2D(1, 2, OffsetTypes.OddRowsRight).To3D(), + // From there we can move up and right + new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D(), + // And from there we can go to our final goal. + goal + }; + + var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Walking); + + Assert.That(path, Is.EqualTo(expectedPath)); + + // Let's make 0,1 water too and move our starting point to bottom left + graph.SetCellsTerrainType(new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D(), + MovementTypesFixture.Water); + start = new Coordinate2D(0, 2, OffsetTypes.OddRowsRight).To3D(); + + // What's different from previous test - even if 0,1 is water, if we go from the bottom left + // to the top left - go through water still we preferable - path length will be only two cells, but because + // of penalty it'll cost 3 movement point. Going through all corners will take 6 points - 6 cells, 1 point each. + expectedPath = new[] + { + // Going up to the water + new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D(), + goal + }; + + path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Walking); + + Assert.That(path, Is.EqualTo(expectedPath)); + } + + [Test] + public void FindShortestPath_ShouldFindShortestPath_WhenThereArePenaltiesAndObstacles() + { + // Now let's make 1,1 water and block 1,2 + var graph = GraphFactory.CreateRectangularGraph(3, 3, MovementTypesFixture.GetMovementTypes(), + MovementTypesFixture.Ground); + + graph.BlockCells(new Coordinate2D(1, 2, OffsetTypes.OddRowsRight).To3D()); + graph.SetCellsTerrainType(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight).To3D(), + MovementTypesFixture.Water); + + var start = new Coordinate2D(2, 2, OffsetTypes.OddRowsRight).To3D(); + var goal = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight).To3D(); + + // Now we have two shortest paths - 1,1, 1,0, 0,0 costs 4, since there is a penalty on 1,1 + // And 2,1, 2,0, 1,0 0,0, costs 4 too. It's 1 cell longer, but there is no penalties. + // We are expecting to take path 1 because of the heuristics - it's leades to our goal a bit more stright. + var expectedPath = new[] + { + new Coordinate2D(1, 1, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(1, 0, OffsetTypes.OddRowsRight).To3D(), + goal + }; + + var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Walking); + + Assert.That(path, Is.EqualTo(expectedPath)); + } + [Test] public void FindShortestPath_ShouldFindShortestPathOnBiggerGraph() { // This test wouldn't be that different from previous ones, except size of the graph - // Not so square anymore! 7 columns, 10 rows. var graph = GraphFactory.CreateRectangularGraph(7, 10, MovementTypesFixture.GetMovementTypes(), MovementTypesFixture.Ground); // First, let's do simple test - from 5,6 to 1,2 without obstacles - var startOddR = new Coordinate2D(5, 6, OffsetTypes.OddRowsRight); - var goalOddR = new Coordinate2D(1, 2, OffsetTypes.OddRowsRight); - var start = startOddR.To3D(); - var goal = goalOddR.To3D(); + var start = new Coordinate2D(5, 6, OffsetTypes.OddRowsRight).To3D(); + var goal = new Coordinate2D(1, 2, OffsetTypes.OddRowsRight).To3D(); // We expect algo to go up by diagonal and then turn left - var expectedOffsetPath = new List + var expectedPath = new[] { // Go up and left - new Coordinate2D(4, 5, OffsetTypes.OddRowsRight), - new Coordinate2D(4, 4, OffsetTypes.OddRowsRight), - new Coordinate2D(3, 3, OffsetTypes.OddRowsRight), - new Coordinate2D(3, 2, OffsetTypes.OddRowsRight), + new Coordinate2D(4, 5, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(4, 4, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(3, 3, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(3, 2, OffsetTypes.OddRowsRight).To3D(), // And now just left until the goal is reached - new Coordinate2D(2, 2, OffsetTypes.OddRowsRight), - goalOddR + new Coordinate2D(2, 2, OffsetTypes.OddRowsRight).To3D(), + goal }; - var expectedPath = Coordinate2D.To3D(expectedOffsetPath); - var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Ground); + var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Walking); Assert.That(path, Is.EqualTo(expectedPath)); // Good! Now let's block some of them, and also let's add a lake in the middle. - graph.SetManyCellsBlocked(Coordinate2D.To3D(new List + graph.BlockCells(Coordinate2D.To3D(new List { new Coordinate2D(4, 6, OffsetTypes.OddRowsRight), new Coordinate2D(4, 5, OffsetTypes.OddRowsRight), new Coordinate2D(4, 7, OffsetTypes.OddRowsRight), new Coordinate2D(5, 5, OffsetTypes.OddRowsRight) - }), true); - graph.SetManyCellsMovementType(Coordinate2D.To3D(new List + })); + graph.SetCellsTerrainType(Coordinate2D.To3D(new List { new Coordinate2D(4, 2, OffsetTypes.OddRowsRight), new Coordinate2D(3, 2, OffsetTypes.OddRowsRight), @@ -60,135 +132,46 @@ public void FindShortestPath_ShouldFindShortestPathOnBiggerGraph() new Coordinate2D(3, 3, OffsetTypes.OddRowsRight) }), MovementTypesFixture.Water); - //Let's see what's going to happen! - expectedOffsetPath = new List + expectedPath = new[] { // Avoiding obstacles - new Coordinate2D(6, 6, OffsetTypes.OddRowsRight), - new Coordinate2D(6, 5, OffsetTypes.OddRowsRight), - new Coordinate2D(6, 4, OffsetTypes.OddRowsRight), + new Coordinate2D(6, 6, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(6, 5, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(6, 4, OffsetTypes.OddRowsRight).To3D(), // Going parallel to the bank - new Coordinate2D(5, 4, OffsetTypes.OddRowsRight), - new Coordinate2D(4, 4, OffsetTypes.OddRowsRight), - new Coordinate2D(3, 4, OffsetTypes.OddRowsRight), - new Coordinate2D(2, 4, OffsetTypes.OddRowsRight), + new Coordinate2D(5, 4, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(4, 4, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(3, 4, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(2, 4, OffsetTypes.OddRowsRight).To3D(), // Now we are going to cross the water, since it's shortest available solution from this point - new Coordinate2D(1, 3, OffsetTypes.OddRowsRight), + new Coordinate2D(1, 3, OffsetTypes.OddRowsRight).To3D(), // And we are here. - goalOddR + goal }; - expectedPath = Coordinate2D.To3D(expectedOffsetPath); - path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Ground); + path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Walking); Assert.That(path, Is.EqualTo(expectedPath)); // Now let's check water movement type - it should prefer going through the water rather than the ground - path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Water); + path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Swimming); - expectedOffsetPath = new List + expectedPath = new[] { // Avoiding obstacles - new Coordinate2D(6, 6, OffsetTypes.OddRowsRight), - new Coordinate2D(6, 5, OffsetTypes.OddRowsRight), - new Coordinate2D(6, 4, OffsetTypes.OddRowsRight), + new Coordinate2D(6, 6, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(6, 5, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(6, 4, OffsetTypes.OddRowsRight).To3D(), // Head right to the water - new Coordinate2D(5, 3, OffsetTypes.OddRowsRight), - new Coordinate2D(5, 2, OffsetTypes.OddRowsRight), + new Coordinate2D(5, 3, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(5, 2, OffsetTypes.OddRowsRight).To3D(), // Swim - new Coordinate2D(4, 2, OffsetTypes.OddRowsRight), - new Coordinate2D(3, 2, OffsetTypes.OddRowsRight), - new Coordinate2D(2, 2, OffsetTypes.OddRowsRight), + new Coordinate2D(4, 2, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(3, 2, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(2, 2, OffsetTypes.OddRowsRight).To3D(), // And we are here. - goalOddR + goal }; - expectedPath = Coordinate2D.To3D(expectedOffsetPath); - - Assert.That(path, Is.EqualTo(expectedPath)); - } - - [Test] - public void FindShortestPath_ShouldFindShortestPathWhenThereIsPenalties() - { - // Everything is like before, but now instead of blocking 1,1 let's make it water to apply some penalties - // to our ground moving type - var graph = GraphFactory.CreateRectangularGraph(3, 3, MovementTypesFixture.GetMovementTypes(), - MovementTypesFixture.Ground); - - graph.SetOneCellMovementType(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight).To3D(), - MovementTypesFixture.Water); - // And we expect to achieve same result - even through 1,1 is not blocked - - var startOddR = new Coordinate2D(2, 2, OffsetTypes.OddRowsRight); - var goalOddR = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight); - var start = startOddR.To3D(); - var goal = goalOddR.To3D(); - - var expectedOffsetPath = new List - { - // But this time we can't go to 1,1, since there is movement penalty. Instead, we are going to the left - 1,2 first - new Coordinate2D(1, 2, OffsetTypes.OddRowsRight), - // From there we can move up and right - new Coordinate2D(0, 1, OffsetTypes.OddRowsRight), - // And from there we can go to our final goal. - goalOddR - }; - var expectedPath = Coordinate2D.To3D(expectedOffsetPath); - - var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Ground); - - Assert.That(path, Is.EqualTo(expectedPath)); - - // Let's make 0,1 water too and move our starting point to bottom left - graph.SetOneCellMovementType(new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D(), - MovementTypesFixture.Water); - startOddR = new Coordinate2D(0, 2, OffsetTypes.OddRowsRight); - start = startOddR.To3D(); - - // What's different from previous test - even if 0,1 is water, if we go from the bottom left - // to the top left - go through water still we preferable - path length will be only two cells, but because - // of penalty it'll cost 3 movement point. Going through all corners will take 6 points - 6 cells, 1 point each. - expectedOffsetPath = new List - { - // Going up to the water - new Coordinate2D(0, 1, OffsetTypes.OddRowsRight), - goalOddR - }; - expectedPath = Coordinate2D.To3D(expectedOffsetPath); - - path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Ground); - - Assert.That(path, Is.EqualTo(expectedPath)); - } - - [Test] - public void FindShortestPath_ShouldFindShortestPathWhenThereIsPenaltiesAndObstacles() - { - // Now let's make 1,1 water and block 1,2 - var graph = GraphFactory.CreateRectangularGraph(3, 3, MovementTypesFixture.GetMovementTypes(), - MovementTypesFixture.Ground); - - graph.SetOneCellBlocked(new Coordinate2D(1, 2, OffsetTypes.OddRowsRight).To3D(), true); - graph.SetOneCellMovementType(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight).To3D(), - MovementTypesFixture.Water); - - var startOddR = new Coordinate2D(2, 2, OffsetTypes.OddRowsRight); - var goalOddR = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight); - var start = startOddR.To3D(); - var goal = goalOddR.To3D(); - - // Now we have two shortest paths - 1,1, 1,0, 0,0 costs 4, since there is a penalty on 1,1 - // And 2,1, 2,0, 1,0 0,0, costs 4 too. It's 1 cell longer, but there is no penalties. - // We are expecting to take path 1 because of the heuristics - it's leades to our goal a bit more stright. - var expectedOffsetPath = new List - { - new Coordinate2D(1, 1, OffsetTypes.OddRowsRight), - new Coordinate2D(1, 0, OffsetTypes.OddRowsRight), - goalOddR - }; - var expectedPath = Coordinate2D.To3D(expectedOffsetPath); - - var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Ground); Assert.That(path, Is.EqualTo(expectedPath)); } @@ -200,49 +183,44 @@ public void FindShortestPath_ShouldFindShortestPathWithObstacles() var graph = GraphFactory.CreateRectangularGraph(3, 3, MovementTypesFixture.GetMovementTypes(), MovementTypesFixture.Ground); - graph.SetOneCellBlocked(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight).To3D(), true); + graph.BlockCells(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight).To3D()); // Same as in prevoius test - var startOddR = new Coordinate2D(2, 2, OffsetTypes.OddRowsRight); - var goalOddR = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight); - var start = startOddR.To3D(); - var goal = goalOddR.To3D(); + var start = new Coordinate2D(2, 2, OffsetTypes.OddRowsRight).To3D(); + var goal = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight).To3D(); - var expectedOffsetPath = new List + var expectedPath = new[] { // But this time we can't go to 1,1, since it's blocked. Instead, we are going to the left - 1,2 first - new Coordinate2D(1, 2, OffsetTypes.OddRowsRight), + new Coordinate2D(1, 2, OffsetTypes.OddRowsRight).To3D(), // From there we can move up and right - new Coordinate2D(0, 1, OffsetTypes.OddRowsRight), + new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D(), // And from there we can go to our final goal. - goalOddR + goal }; - var expectedPath = Coordinate2D.To3D(expectedOffsetPath); - var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Ground); + var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Walking); Assert.That(path, Is.EqualTo(expectedPath)); // Let's block 0,1 and move our starting point to bottom left - graph.SetOneCellBlocked(new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D(), true); - startOddR = new Coordinate2D(0, 2, OffsetTypes.OddRowsRight); - start = startOddR.To3D(); + graph.BlockCells(new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D()); + start = new Coordinate2D(0, 2, OffsetTypes.OddRowsRight).To3D(); - expectedOffsetPath = new List + expectedPath = new[] { // Now we need to go through all corners - first let's go to the bottom right - new Coordinate2D(1, 2, OffsetTypes.OddRowsRight), - new Coordinate2D(2, 2, OffsetTypes.OddRowsRight), + new Coordinate2D(1, 2, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(2, 2, OffsetTypes.OddRowsRight).To3D(), // Up to the top right - new Coordinate2D(2, 1, OffsetTypes.OddRowsRight), - new Coordinate2D(2, 0, OffsetTypes.OddRowsRight), + new Coordinate2D(2, 1, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(2, 0, OffsetTypes.OddRowsRight).To3D(), // And from there we can go left until we reach our goal - new Coordinate2D(1, 0, OffsetTypes.OddRowsRight), - goalOddR + new Coordinate2D(1, 0, OffsetTypes.OddRowsRight).To3D(), + goal }; - expectedPath = Coordinate2D.To3D(expectedOffsetPath); - path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Ground); + path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Walking); Assert.That(path, Is.EqualTo(expectedPath)); } @@ -258,27 +236,43 @@ public void FindShortestPath_ShouldFindShortestPathWithoutObstacles() // much easier to operate them when in comes to actual algorythms // From bottom right - var startOddR = new Coordinate2D(2, 2, OffsetTypes.OddRowsRight); + var start = new Coordinate2D(2, 2, OffsetTypes.OddRowsRight).To3D(); // To top left - var goalOddR = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight); - var start = startOddR.To3D(); - var goal = goalOddR.To3D(); + var goal = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight).To3D(); - // Start point is excluded from the path - var expectedOffsetPath = new List + var expectedPath = new[] { // From 2, 2 we move to 1,1, which is central - new Coordinate2D(1, 1, OffsetTypes.OddRowsRight), + new Coordinate2D(1, 1, OffsetTypes.OddRowsRight).To3D(), // From 1,1 we move to 1,0, since there is no direct connection between 1,1 and 0,0 - new Coordinate2D(1, 0, OffsetTypes.OddRowsRight), - // And then moving to our goal. - goalOddR + new Coordinate2D(1, 0, OffsetTypes.OddRowsRight).To3D(), + goal }; - var expectedPath = Coordinate2D.To3D(expectedOffsetPath); // For the simplest test we assume that all cells have type ground, as well as a unit - var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Ground); + var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Walking); Assert.That(path, Is.EqualTo(expectedPath)); } + + [Test] + public void FindShortestPathShouldReturnAnEmptyList_WhenNoPathFound() + { + // There's no connection between (0,1) and (2,5) + var graph = new Graph(new[] + { + new CellState(false, new Coordinate2D(0, 0, OffsetTypes.OddRowsRight), MovementTypesFixture.Ground), + new CellState(false, new Coordinate2D(0, 1, OffsetTypes.OddRowsRight), MovementTypesFixture.Ground), + new CellState(false, new Coordinate2D(0, 2, OffsetTypes.OddRowsRight), MovementTypesFixture.Ground), + new CellState(false, new Coordinate2D(1, 0, OffsetTypes.OddRowsRight), MovementTypesFixture.Ground), + new CellState(false, new Coordinate2D(1, 1, OffsetTypes.OddRowsRight), MovementTypesFixture.Ground), + new CellState(false, new Coordinate2D(2, 5, OffsetTypes.OddRowsRight), MovementTypesFixture.Ground) + }, MovementTypesFixture.GetMovementTypes()); + + var start = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight).To3D(); + var goal = new Coordinate2D(2, 5, OffsetTypes.OddRowsRight).To3D(); + + var path = AStarSearch.FindShortestPath(graph, start, goal, MovementTypesFixture.Walking); + Assert.That(path, Is.Empty); + } } } \ No newline at end of file diff --git a/HexCore.Tests/HexGraph/Graph.Test.cs b/HexCore.Tests/HexGraph/Graph.Test.cs index 5688a0e..759d726 100644 --- a/HexCore.Tests/HexGraph/Graph.Test.cs +++ b/HexCore.Tests/HexGraph/Graph.Test.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using HexCore.DataStructures; -using HexCore.HexGraph; +using HexCore; using HexCoreTests.Fixtures; using NUnit.Framework; @@ -64,7 +63,8 @@ public void GetMovementRange_ShouldGetCorrectMovementRange() { var graph = GraphFactory.CreateRectangularGraph(6, 7, MovementTypesFixture.GetMovementTypes(), MovementTypesFixture.Ground); - var center = new Coordinate2D(3, 2, OffsetTypes.OddRowsRight).To3D(); + var center2d = new Coordinate2D(3, 2, OffsetTypes.OddRowsRight); + var center = center2d.To3D(); var expectedMovementRange2D = new List { @@ -92,21 +92,21 @@ public void GetMovementRange_ShouldGetCorrectMovementRange() var expectedMovementRange = Coordinate2D.To3D(expectedMovementRange2D); - var movementRange = graph.GetMovementRange(center, 2, MovementTypesFixture.Ground); + var movementRange = graph.GetMovementRange(center, 2, MovementTypesFixture.Walking); Assert.That(movementRange.Count, Is.EqualTo(expectedMovementRange.Count)); Assert.That(movementRange, Is.EqualTo(expectedMovementRange)); // If 2,3 is water, we shouldn't be able to access 2,4. If we make 1,3 water - we just shouldn't be able to // access it, since going to 1,3 will cost more than movement points we have. - graph.SetManyCellsMovementType(Coordinate2D.To3D(new List + graph.SetCellsTerrainType(Coordinate2D.To3D(new List { new Coordinate2D(2, 3, OffsetTypes.OddRowsRight), new Coordinate2D(1, 3, OffsetTypes.OddRowsRight) }), MovementTypesFixture.Water); // Blocking 2,1 will prevent us from going to 2,1 and 2,0 at the same time - graph.SetOneCellBlocked(new Coordinate2D(2, 1, OffsetTypes.OddRowsRight).To3D(), true); + graph.BlockCells(new Coordinate2D(2, 1, OffsetTypes.OddRowsRight).To3D()); // 2,4 isn't accessible because the only path to it thorough the water expectedMovementRange2D.Remove(new Coordinate2D(2, 4, OffsetTypes.OddRowsRight)); @@ -117,10 +117,14 @@ public void GetMovementRange_ShouldGetCorrectMovementRange() expectedMovementRange2D.Remove(new Coordinate2D(2, 0, OffsetTypes.OddRowsRight)); expectedMovementRange = Coordinate2D.To3D(expectedMovementRange2D); - - movementRange = graph.GetMovementRange(center, 2, MovementTypesFixture.Ground); + movementRange = graph.GetMovementRange(center, 2, MovementTypesFixture.Walking); Assert.That(movementRange, Is.EqualTo(expectedMovementRange)); + + Assert.That( + graph.GetMovementRange(center2d, 2, MovementTypesFixture.Walking), + Is.EquivalentTo(expectedMovementRange2D) + ); } [Test] @@ -145,6 +149,11 @@ public void GetNeighbours_ShouldGetCorrectNeighbors() new Coordinate3D(2, -4, 2) }; Assert.That(neighbors, Is.EqualTo(expectedNeighbors)); + + Assert.That( + graph.GetNeighbors(offsetTarget, false).ToList(), + Is.EquivalentTo(Coordinate3D.To2D(expectedNeighbors, OffsetTypes.OddRowsRight)) + ); } [Test] @@ -152,7 +161,8 @@ public void GetRange_ShouldGetCorrectRange() { var graph = GraphFactory.CreateRectangularGraph(6, 7, MovementTypesFixture.GetMovementTypes(), MovementTypesFixture.Ground); - var center = new Coordinate2D(3, 2, OffsetTypes.OddRowsRight).To3D(); + var center2d = new Coordinate2D(3, 2, OffsetTypes.OddRowsRight); + var center = center2d.To3D(); var expectedRange2D = new List { @@ -182,11 +192,13 @@ public void GetRange_ShouldGetCorrectRange() var range = graph.GetRange(center, 2); - Assert.That(range, Is.EqualTo(expectedMovementRange)); + Assert.That(range, Is.EquivalentTo(expectedMovementRange)); + + Assert.That(graph.GetRange(center2d, 2), Is.EquivalentTo(expectedRange2D)); } [Test] - public void GetShortestPath_ShouldBeAbleToFindingShortesetPath() + public void GetShortestPath_ShouldBeAbleToFindingShortestPath() { // Note: this method uses AStarSearch class inside. // AStarSerach has its own comprehensive tests, so this test is only to ensure that this method exists and @@ -195,7 +207,7 @@ public void GetShortestPath_ShouldBeAbleToFindingShortesetPath() 3, MovementTypesFixture.GetMovementTypes(), MovementTypesFixture.Ground); var start = new Coordinate2D(0, 0, OffsetTypes.OddRowsRight).To3D(); var goal = new Coordinate2D(2, 2, OffsetTypes.OddRowsRight).To3D(); - var shortestPath = graph.GetShortestPath(start, goal, MovementTypesFixture.Ground); + var shortestPath = graph.GetShortestPath(start, goal, MovementTypesFixture.Walking); var expectedShortestPath = Coordinate2D.To3D(new List { new Coordinate2D(1, 0, OffsetTypes.OddRowsRight), @@ -213,22 +225,25 @@ public void IsInBounds_ShouldReturnTrueIfThePositionIsWithinTheGraphBounds() MovementTypesFixture.Ground); var position = new Coordinate3D(0, -1, 1); - Assert.That(graph.IsInBounds(position), Is.True); + Assert.That(graph.Contains(position), Is.True); position = new Coordinate3D(-1, 2, -1); - Assert.That(graph.IsInBounds(position), Is.False); + Assert.That(graph.Contains(position), Is.False); + + Assert.That(graph.Contains(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight)), Is.True); + Assert.That(graph.Contains(new Coordinate2D(10, 10, OffsetTypes.OddRowsRight)), Is.False); } [Test] - public void SetManyCellsMovementType_ShouldSetMovementTypesToCells() + public void SetManyCellsMovementType_ShouldSetTerrainTypesToCells() { var graph = GraphFactory.CreateRectangularGraph(3, 3, MovementTypesFixture.GetMovementTypes(), MovementTypesFixture.Ground); var coordinateToSet = new Coordinate2D(2, 1, OffsetTypes.OddRowsRight).To3D(); - graph.SetOneCellMovementType(coordinateToSet, MovementTypesFixture.Water); - Assert.That(graph.GetCellState(coordinateToSet).MovementType, Is.EqualTo(MovementTypesFixture.Water)); + graph.SetCellsTerrainType(coordinateToSet, MovementTypesFixture.Water); + Assert.That(graph.GetCellState(coordinateToSet).TerrainType, Is.EqualTo(MovementTypesFixture.Water)); var coordinatesToSet = Coordinate2D.To3D(new List { @@ -236,21 +251,35 @@ public void SetManyCellsMovementType_ShouldSetMovementTypesToCells() new Coordinate2D(0, 2, OffsetTypes.OddRowsRight) }); - graph.SetManyCellsMovementType(coordinatesToSet, MovementTypesFixture.Water); + graph.SetCellsTerrainType(coordinatesToSet, MovementTypesFixture.Water); + foreach (var coordinate in coordinatesToSet) + Assert.That(graph.GetCellState(coordinate).TerrainType, Is.EqualTo(MovementTypesFixture.Water)); + + graph.SetCellsTerrainType(new [] + { + new Coordinate2D(0, 1, OffsetTypes.OddRowsRight), + new Coordinate2D(0, 2, OffsetTypes.OddRowsRight) + }, MovementTypesFixture.Air); foreach (var coordinate in coordinatesToSet) - Assert.That(graph.GetCellState(coordinate).MovementType, Is.EqualTo(MovementTypesFixture.Water)); + Assert.That(graph.GetCellState(coordinate).TerrainType, Is.EqualTo(MovementTypesFixture.Air)); } [Test] - public void SetOneCellBlocked_ShouldBlockCell() + public void UnblockCell_ShouldUnblockCell() { var graph = GraphFactory.CreateRectangularGraph(3, 3, MovementTypesFixture.GetMovementTypes(), MovementTypesFixture.Ground); Assert.That(graph.IsCellBlocked(new Coordinate3D(0, 0, 0)), Is.False); - graph.SetOneCellBlocked(new Coordinate3D(0, 0, 0), true); + graph.BlockCells(new Coordinate3D(0, 0, 0)); Assert.That(graph.IsCellBlocked(new Coordinate3D(0, 0, 0)), Is.True); - graph.SetOneCellBlocked(new Coordinate3D(0, 0, 0), false); + graph.UnblockCells(new Coordinate3D(0, 0, 0)); Assert.That(graph.IsCellBlocked(new Coordinate3D(0, 0, 0)), Is.False); + + Assert.That(graph.IsCellBlocked(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight)), Is.False); + graph.BlockCells(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight)); + Assert.That(graph.IsCellBlocked(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight)), Is.True); + graph.UnblockCells(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight)); + Assert.That(graph.IsCellBlocked(new Coordinate2D(1, 1, OffsetTypes.OddRowsRight)), Is.False); } } } \ No newline at end of file diff --git a/HexCore.Tests/HexGraph/GraphFactory.Test.cs b/HexCore.Tests/HexGraph/GraphFactory.Test.cs index 40b9dce..be84a50 100644 --- a/HexCore.Tests/HexGraph/GraphFactory.Test.cs +++ b/HexCore.Tests/HexGraph/GraphFactory.Test.cs @@ -1,5 +1,4 @@ -using HexCore.DataStructures; -using HexCore.HexGraph; +using HexCore; using HexCoreTests.Fixtures; using NUnit.Framework; diff --git a/HexCore.Tests/HexGraph/GraphUtils.Test.cs b/HexCore.Tests/HexGraph/GraphUtils.Test.cs index ffb42e0..d2a4b12 100644 --- a/HexCore.Tests/HexGraph/GraphUtils.Test.cs +++ b/HexCore.Tests/HexGraph/GraphUtils.Test.cs @@ -1,5 +1,4 @@ -using HexCore.DataStructures; -using HexCore.HexGraph; +using HexCore; using HexCoreTests.Fixtures; using NUnit.Framework; @@ -112,7 +111,7 @@ public void ResizeSquareGraph_ShouldMaintainCellStatesOnResize() var graph = GraphFactory.CreateRectangularGraph(3, 3, MovementTypesFixture.GetMovementTypes(), MovementTypesFixture.Ground); Assert.False(graph.IsCellBlocked(new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D())); - graph.SetOneCellBlocked(new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D(), true); + graph.BlockCells(new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D()); Assert.True(graph.IsCellBlocked(new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D())); GraphUtils.ResizeSquareGraph(graph, OffsetTypes.OddRowsRight, 2, 2, MovementTypesFixture.Ground); Assert.True(graph.IsCellBlocked(new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D())); diff --git a/HexCore.Tests/HexGraph/MovementTypes.Test.cs b/HexCore.Tests/HexGraph/MovementTypes.Test.cs index 14b6239..8a02406 100644 --- a/HexCore.Tests/HexGraph/MovementTypes.Test.cs +++ b/HexCore.Tests/HexGraph/MovementTypes.Test.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using HexCore.HexGraph; +using HexCore; using NUnit.Framework; namespace HexCoreTests.HexGraph @@ -10,104 +10,114 @@ public class MovementTypesTest [Test] public void Constructor_ShouldAddNewType() { - var ground = new MovementType(1, "Ground"); - var movementTypes = new MovementTypes( - new Dictionary> + var ground = new TerrainType(1, "Ground"); + var walking = new MovementType(1, "Walking"); + var movementTypes = new MovementTypes(new ITerrainType[] {ground}, + new Dictionary> { - [ground] = new Dictionary + [walking] = new Dictionary { [ground] = 1 } - } - ); + }); - Assert.That(movementTypes.Contains(ground), Is.True); + Assert.That(movementTypes.ContainsTerrainType(ground), Is.True); + Assert.That(movementTypes.ContainsMovementType(walking), Is.True); } [Test] public void Constructor_ShouldThrow_WhenAddingATypeWithCostsOutsideOfKnownTypes() { - var ground = new MovementType(1, "Ground"); - var water = new MovementType(2, "Water"); - var heavy = new MovementType(3, "Heavy"); + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + var air = new TerrainType(3, "Air"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(2, "Swimming"); + Assert.That(() => { - var movementTypes = new MovementTypes( - new Dictionary> + var movementTypes = new MovementTypes(new ITerrainType[] {ground, water}, + new Dictionary> { - [ground] = new Dictionary + [walkingType] = new Dictionary { [ground] = 1, [water] = 2, - [heavy] = 1 + [air] = 1 }, - [water] = new Dictionary + [swimmingType] = new Dictionary { [ground] = 2, [water] = 1, - [heavy] = 1 + [air] = 1 } - } - ); + }); }, Throws.ArgumentException.With.Message.EqualTo( - "Error when adding movement type 'Ground': movement costs contain unknown type: 'Heavy'")); + "Error when adding movement type 'Walking': movement costs contain unknown type: 'Air'")); } [Test] public void Constructor_ShouldThrow_WhenAddingATypeWithIncompleteCosts() { - var ground = new MovementType(1, "Ground"); - var water = new MovementType(2, "Water"); - var heavy = new MovementType(3, "Heavy"); + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + var air = new TerrainType(3, "Air"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(2, "Swimming"); + var flyingType = new MovementType(3, "Flying"); + Assert.That(() => { - var movementTypes = new MovementTypes( - new Dictionary> + var movementTypes = new MovementTypes(new ITerrainType[] {ground, water, air}, + new Dictionary> { - [ground] = new Dictionary + [walkingType] = new Dictionary { [ground] = 1 }, - [water] = new Dictionary + [swimmingType] = new Dictionary { [ground] = 2, [water] = 1 }, - [heavy] = new Dictionary + [flyingType] = new Dictionary { [ground] = 2, [water] = 1 } - } - ); + }); }, Throws.ArgumentException.With.Message.EqualTo( - "Error when adding movement type 'Ground': missing movement costs to types: 'Water', 'Heavy'")); + "Error when adding movement type 'Walking': missing movement costs to types: 'Water', 'Air'")); } [Test] public void Constructor_ShouldThrow_WhenAddingTwoTypesWithTheSameId() { - var ground = new MovementType(1, "Ground"); - var water = new MovementType(1, "Water"); + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(1, "Swimming"); Assert.That(() => { - var movementTypes = new MovementTypes( - new Dictionary> + var movementTypes = new MovementTypes(new ITerrainType[] {ground, water}, + new Dictionary> { - [ground] = new Dictionary + [walkingType] = new Dictionary { [ground] = 1, [water] = 2 }, - [water] = new Dictionary + [swimmingType] = new Dictionary { [ground] = 2, [water] = 1 } - } - ); + }); }, Throws.ArgumentException.With.Message.EqualTo( "An item with the same key has already been added. Key: 1")); @@ -116,126 +126,159 @@ public void Constructor_ShouldThrow_WhenAddingTwoTypesWithTheSameId() [Test] public void Constructor_ShouldThrowIfAnEmptyDictionaryIsPassed() { - var ground = new MovementType(1, "Ground"); + var ground = new TerrainType(1, "Ground"); + var walking = new MovementType(1, "Walking"); Assert.That(() => { var movementTypes = - new MovementTypes(new Dictionary>()); + new MovementTypes(new ITerrainType[] {ground}, + new Dictionary>()); }, Throws.ArgumentException.With.Message.EqualTo( "Movement types should always have at least one explicitly defined type. For the reasoning, please visit the movement types section in the library's docs")); + + Assert.That(() => + { + var movementTypes = + new MovementTypes(new ITerrainType[] { }, + new Dictionary> + { + [walking] = new Dictionary + { + [ground] = 1 + } + }); + }, + Throws.ArgumentException.With.Message.EqualTo( + "Error when adding movement type 'Walking': movement costs contain unknown type: 'Ground'")); } [Test] public void GetAllTypes_ShouldReturnAllAddedTypes() { - var ground = new MovementType(1, "Ground"); - var water = new MovementType(123, "Water"); - var movementTypes = new MovementTypes( - new Dictionary> + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(2, "Swimming"); + + var movementTypes = new MovementTypes(new ITerrainType[] {ground, water}, + new Dictionary> { - [ground] = new Dictionary + [walkingType] = new Dictionary { [ground] = 1, [water] = 2 }, - [water] = new Dictionary + [swimmingType] = new Dictionary { [ground] = 2, [water] = 1 } - } - ); + }); - Assert.That(movementTypes.GetAllTypes(), Is.EquivalentTo(new[] {ground, water})); + Assert.That(movementTypes.GetAllMovementTypes(), Is.EquivalentTo(new[] {walkingType, swimmingType})); + Assert.That(movementTypes.GetAllTerrainTypes(), Is.EquivalentTo(new[] {ground, water})); } [Test] public void GetId_ShouldReturnIdOfTheType() { - var ground = new MovementType(1, "Ground"); - var water = new MovementType(123, "Water"); - var movementTypes = new MovementTypes( - new Dictionary> + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(123, "Swimming"); + + var movementTypes = new MovementTypes(new ITerrainType[] {ground, water}, + new Dictionary> { - [ground] = new Dictionary + [walkingType] = new Dictionary { [ground] = 1, [water] = 2 }, - [water] = new Dictionary + [swimmingType] = new Dictionary { [ground] = 2, [water] = 1 } - } - ); + }); - Assert.That(movementTypes.GetId(ground), Is.EqualTo(1)); - Assert.That(movementTypes.GetId(water), Is.EqualTo(123)); + Assert.That(movementTypes.GetMovementTypeId(walkingType), Is.EqualTo(1)); + Assert.That(movementTypes.GetMovementTypeId(swimmingType), Is.EqualTo(123)); } [Test] public void GetMovementCost_ShouldReturnCostFromAToB() { - var ground = new MovementType(1, "Ground"); - var water = new MovementType(2, "Water"); - var air = new MovementType(3, "Air"); - var movementTypes = new MovementTypes( - new Dictionary> + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + var air = new TerrainType(3, "Air"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(2, "Swimming"); + var flyingType = new MovementType(3, "Flying"); + + var movementTypes = new MovementTypes(new ITerrainType[] + { + ground, water, air + }, new Dictionary> + { + [walkingType] = new Dictionary { - [ground] = new Dictionary - { - [ground] = 1, - [water] = 2, - [air] = 999 - }, - [water] = new Dictionary - { - [ground] = 2, - [water] = 1, - [air] = 999 - }, - [air] = new Dictionary - { - [ground] = 1, - [water] = 1, - [air] = 1 - } + [ground] = 1, + [water] = 2, + [air] = 999 + }, + [swimmingType] = new Dictionary + { + [ground] = 2, + [water] = 1, + [air] = 999 + }, + [flyingType] = new Dictionary + { + [ground] = 1, + [water] = 1, + [air] = 1 } - ); - - Assert.That(movementTypes.GetMovementCost(ground, ground), Is.EqualTo(1)); - Assert.That(movementTypes.GetMovementCost(water, water), Is.EqualTo(1)); - Assert.That(movementTypes.GetMovementCost(ground, water), Is.EqualTo(2)); - Assert.That(movementTypes.GetMovementCost(water, ground), Is.EqualTo(2)); - Assert.That(movementTypes.GetMovementCost(ground, air), Is.EqualTo(999)); - Assert.That(movementTypes.GetMovementCost(air, ground), Is.EqualTo(1)); + }); + + Assert.That(movementTypes.GetMovementCost(walkingType, ground), Is.EqualTo(1)); + Assert.That(movementTypes.GetMovementCost(swimmingType, water), Is.EqualTo(1)); + Assert.That(movementTypes.GetMovementCost(walkingType, water), Is.EqualTo(2)); + Assert.That(movementTypes.GetMovementCost(swimmingType, ground), Is.EqualTo(2)); + Assert.That(movementTypes.GetMovementCost(walkingType, air), Is.EqualTo(999)); + Assert.That(movementTypes.GetMovementCost(flyingType, ground), Is.EqualTo(1)); } [Test] public void GetType_ShouldReturnTypeByTheId() { - var ground = new MovementType(1, "Ground"); - var water = new MovementType(123, "Water"); - var movementTypes = new MovementTypes( - new Dictionary> + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(123, "Swimming"); + + var movementTypes = new MovementTypes(new ITerrainType[] {ground, water}, + new Dictionary> { - [ground] = new Dictionary + [walkingType] = new Dictionary { [ground] = 1, [water] = 2 }, - [water] = new Dictionary + [swimmingType] = new Dictionary { [ground] = 2, [water] = 1 } - } - ); + }); - Assert.That(movementTypes.GetType(1), Is.EqualTo(ground)); - Assert.That(movementTypes.GetType(123), Is.EqualTo(water)); + Assert.That(movementTypes.GetMovementTypeById(1), Is.EqualTo(walkingType)); + Assert.That(movementTypes.GetMovementTypeById(123), Is.EqualTo(swimmingType)); } } } \ No newline at end of file diff --git a/HexCore.Tests/QuickStart.cs b/HexCore.Tests/QuickStart.cs new file mode 100644 index 0000000..4326d8c --- /dev/null +++ b/HexCore.Tests/QuickStart.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using HexCore; + +namespace HexCoreTests +{ + public class QuickStart + { + public static void Demo() + { + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(2, "Swimming"); + + var movementTypes = new MovementTypes( + new ITerrainType[] { ground, water }, + new Dictionary> + { + [walkingType] = new Dictionary + { + [ground] = 1, + [water] = 2 + }, + [swimmingType] = new Dictionary + { + [ground] = 2, + [water] = 1 + } + } + ); + + var graph = new Graph(new CellState[] { + new CellState(false, new Coordinate2D(0,0, OffsetTypes.OddRowsRight), ground), + new CellState(false, new Coordinate2D(0,1, OffsetTypes.OddRowsRight), ground), + new CellState(true, new Coordinate2D(1,0, OffsetTypes.OddRowsRight), water), + new CellState(false, new Coordinate2D(1,1, OffsetTypes.OddRowsRight), water), + new CellState(false, new Coordinate2D(1,2, OffsetTypes.OddRowsRight), ground) + }, movementTypes); + + var pawnPosition = new Coordinate2D(0,0, OffsetTypes.OddRowsRight).To3D(); + // Mark pawn's position as occupied + graph.BlockCells(pawnPosition); + + const int pawnMovementPoints = 3; + + var pawnMovementRange = graph.GetMovementRange( + pawnPosition, pawnMovementPoints, walkingType + ); + + var pawnGoal = new Coordinate2D(1, 2, OffsetTypes.OddRowsRight).To3D(); + + var theShortestPath = graph.GetShortestPath( + pawnPosition, + pawnGoal, + walkingType + ); + // When moving pawn, unblock old position and block the new one. + graph.UnblockCells(pawnPosition); + pawnPosition = pawnGoal; + graph.BlockCells(pawnGoal); + } + } +} \ No newline at end of file diff --git a/HexCore.Tests/Quickstart.Test.cs b/HexCore.Tests/Quickstart.Test.cs new file mode 100644 index 0000000..66a64ee --- /dev/null +++ b/HexCore.Tests/Quickstart.Test.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using HexCore; +using NUnit.Framework; + +namespace HexCoreTests +{ + [TestFixture] + public class QuickstartTest + { + [Test] + public void QuickstartExample_ShouldWork() + { + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(2, "Swimming"); + + var movementTypes = new MovementTypes( + new ITerrainType[] { ground, water }, + new Dictionary> + { + [walkingType] = new Dictionary + { + [ground] = 1, + [water] = 2 + }, + [swimmingType] = new Dictionary + { + [ground] = 2, + [water] = 1 + } + } + ); + + var graph = new Graph(new CellState[] { + new CellState(false, new Coordinate2D(0,0, OffsetTypes.OddRowsRight), ground), + new CellState(false, new Coordinate2D(0,1, OffsetTypes.OddRowsRight), ground), + new CellState(true, new Coordinate2D(1,0, OffsetTypes.OddRowsRight), water), + new CellState(false, new Coordinate2D(1,1, OffsetTypes.OddRowsRight), water), + new CellState(false, new Coordinate2D(1,2, OffsetTypes.OddRowsRight), ground) + }, movementTypes); + + var pawnPosition = new Coordinate2D(0,0, OffsetTypes.OddRowsRight).To3D(); + // Mark pawn's position as occupied + graph.BlockCells(pawnPosition); + + const int pawnMovementPoints = 3; + + var pawnMovementRange = graph.GetMovementRange( + pawnPosition, pawnMovementPoints, walkingType + ); + + var pawnGoal = new Coordinate2D(1, 2, OffsetTypes.OddRowsRight).To3D(); + + var theShortestPath = graph.GetShortestPath( + pawnPosition, + pawnGoal, + walkingType + ); + // When moving pawn, unblock old position and block the new one. + graph.UnblockCells(pawnPosition); + pawnPosition = pawnGoal; + graph.BlockCells(pawnGoal); + + Assert.That(graph.IsCellBlocked(pawnPosition), Is.True); + Assert.That(theShortestPath, Is.EquivalentTo(new [] + { + new Coordinate2D(0, 1, OffsetTypes.OddRowsRight).To3D(), + pawnGoal + })); + + var expectedMovementRange = new[] + { + new Coordinate2D(0,1, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(1,1, OffsetTypes.OddRowsRight).To3D(), + new Coordinate2D(1,2, OffsetTypes.OddRowsRight).To3D(), + }; + Assert.That(pawnMovementRange, Is.EquivalentTo(expectedMovementRange)); + } + } +} \ No newline at end of file diff --git a/HexCore.sln b/HexCore.sln index 97baf8d..9fccd00 100644 --- a/HexCore.sln +++ b/HexCore.sln @@ -7,7 +7,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TextFiles", "TextFiles", "{2B8FFB0F-C2EA-4C52-8548-CB06D997DA64}" ProjectSection(SolutionItems) = preProject README.md = README.md - TODO.md = TODO.md + LICENSE.md = LICENSE.md + CONTRIBUTING.md = CONTRIBUTING.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{5A67B6A9-1364-4362-88BC-28D1DE110B48}" @@ -19,6 +20,8 @@ ProjectSection(SolutionItems) = preProject publish.sh = publish.sh icon.png = icon.png icon48.png = icon48.png + NuGet.config = NuGet.config + .gitignore = .gitignore EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{8312DC1F-A2C1-4A30-8E62-59F41C9DA8CE}" diff --git a/HexCore/AStar/README.md b/HexCore/AStar/README.md deleted file mode 100644 index 073a1a7..0000000 --- a/HexCore/AStar/README.md +++ /dev/null @@ -1,9 +0,0 @@ -## A* - -Most of the time you don't need to use this class directly; Instead, you can use `FindShortestPath` -method of `HexGraph` instance. `AStarSearch` class is used by this method internally. - -A* one of the most common pathfing algorythms. -In order to work requires implementation of `IWeightedGraph` interface. - -`IWeightedInterface` should be able to provide information about neighbors of a givent point and movement cost to that point. diff --git a/HexCore/AStar/AStarSearch.cs b/HexCore/AStarSearch.cs similarity index 88% rename from HexCore/AStar/AStarSearch.cs rename to HexCore/AStarSearch.cs index ce28129..3c32b93 100644 --- a/HexCore/AStar/AStarSearch.cs +++ b/HexCore/AStarSearch.cs @@ -1,9 +1,7 @@ -using System; +using System; using System.Collections.Generic; -using HexCore.DataStructures; -using HexCore.HexGraph; -namespace HexCore.AStar +namespace HexCore { /** * This is path finding algorithm called 'A*'. It's one of the most common pathfindg algorithms. @@ -36,7 +34,7 @@ public static List FindShortestPath(IWeightedGraph graph, Coordina foreach (var next in graph.GetPassableNeighbors(current)) { - var newCost = costSoFar[current] + graph.GetMovementCost(next, unitMovementType); + var newCost = costSoFar[current] + graph.GetMovementCostForTheType(next, unitMovementType); if (costSoFar.ContainsKey(next) && newCost >= costSoFar[next]) continue; costSoFar[next] = newCost; var priority = newCost + Heuristic(next, goal); @@ -45,16 +43,20 @@ public static List FindShortestPath(IWeightedGraph graph, Coordina } } + var path = new List(); + var pathWasNotFound = !cameFrom.ContainsKey(goal); + + // Returning an empty list if the path wasn't found + if (pathWasNotFound) return path; + // Reconstructing path var curr = goal; - var path = new List(); while (!curr.Equals(start)) { path.Add(curr); curr = cameFrom[curr]; } - // path.Add(start); // optional // Reverse it to start at actual start point path.Reverse(); return path; diff --git a/HexCore/CellState.cs b/HexCore/CellState.cs new file mode 100644 index 0000000..ab273f5 --- /dev/null +++ b/HexCore/CellState.cs @@ -0,0 +1,29 @@ +using System; + +namespace HexCore +{ + [Serializable] + public class CellState + { + public Coordinate3D Coordinate3; + + // Never set this field directly, only through graph.BlockCells, since changing cell state requires + // rebuild of some graph coordinates. + public bool IsBlocked; + public ITerrainType TerrainType; + + public CellState(bool isBlocked, Coordinate3D coordinate3, ITerrainType terrainType) + { + IsBlocked = isBlocked; + Coordinate3 = coordinate3; + TerrainType = terrainType; + } + + public CellState(bool isBlocked, Coordinate2D coordinate2, ITerrainType terrainType) + { + IsBlocked = isBlocked; + Coordinate3 = coordinate2.To3D(); + TerrainType = terrainType; + } + } +} \ No newline at end of file diff --git a/HexCore/DataStructures/Coordinate2D.cs b/HexCore/Coordinate2D.cs similarity index 86% rename from HexCore/DataStructures/Coordinate2D.cs rename to HexCore/Coordinate2D.cs index ad5f7e4..5554a1d 100644 --- a/HexCore/DataStructures/Coordinate2D.cs +++ b/HexCore/Coordinate2D.cs @@ -2,18 +2,18 @@ using System.Collections.Generic; using System.Linq; -namespace HexCore.DataStructures +namespace HexCore { [Serializable] public struct Coordinate2D { public readonly int X, Y; - private readonly OffsetTypes _offsetType; + public readonly OffsetTypes OffsetType; public Coordinate3D To3D() { int x, y, z; - switch (_offsetType) + switch (OffsetType) { case OffsetTypes.OddRowsRight: x = X - (Y - Y % 2) / 2; @@ -36,7 +36,7 @@ public Coordinate3D To3D() y = -x - z; return new Coordinate3D(x, y, z); default: - throw new ArgumentOutOfRangeException(nameof(_offsetType), _offsetType, null); + throw new ArgumentOutOfRangeException(nameof(OffsetType), OffsetType, null); } } @@ -49,7 +49,9 @@ public Coordinate2D(int x, int y, OffsetTypes offsetType) { X = x; Y = y; - _offsetType = offsetType; + OffsetType = offsetType; } + + public override string ToString() => $"({X}, {Y})"; } } \ No newline at end of file diff --git a/HexCore/DataStructures/Coordinate3D.cs b/HexCore/Coordinate3D.cs similarity index 96% rename from HexCore/DataStructures/Coordinate3D.cs rename to HexCore/Coordinate3D.cs index 11df398..43a728c 100644 --- a/HexCore/DataStructures/Coordinate3D.cs +++ b/HexCore/Coordinate3D.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace HexCore.DataStructures +namespace HexCore { [Serializable] public struct Coordinate3D @@ -73,5 +73,7 @@ public Coordinate3D(int x, int y, int z) { return new Coordinate3D(a.X * scalar, a.Y * scalar, a.Z * scalar); } + + public override string ToString() => $"({X}, {Y}, {Z})"; } } \ No newline at end of file diff --git a/HexCore/DataStructures/README.md b/HexCore/DataStructures/README.md deleted file mode 100644 index 0c75b7c..0000000 --- a/HexCore/DataStructures/README.md +++ /dev/null @@ -1,18 +0,0 @@ -## Data structures - -This folder contains various data structures used by the grid toolkit. - -### Coordinate2D - -A data structure representing coordinate on a grid with offset (columns/rows) system. As columns and rows have offset, -offset needs to be specified when creating a coordinate. Can be converted to `Coordinate3D` using `.To3D()` instance -method. A list of `Coordinate2D `s can be converted to a list of `Coordinate3D`s using static `.To3D(Coordinate3D[])` -method. - -### Coordinate3D - -A data structure representing coordinate on a grid using a cubic coordinate system. All algorithms in this lib internally use -`Coordinate3D`. It's harder to understand, but easier to write algorithms with. Can be converted to 2D using instance -`.To2D(OffsetType)` and static `.To2D(Coordinate3D[], OffsetType)` methods. - -`PriorityQueue` - Like an ordinary queue, but with item weights \ No newline at end of file diff --git a/HexCore/Graph.cs b/HexCore/Graph.cs new file mode 100644 index 0000000..f38e6fb --- /dev/null +++ b/HexCore/Graph.cs @@ -0,0 +1,390 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HexCore +{ + [Serializable] + public class Graph : IWeightedGraph + { + // Possible directions to detect neighbors + public static readonly Coordinate3D[] Directions = { + new Coordinate3D(+1, -1, 0), + new Coordinate3D(+1, 0, -1), + new Coordinate3D(0, +1, -1), + new Coordinate3D(-1, +1, 0), + new Coordinate3D(-1, 0, +1), + new Coordinate3D(0, -1, +1) + }; + + private readonly List _cellStatesList = new List(); + + private List _allCoordinates; + + private Dictionary _cellStatesDictionary; + + private MovementTypes _movementTypes; + + public Graph(IEnumerable cellStatesList, MovementTypes movementTypes) + { + _movementTypes = movementTypes; + + AddCells(cellStatesList); + UpdateCoordinatesList(); + } + + /// + /// Returns passable neighbors of the cell + /// + /// + /// + public IEnumerable GetPassableNeighbors(Coordinate3D position) + { + return GetNeighbors(position, true); + } + + public IEnumerable GetPassableNeighbors(Coordinate2D position) + { + return GetPassableNeighbors(position.To3D()); + } + + /// + /// This methods gets movement costs to the coordinate for the movement type in range = 1. + /// Used internally by path finding and range finding. + /// + /// + /// + /// + /// + public int GetMovementCostForTheType(Coordinate3D coordinate, IMovementType unitMovementType) + { + if (!_movementTypes.ContainsMovementType(unitMovementType)) + throw new InvalidOperationException( + $"Unknown movement type: {unitMovementType.Name}"); + var cellState = GetCellState(coordinate); + return _movementTypes.GetMovementCost(unitMovementType, cellState.TerrainType); + } + + /* Private methods */ + + private void UpdateCellStateDictionary() + { + _cellStatesDictionary = new Dictionary(); + foreach (var cellState in _cellStatesList) _cellStatesDictionary.Add(cellState.Coordinate3, cellState); + } + + private void UpdateCoordinatesList() + { + _allCoordinates = _cellStatesList.Select(cell => cell.Coordinate3).ToList(); + } + + private void SetCellBlockStatus(IEnumerable coordinates, bool isBlocked) + { + foreach (var coordinate in coordinates) + { + var cellState = GetCellState(coordinate); + cellState.IsBlocked = isBlocked; + } + } + + private void SetCellBlockStatus(Coordinate3D coordinate, bool isBlocked) + { + SetCellBlockStatus(new []{coordinate}, isBlocked); + } + + /* Public methods */ + + /// + /// Returns neighbors for the cell + /// + /// Coordinate to get neighbors to + /// Return only passable neighbors + /// The list of this cell's neighbors + public IEnumerable GetNeighbors(Coordinate3D position, bool onlyPassable) + { + return Directions + .Select(direction => position + direction) + .Where(next => Contains(next) && !(onlyPassable && IsCellBlocked(next))); + } + + public IEnumerable GetNeighbors(Coordinate2D position, bool onlyPassable) + { + return Coordinate3D.To2D( + GetNeighbors(position.To3D(), onlyPassable), + position.OffsetType + ); + } + + public void AddCells(IEnumerable newCellStatesList) + { + var cellStates = newCellStatesList as CellState[] ?? newCellStatesList.ToArray(); + foreach (var cell in cellStates.Where(cell => !_movementTypes.ContainsTerrainType(cell.TerrainType))) + throw new InvalidOperationException( + $"One of the cells in graph has an unknown type: {cell.TerrainType.Name}"); + _cellStatesList.AddRange(cellStates); + UpdateCellStateDictionary(); + UpdateCoordinatesList(); + } + + public void RemoveCells(IEnumerable coordinatesToRemove) + { + _cellStatesList.RemoveAll(cellState => coordinatesToRemove.Contains(cellState.Coordinate3)); + UpdateCellStateDictionary(); + UpdateCoordinatesList(); + } + + public void RemoveCells(IEnumerable coordinatesToRemove2d) + { + RemoveCells(Coordinate2D.To3D(coordinatesToRemove2d)); + } + + public List GetAllCellsCoordinates() + { + return _allCoordinates; + } + + public List GetAllCellsCoordinates(OffsetTypes offsetType) + { + return Coordinate3D.To2D(_allCoordinates, offsetType); + } + + public List GetAllCells() + { + return _cellStatesList; + } + + public void BlockCells(IEnumerable coordinates) + { + SetCellBlockStatus(coordinates, true); + } + + public void BlockCells(Coordinate3D coordinate) + { + SetCellBlockStatus(coordinate, true); + } + + public void BlockCells(IEnumerable coordinates) + { + BlockCells(Coordinate2D.To3D(coordinates)); + } + + public void BlockCells(Coordinate2D coordinate) + { + BlockCells(coordinate.To3D()); + } + + public void UnblockCells(IEnumerable coordinates) + { + SetCellBlockStatus(coordinates, false); + } + + public void UnblockCells(Coordinate3D coordinate) + { + SetCellBlockStatus(coordinate, false); + } + + public void UnblockCells(IEnumerable coordinates) + { + UnblockCells(Coordinate2D.To3D(coordinates)); + } + + public void UnblockCells(Coordinate2D coordinate) + { + UnblockCells(coordinate.To3D()); + } + + public void SetCellsTerrainType(IEnumerable coordinates, ITerrainType terrainType) + { + foreach (var coordinate in coordinates) + { + var cellState = GetCellState(coordinate); + cellState.TerrainType = terrainType; + } + } + + public void SetCellsTerrainType(Coordinate3D coordinate, ITerrainType terrainType) + { + SetCellsTerrainType(new []{coordinate}, terrainType); + } + + public void SetCellsTerrainType(IEnumerable coordinates, ITerrainType terrainType) + { + SetCellsTerrainType(Coordinate2D.To3D(coordinates), terrainType); + } + + public void SetCellsTerrainType(Coordinate2D coordinate, ITerrainType terrainType) + { + SetCellsTerrainType(coordinate.To3D(), terrainType); + } + + /// + /// Returns true if graph contains the coordinate, false otherwise + /// + /// + /// + public bool Contains(Coordinate3D coordinate) + { + return _allCoordinates.Contains(coordinate); + } + + public bool Contains(Coordinate2D coordinate) + { + return Contains(coordinate.To3D()); + } + + /// + /// Returns true if the cell is marked as not passable, false otherwise + /// + /// + /// + public bool IsCellBlocked(Coordinate3D coordinate) + { + return GetCellState(coordinate).IsBlocked; + } + + public bool IsCellBlocked(Coordinate2D coordinate) + { + return IsCellBlocked(coordinate.To3D()); + } + + /// + /// Returns circular range of the cell + /// + /// The center of the range + /// Radius of the range + /// + public List GetRange(Coordinate3D startPosition, int radius) + { + var visited = new List {startPosition}; + var fringes = new List> + { + new List {new Fringe {Coordinate = startPosition, CostSoFar = 0}} + }; + + for (var currentRange = 0; currentRange < radius; currentRange++) + { + var newFringes = new List(); + foreach (var currentFringe in fringes[currentRange]) + foreach (var neighbor in GetNeighbors(currentFringe.Coordinate, false)) + { + if (visited.Contains(neighbor)) continue; + visited.Add(neighbor); + newFringes.Add(new Fringe {Coordinate = neighbor, CostSoFar = 0}); + } + + fringes.Add(newFringes); + } + + // So start position won't be included in the range + visited.Remove(startPosition); + return visited; + } + + public List GetRange(Coordinate2D startPosition, int radius) + { + return Coordinate3D.To2D( + GetRange(startPosition.To3D(), radius), + startPosition.OffsetType + ); + } + + public IEnumerable GetLine(Coordinate3D start, Coordinate3D direction, int length) + { + if (!Directions.Contains(direction)) throw new InvalidOperationException("Invalid direction"); + + for (var currentLength = 1; currentLength < length + 1; currentLength++) + { + var next = start + direction * currentLength; + if (Contains(next)) yield return next; + } + } + + /// + /// Similar to get range, but also takes two additional params: + /// + /// Center of the range + /// Amount of points allowed to spend on the movement + /// Movement type of the pawn to calculate movement range based on the movement points + /// + public List GetMovementRange(Coordinate3D startPosition, int movementPoints, + IMovementType movementType) + { + var visited = new List {startPosition}; + var fringes = new List> + { + new List {new Fringe {Coordinate = startPosition, CostSoFar = 0}} + }; + + for (var currentRangeIndex = 0; currentRangeIndex < movementPoints; currentRangeIndex++) + { + var newFringes = new List(); + foreach (var currentFringe in fringes[currentRangeIndex]) + foreach (var neighbor in GetPassableNeighbors(currentFringe.Coordinate)) + { + if (visited.Contains(neighbor)) continue; + var movementCostToNeighbor = GetMovementCostForTheType(neighbor, movementType); + var newCost = currentFringe.CostSoFar + movementCostToNeighbor; + if (newCost > movementPoints) continue; + visited.Add(neighbor); + newFringes.Add(new Fringe {Coordinate = neighbor, CostSoFar = newCost}); + } + + fringes.Add(newFringes); + } + + // So start position won't be included in the range + visited.Remove(startPosition); + return visited; + } + + public List GetMovementRange(Coordinate2D startPosition, int movementPoints, + IMovementType movementType) + { + return Coordinate3D.To2D( + GetMovementRange(startPosition.To3D(), movementPoints, movementType), + startPosition.OffsetType + ); + } + + /// + /// Returns the state of the cell for a given coordinate + /// + /// + /// + public CellState GetCellState(Coordinate3D coordinate) + { + return _cellStatesDictionary[coordinate]; + } + + public CellState GetCellState(Coordinate2D coordinate) + { + return GetCellState(coordinate.To3D()); + } + + /// + /// Finds shortest path from the start to the goal. Requires pawn's movement type to operate + /// + /// + /// + /// + /// + public List GetShortestPath(Coordinate3D start, Coordinate3D goal, IMovementType unitMovementType) + { + return AStarSearch.FindShortestPath(this, start, goal, unitMovementType); + } + + public List GetShortestPath(Coordinate2D start, Coordinate2D goal, IMovementType unitMovementType) + { + return Coordinate3D.To2D( + GetShortestPath(start.To3D(), goal.To3D(), unitMovementType), + start.OffsetType + ); + } + + private struct Fringe + { + public Coordinate3D Coordinate; + public int CostSoFar; + } + } +} \ No newline at end of file diff --git a/HexCore/HexGraph/GraphFactory.cs b/HexCore/GraphFactory.cs similarity index 74% rename from HexCore/HexGraph/GraphFactory.cs rename to HexCore/GraphFactory.cs index 6e66dbc..dd0150c 100644 --- a/HexCore/HexGraph/GraphFactory.cs +++ b/HexCore/GraphFactory.cs @@ -1,17 +1,15 @@ -using HexCore.DataStructures; - -namespace HexCore.HexGraph +namespace HexCore { public static class GraphFactory { public static Graph CreateRectangularGraph(int width, int height, MovementTypes movementTypes, - MovementType defaultMovementType, + ITerrainType defaultTerrainType, OffsetTypes offsetType = OffsetTypes.OddRowsRight) { var graph = new Graph(new CellState[] { }, movementTypes); - GraphUtils.ResizeSquareGraph(graph, offsetType, width, height, defaultMovementType); + GraphUtils.ResizeSquareGraph(graph, offsetType, width, height, defaultTerrainType); return graph; } diff --git a/HexCore/HexGraph/GraphUtils.cs b/HexCore/GraphUtils.cs similarity index 89% rename from HexCore/HexGraph/GraphUtils.cs rename to HexCore/GraphUtils.cs index ddd3a87..a5367d3 100644 --- a/HexCore/HexGraph/GraphUtils.cs +++ b/HexCore/GraphUtils.cs @@ -1,13 +1,12 @@ using System.Collections.Generic; using System.Linq; -using HexCore.DataStructures; -namespace HexCore.HexGraph +namespace HexCore { public static class GraphUtils { public static void ResizeSquareGraph(Graph graph, OffsetTypes offsetType, int newWidth, int newHeight, - MovementType defaultMovementType) + ITerrainType defaultTerrainType) { var offsetCoordinates = Coordinate3D.To2D(graph.GetAllCellsCoordinates(), offsetType); @@ -36,7 +35,7 @@ public static void ResizeSquareGraph(Graph graph, OffsetTypes offsetType, int ne var cellsToAdd = new List(); // We'll add new columns width already updated height for (var x = width; x < newWidth; x++) - cellsToAdd.AddRange(CreateNewCellsForColumn(x, 0, newHeight, defaultMovementType, + cellsToAdd.AddRange(CreateNewCellsForColumn(x, 0, newHeight, defaultTerrainType, offsetType)); graph.AddCells(cellsToAdd); @@ -47,7 +46,7 @@ public static void ResizeSquareGraph(Graph graph, OffsetTypes offsetType, int ne var cellsToAdd = new List(); // New columns already will have correct height; So only old needs to be resized. for (var x = 0; x < width; x++) - cellsToAdd.AddRange(CreateNewCellsForColumn(x, height, newHeight, defaultMovementType, + cellsToAdd.AddRange(CreateNewCellsForColumn(x, height, newHeight, defaultTerrainType, offsetType)); graph.AddCells(cellsToAdd); @@ -55,14 +54,14 @@ public static void ResizeSquareGraph(Graph graph, OffsetTypes offsetType, int ne } private static IEnumerable CreateNewCellsForColumn(int x, int oldY, int newY, - MovementType defaultMovementType, OffsetTypes offsetType) + ITerrainType defaultTerrainType, OffsetTypes offsetType) { var newCells = new List(); for (var y = oldY; y < newY; y++) { var position = new Coordinate2D(x, y, offsetType); var cubeCoordinate = position.To3D(); - newCells.Add(new CellState(false, cubeCoordinate, defaultMovementType)); + newCells.Add(new CellState(false, cubeCoordinate, defaultTerrainType)); } return newCells; diff --git a/HexCore/HexCore.csproj b/HexCore/HexCore.csproj index fbabfe5..71869a1 100644 --- a/HexCore/HexCore.csproj +++ b/HexCore/HexCore.csproj @@ -3,9 +3,9 @@ netstandard2.0 HexCore - 3.0.2 + 4.0.0 Anton Suprunchuk - Yet another library to manage hexagonal grids + Yet another library to manage hexagonal grids. For docs and usage examples please see the GitHub repo. Git https://github.com/antouhou/HexCore HexCore diff --git a/HexCore/HexGraph/CellState.cs b/HexCore/HexGraph/CellState.cs deleted file mode 100644 index 495e98a..0000000 --- a/HexCore/HexGraph/CellState.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using HexCore.DataStructures; - -namespace HexCore.HexGraph -{ - [Serializable] - public class CellState - { - public Coordinate3D Coordinate3; - - // Never set this field directly, only through graph.SetCellBlocked, since changing cell state requires - // rebuild of some graph coordinates. - public bool IsBlocked; - public IMovementType MovementType; - - public CellState(bool isBlocked, Coordinate3D coordinate3, IMovementType movementType) - { - IsBlocked = isBlocked; - Coordinate3 = coordinate3; - MovementType = movementType; - } - } -} \ No newline at end of file diff --git a/HexCore/HexGraph/GRAPH_FACTORY_README.md b/HexCore/HexGraph/GRAPH_FACTORY_README.md deleted file mode 100644 index e69de29..0000000 diff --git a/HexCore/HexGraph/Graph.cs b/HexCore/HexGraph/Graph.cs deleted file mode 100644 index 4c5fba3..0000000 --- a/HexCore/HexGraph/Graph.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using HexCore.AStar; -using HexCore.DataStructures; - -namespace HexCore.HexGraph -{ - [Serializable] - public class Graph : IWeightedGraph - { - // Possible directions to detect neighbors - private static readonly List Directions = new List - { - new Coordinate3D(+1, -1, 0), - new Coordinate3D(+1, 0, -1), - new Coordinate3D(0, +1, -1), - new Coordinate3D(-1, +1, 0), - new Coordinate3D(-1, 0, +1), - new Coordinate3D(0, -1, +1) - }; - - private readonly List _cellStatesList = new List(); - - private List _allCoordinates; - - private Dictionary _cellStatesDictionary; - - private List _emptyCells; - - private MovementTypes _movementTypes; - - public Graph(IEnumerable cellStatesList, MovementTypes movementTypes) - { - _movementTypes = movementTypes; - - AddCells(cellStatesList); - UpdateCoordinatesList(); - } - - public IEnumerable GetPassableNeighbors(Coordinate3D position) - { - return GetNeighbors(position, true); - } - - public int GetMovementCost(Coordinate3D coordinate, IMovementType unitMovementType) - { - if (!_movementTypes.Contains(unitMovementType)) - throw new InvalidOperationException( - $"Unknown movement type: {unitMovementType.Name}"); - var cellState = GetCellState(coordinate); - return _movementTypes.GetMovementCost(unitMovementType, cellState.MovementType); - } - - public void AddCells(IEnumerable newCellStatesList) - { - var cellStates = newCellStatesList as CellState[] ?? newCellStatesList.ToArray(); - foreach (var cell in cellStates.Where(cell => !_movementTypes.Contains(cell.MovementType))) - throw new InvalidOperationException( - $"One of the cells in graph has an unknown type: {cell.MovementType.Name}"); - _cellStatesList.AddRange(cellStates); - UpdateCellStateDictionary(); - UpdateCoordinatesList(); - } - - public void RemoveCells(List coordinatesToRemove) - { - _cellStatesList.RemoveAll(cellState => coordinatesToRemove.Contains(cellState.Coordinate3)); - UpdateCellStateDictionary(); - UpdateCoordinatesList(); - } - - public List GetAllCellsCoordinates() - { - return _allCoordinates; - } - - public void SetManyCellsBlocked(IEnumerable coordinates, bool isBlocked) - { - foreach (var coordinate in coordinates) - { - var cellState = GetCellState(coordinate); - cellState.IsBlocked = isBlocked; - } - - UpdateCoordinatesList(); - } - - public void SetOneCellBlocked(Coordinate3D coordinate, bool isBlocked) - { - SetManyCellsBlocked(new List {coordinate}, isBlocked); - } - - public void SetManyCellsMovementType(IEnumerable coordinates, IMovementType movementType) - { - foreach (var coordinate in coordinates) - { - var cellState = GetCellState(coordinate); - cellState.MovementType = movementType; - } - } - - public void SetOneCellMovementType(Coordinate3D coordinate, IMovementType movementType) - { - SetManyCellsMovementType(new List {coordinate}, movementType); - } - - private void UpdateCellStateDictionary() - { - _cellStatesDictionary = new Dictionary(); - foreach (var cellState in _cellStatesList) _cellStatesDictionary.Add(cellState.Coordinate3, cellState); - } - - private void UpdateCoordinatesList() - { - _allCoordinates = _cellStatesList.Select(cell => cell.Coordinate3).ToList(); - _emptyCells = _cellStatesList.Where(cell => !cell.IsBlocked).Select(cell => cell.Coordinate3).ToList(); - } - - public bool IsInBounds(Coordinate3D coordinate) - { - return _allCoordinates.Contains(coordinate); - } - - public bool IsCellBlocked(Coordinate3D coordinate) - { - return GetCellState(coordinate).IsBlocked; - } - - public IEnumerable GetNeighbors(Coordinate3D position, bool onlyPassable) - { - foreach (var direction in Directions) - { - var next = position + direction; - if (IsInBounds(next) && !(onlyPassable && IsCellBlocked(next))) yield return next; - } - } - - public List GetRange(Coordinate3D startPosition, int radius) - { - var visited = new List {startPosition}; - var fringes = new List> - { - new List {new Fringe {Coordinate = startPosition, CostSoFar = 0}} - }; - - for (var currentRange = 0; currentRange < radius; currentRange++) - { - var newFringes = new List(); - foreach (var currentFringe in fringes[currentRange]) - foreach (var neighbor in GetNeighbors(currentFringe.Coordinate, false)) - { - if (visited.Contains(neighbor)) continue; - visited.Add(neighbor); - newFringes.Add(new Fringe {Coordinate = neighbor, CostSoFar = 0}); - } - - fringes.Add(newFringes); - } - - // So start position won't be included in the range - visited.Remove(startPosition); - return visited; - } - - public IEnumerable GetLine(Coordinate3D start, Coordinate3D direction, int length) - { - if (!Directions.Contains(direction)) throw new InvalidOperationException("Invalid direction"); - - for (var currentLength = 1; currentLength < length + 1; currentLength++) - { - var next = start + direction * currentLength; - if (IsInBounds(next)) yield return next; - } - } - - public List GetMovementRange(Coordinate3D startPosition, int movementPoints, - IMovementType movementType) - { - var visited = new List {startPosition}; - var fringes = new List> - { - new List {new Fringe {Coordinate = startPosition, CostSoFar = 0}} - }; - - for (var currentRangeIndex = 0; currentRangeIndex < movementPoints; currentRangeIndex++) - { - var newFringes = new List(); - foreach (var currentFringe in fringes[currentRangeIndex]) - foreach (var neighbor in GetPassableNeighbors(currentFringe.Coordinate)) - { - if (visited.Contains(neighbor)) continue; - var movementCostToNeighbor = GetMovementCost(neighbor, movementType); - var newCost = currentFringe.CostSoFar + movementCostToNeighbor; - if (newCost > movementPoints) continue; - visited.Add(neighbor); - newFringes.Add(new Fringe {Coordinate = neighbor, CostSoFar = newCost}); - } - - fringes.Add(newFringes); - } - - // So start position won't be included in the range - visited.Remove(startPosition); - return visited; - } - - public CellState GetCellState(Coordinate3D coordinate) - { - return _cellStatesDictionary[coordinate]; - } - - public List GetShortestPath(Coordinate3D start, Coordinate3D goal, IMovementType unitMovementType) - { - return AStarSearch.FindShortestPath(this, start, goal, unitMovementType); - } - - private struct Fringe - { - public Coordinate3D Coordinate; - public int CostSoFar; - } - } -} \ No newline at end of file diff --git a/HexCore/HexGraph/MovementTypes.cs b/HexCore/HexGraph/MovementTypes.cs deleted file mode 100644 index 3fab54c..0000000 --- a/HexCore/HexGraph/MovementTypes.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace HexCore.HexGraph -{ - public class MovementTypes - { - private readonly Dictionary _ids = new Dictionary(); - - private readonly Dictionary _inverseIds = new Dictionary(); - - // > - private readonly Dictionary> _movementCosts = - new Dictionary>(); - - private readonly HashSet _movementTypes; - - // Public constructor - public MovementTypes(Dictionary> movementTypesWithCosts) - { - if (!movementTypesWithCosts.Any()) - throw new ArgumentException( - "Movement types should always have at least one explicitly defined type. For the reasoning, please visit the movement types section in the library's docs"); - _movementTypes = new HashSet(movementTypesWithCosts.Keys); - foreach (var movementType in movementTypesWithCosts) - { - // For fast lookups - _ids.Add(movementType.Key.Id, movementType.Key); - _inverseIds.Add(movementType.Key, movementType.Key.Id); - AddCostsForType(movementType.Key, movementType.Value); - } - } - - // Private methods - private void AddCostsForType( - IMovementType type, - Dictionary movementCostsUnitToPosition - ) - { - var missingTypes = GetAllTypes().Except(movementCostsUnitToPosition.Keys).ToArray(); - if (missingTypes.Any()) - { - var missingTypesEnumerationString = CreateTypesEnumerationString(missingTypes); - throw new ArgumentException( - $"Error when adding movement type '{type.Name}': missing movement costs to {missingTypesEnumerationString}"); - } - - var excessTypes = movementCostsUnitToPosition.Keys.Except(GetAllTypes()).ToArray(); - if (excessTypes.Any()) - { - var excessTypesEnumerationString = CreateTypesEnumerationString(excessTypes); - throw new ArgumentException( - $"Error when adding movement type '{type.Name}': movement costs contain unknown {excessTypesEnumerationString}"); - } - - _movementCosts.Add(type, movementCostsUnitToPosition); - } - - private static string CreateTypesEnumerationString(IMovementType[] movementTypes) - { - var movementTypeNames = movementTypes - .Select(movementType => movementType.Name).ToArray(); - var movementTypesNamesConcatenated = string.Join("', '", movementTypeNames); - var pluralEnding = movementTypeNames.Length > 1 ? "s" : ""; - return $"type{pluralEnding}: '{movementTypesNamesConcatenated}'"; - } - - // Public methods - public int GetId(IMovementType movementType) - { - return _inverseIds[movementType]; - } - - public IMovementType GetType(int typeId) - { - return _ids[typeId]; - } - - public IEnumerable GetAllTypes() - { - return _movementTypes; - } - - public int GetMovementCost(IMovementType from, IMovementType to) - { - return _movementCosts[from][to]; - } - - public bool Contains(IMovementType movementType) - { - return GetAllTypes().Contains(movementType); - } - } -} \ No newline at end of file diff --git a/HexCore/HexGraph/README.md b/HexCore/HexGraph/README.md deleted file mode 100644 index 25c664e..0000000 --- a/HexCore/HexGraph/README.md +++ /dev/null @@ -1,53 +0,0 @@ -## HexCore.HexGraph - -This directory contains a set of tools to interact with a hexagonal-based grid. - -The main class that you'll be interested in is `Graph`. - -### Basic usage - -`Graph` is used for most of the operations with a hexagonal grid, such as: -* Finding neighboring cells; -* Finding ranges; -* Pathfinding - -There is a factory class, `GraphFactory`, that will produce graphs for you. You need to specify graph size: - -```c# - var simpleGraph = GraphFactory.createSquareGraph(height: 3, width: 4); -``` - -Doing so will create a graph that looks like this: -``` - ⬡⬡⬡⬡ - ⬡⬡⬡⬡ - ⬡⬡⬡⬡ -``` -This is a square graph with odd rows placed right. `GraphFactory` can do more, check [its documentation](./GRAPH_FACTORY_README.md). - -### Pathfinding and ranges - -`Graph` class hash everything you can need for finding paths and ranges. - -To get the shortest path from A to B, call `graph.getShortestPath(coordinateA, coordinateB, movementType);`. If you -don't have any movement types, you can use default ones. A movement type is needed for applying movement penalties. -Imagine a situation, when your unit has far lower movement range in the water than on the ground. In this case, the shortest path would include as few water cells as possible. - -To get range, call `graph.getRange(center, radius)` - -To get movement range for a unit, call `graph.GetMovementRange(center, radius, movementType)`. This method will apply -movement penalties for different movement types. - -### Advanced usage - -If `GraphFactory` isn't enough for your needs, you can create your graphs without using it. -All you need to create a graph is a list of -`CellState` instances. `CellState` instances keep track of internal cell state and consist of -three fields and one method. These are: -- Cell coordinate in the grid; -- Is cell passable or not; -- Movement type. Pathfinding and movement range algorithms use movement type. - -To create a hex map, you need to define: -1. Movement types. There is a class for this, `MovementType`. -2. Create a list of cell states. \ No newline at end of file diff --git a/HexCore/HexGraph/IMovementType.cs b/HexCore/IMovementType.cs similarity index 84% rename from HexCore/HexGraph/IMovementType.cs rename to HexCore/IMovementType.cs index 4638fb7..4e30b0f 100644 --- a/HexCore/HexGraph/IMovementType.cs +++ b/HexCore/IMovementType.cs @@ -1,6 +1,6 @@ using System; -namespace HexCore.HexGraph +namespace HexCore { public interface IMovementType : IEquatable { diff --git a/HexCore/ITerrainType.cs b/HexCore/ITerrainType.cs new file mode 100644 index 0000000..15ed166 --- /dev/null +++ b/HexCore/ITerrainType.cs @@ -0,0 +1,8 @@ +namespace HexCore +{ + public interface ITerrainType + { + string Name { get; } + int Id { get; } + } +} \ No newline at end of file diff --git a/HexCore/AStar/IWeightedGraph.cs b/HexCore/IWeightedGraph.cs similarity index 51% rename from HexCore/AStar/IWeightedGraph.cs rename to HexCore/IWeightedGraph.cs index bfd143f..1a3760d 100644 --- a/HexCore/AStar/IWeightedGraph.cs +++ b/HexCore/IWeightedGraph.cs @@ -1,12 +1,10 @@ using System.Collections.Generic; -using HexCore.DataStructures; -using HexCore.HexGraph; -namespace HexCore.AStar +namespace HexCore { public interface IWeightedGraph { - int GetMovementCost(Coordinate3D a, IMovementType unitMovementType); + int GetMovementCostForTheType(Coordinate3D a, IMovementType unitMovementType); IEnumerable GetPassableNeighbors(Coordinate3D id); } } \ No newline at end of file diff --git a/HexCore/HexGraph/MovementType.cs b/HexCore/MovementType.cs similarity index 85% rename from HexCore/HexGraph/MovementType.cs rename to HexCore/MovementType.cs index 0c9110e..2cdff56 100644 --- a/HexCore/HexGraph/MovementType.cs +++ b/HexCore/MovementType.cs @@ -1,9 +1,9 @@ using System; -namespace HexCore.HexGraph +namespace HexCore { [Serializable] - public class MovementType : IMovementType + public struct MovementType : IMovementType { public MovementType(int id, string name) { diff --git a/HexCore/MovementTypes.cs b/HexCore/MovementTypes.cs new file mode 100644 index 0000000..39b1207 --- /dev/null +++ b/HexCore/MovementTypes.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HexCore +{ + public class MovementTypes + { + private readonly Dictionary _inverseMovementTypeIds = new Dictionary(); + + private readonly Dictionary _inverseTerrainTypeIds = new Dictionary(); + + // > + private readonly Dictionary> _movementCosts = + new Dictionary>(); + + private readonly Dictionary _movementTypeIds = new Dictionary(); + + private readonly HashSet _movementTypes; + private readonly Dictionary _terrainTypeIds = new Dictionary(); + private readonly HashSet _terrainTypes; + + // Public constructor + public MovementTypes(ITerrainType[] terrainTypes, + Dictionary> movementTypesWithCosts) + { + if (!movementTypesWithCosts.Any()) + throw new ArgumentException( + "Movement types should always have at least one explicitly defined type. For the reasoning, please visit the movement types section in the library's docs"); + _movementTypes = new HashSet(movementTypesWithCosts.Keys); + _terrainTypes = new HashSet(terrainTypes); + + foreach (var terrainType in _terrainTypes) + { + _terrainTypeIds.Add(terrainType.Id, terrainType); + _inverseTerrainTypeIds.Add(terrainType, terrainType.Id); + } + + foreach (var movementType in movementTypesWithCosts) + { + // For fast lookups + _movementTypeIds.Add(movementType.Key.Id, movementType.Key); + _inverseMovementTypeIds.Add(movementType.Key, movementType.Key.Id); + AddCostsForType(movementType.Key, movementType.Value); + } + } + + // Private methods + private void AddCostsForType( + IMovementType type, + Dictionary movementCostsToTerrain + ) + { + var missingTypes = GetAllTerrainTypes().Except(movementCostsToTerrain.Keys).ToArray(); + if (missingTypes.Any()) + { + var missingTypesEnumerationString = CreateTypesEnumerationString(missingTypes); + throw new ArgumentException( + $"Error when adding movement type '{type.Name}': missing movement costs to {missingTypesEnumerationString}"); + } + + var excessTypes = movementCostsToTerrain.Keys.Except(GetAllTerrainTypes()).ToArray(); + if (excessTypes.Any()) + { + var excessTypesEnumerationString = CreateTypesEnumerationString(excessTypes); + throw new ArgumentException( + $"Error when adding movement type '{type.Name}': movement costs contain unknown {excessTypesEnumerationString}"); + } + + _movementCosts.Add(type, movementCostsToTerrain); + } + + private static string CreateTypesEnumerationString(ITerrainType[] movementTypes) + { + var movementTypeNames = movementTypes + .Select(movementType => movementType.Name).ToArray(); + var movementTypesNamesConcatenated = string.Join("', '", movementTypeNames); + var pluralEnding = movementTypeNames.Length > 1 ? "s" : ""; + return $"type{pluralEnding}: '{movementTypesNamesConcatenated}'"; + } + + // Public methods + public int GetMovementTypeId(IMovementType movementType) + { + return _inverseMovementTypeIds[movementType]; + } + + public IMovementType GetMovementTypeById(int typeId) + { + return _movementTypeIds[typeId]; + } + + public IEnumerable GetAllMovementTypes() + { + return _movementTypes; + } + + public IEnumerable GetAllTerrainTypes() + { + return _terrainTypes; + } + + public int GetMovementCost(IMovementType from, ITerrainType to) + { + return _movementCosts[from][to]; + } + + public bool ContainsMovementType(IMovementType movementType) + { + return GetAllMovementTypes().Contains(movementType); + } + + public bool ContainsTerrainType(ITerrainType movementType) + { + return GetAllTerrainTypes().Contains(movementType); + } + } +} \ No newline at end of file diff --git a/HexCore/DataStructures/OffsetTypes.cs b/HexCore/OffsetTypes.cs similarity index 79% rename from HexCore/DataStructures/OffsetTypes.cs rename to HexCore/OffsetTypes.cs index ea5c699..0a98a00 100644 --- a/HexCore/DataStructures/OffsetTypes.cs +++ b/HexCore/OffsetTypes.cs @@ -1,4 +1,4 @@ -namespace HexCore.DataStructures +namespace HexCore { public enum OffsetTypes { diff --git a/HexCore/DataStructures/PriorityQueue.cs b/HexCore/PriorityQueue.cs similarity index 95% rename from HexCore/DataStructures/PriorityQueue.cs rename to HexCore/PriorityQueue.cs index 2d4979f..e82af8a 100644 --- a/HexCore/DataStructures/PriorityQueue.cs +++ b/HexCore/PriorityQueue.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace HexCore.DataStructures +namespace HexCore { public class PriorityQueue { diff --git a/HexCore/TerrainType.cs b/HexCore/TerrainType.cs new file mode 100644 index 0000000..ac9843e --- /dev/null +++ b/HexCore/TerrainType.cs @@ -0,0 +1,25 @@ +using System; + +namespace HexCore +{ + [Serializable] + public struct TerrainType : ITerrainType + { + public TerrainType(int id, string name) + { + Id = id; + Name = name; + } + + public int Id { get; } + public string Name { get; } + + public bool Equals(IMovementType other) + { + if (other is null) + return false; + + return Name == other.Name && Id == other.Id; + } + } +} \ No newline at end of file diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..464f913 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 2bec1b5..4a3ff44 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,102 @@ # HexCore ![alt text][logo] [![NuGet Version and Downloads count](https://buildstats.info/nuget/HexCore)](https://www.nuget.org/packages/HexCore) -HexCore is a library for building hexagonal-grid-based games. -The features include: -- A* pathfinding; -- Finding neighbors; -- Finding ranges; -- Finding ranges with penalties, that can be used to simulate different terrain types; -- Coordinate systems converter +HexCore is a library to perform various operations with a hexagonal grid, such as finding shortest paths from one cell to another, managing terrains and movement types, maintaining the grid state, finding various ranges, neighbors, various coordinate systems and converters between them, and some more stuff you may want to do with a hex grid. +## Installation + +The library can be installed from [NuGet](https://www.nuget.org/packages/HexCore). Run from the command line `dotnet add package HexCore` in your project or use your IDE of choice. + +## Usage + +For the detailed explanations please see [the docs](./Docs). + +### Quickstart + +```c# +using System.Collections.Generic; +using HexCore; + +namespace HexCoreTests +{ + public class QuickStart + { + public static void Demo() + { + var ground = new TerrainType(1, "Ground"); + var water = new TerrainType(2, "Water"); + + var walkingType = new MovementType(1, "Walking"); + var swimmingType = new MovementType(2, "Swimming"); + + var movementTypes = new MovementTypes( + new ITerrainType[] { ground, water }, + new Dictionary> + { + [walkingType] = new Dictionary + { + [ground] = 1, + [water] = 2 + }, + [swimmingType] = new Dictionary + { + [ground] = 2, + [water] = 1 + } + } + ); + + var graph = new Graph(new CellState[] { + new CellState(false, new Coordinate2D(0,0, OffsetTypes.OddRowsRight), ground), + new CellState(false, new Coordinate2D(0,1, OffsetTypes.OddRowsRight), ground), + new CellState(true, new Coordinate2D(1,0, OffsetTypes.OddRowsRight), water), + new CellState(false, new Coordinate2D(1,1, OffsetTypes.OddRowsRight), water), + new CellState(false, new Coordinate2D(1,2, OffsetTypes.OddRowsRight), ground) + }, movementTypes); + + var pawnPosition = new Coordinate2D(0,0, OffsetTypes.OddRowsRight).To3D(); + // Mark pawn's position as occupied + graph.BlockCells(pawnPosition); + + const int pawnMovementPoints = 3; + + var pawnMovementRange = graph.GetMovementRange( + pawnPosition, pawnMovementPoints, walkingType + ); + + var pawnGoal = new Coordinate2D(1, 2, OffsetTypes.OddRowsRight).To3D(); + + var theShortestPath = graph.GetShortestPath( + pawnPosition, + pawnGoal, + walkingType + ); + // When moving pawn, unblock old position and block the new one. + graph.UnblockCells(pawnPosition); + pawnPosition = pawnGoal; + graph.BlockCells(pawnGoal); + } + } +} +``` +The resulting graph would look like this: +``` + ⬡⬡ + ⬡⬡⬡ +``` +The two cells on top would have terrain type "ground", the two cells in the second row would represent our first lake, and the first water cell would be not passable - we can imagine that there's a sharp rock in the lake at that spot. + +For the detailed explanations please see [the docs](./Docs). + +## Contributing + +Everyone is welcome to contribute in any way of form! For the further details, please read [CONTRIBUTING.md](./CONTRIBUTING.md) + +## Authors + - [Anton Suprunchuk](https://github.com/antouhou) - [Website](https://antouhou.com) + +See also the list of contributors who participated in this project. + +## License + +This project is licensed under the MIT License - see the [LICENSE.md](./LICENSE.md) file for details diff --git a/TODO.md b/TODO.md deleted file mode 100644 index a03460b..0000000 --- a/TODO.md +++ /dev/null @@ -1,10 +0,0 @@ -# TODO - -In this file I'll keep track of what needs to be done. - -- [ ] Add attack functionality to unit - - [ ] Add `GetAttackPower` method to unit state which will return attack power based on unit's characteristcs -- [ ] Move unit movement code to UnitBehavior -- [ ] Complete Unit tutorial -- [ ] Complete HexGraph docs -- [ ] Complete BattleCore docs diff --git a/publish.sh b/publish.sh index 72b523d..9074911 100755 --- a/publish.sh +++ b/publish.sh @@ -1,11 +1,16 @@ +#!/usr/bin/env bash + VERSION=$1 API_KEY=$2 +GITHUB_TOKEN=$3 MISSING_OPTIONS_MESSAGE=" Invalid arguments!\n -Usage: ./publish.sh %version_to_publish% %nuget_key% +Usage: ./publish.sh %version_to_publish% %nuget_key% %GitHub_packages_key% " [[ -z "$1" ]] && { echo $MISSING_OPTIONS_MESSAGE; exit 1; } [[ -z "$2" ]] && { echo $MISSING_OPTIONS_MESSAGE ; exit 1; } +[[ -z "$3" ]] && { echo $MISSING_OPTIONS_MESSAGE ; exit 1; } dotnet pack ./HexCore/HexCore.csproj --configuration Release -dotnet nuget push ./HexCore/bin/Release/HexCore.$VERSION.nupkg -k $API_KEY -s https://api.nuget.org/v3/index.json \ No newline at end of file +dotnet nuget push ./HexCore/bin/Release/HexCore.$VERSION.nupkg -k ${API_KEY} -s "nuget.org" +GITHUB_TOKEN=${GITHUB_TOKEN} dotnet nuget push ./HexCore/bin/Release/HexCore.$VERSION.nupkg -s "github" \ No newline at end of file