From b46e3f51ce9051d6e42c3b4f060d7c9e82acab21 Mon Sep 17 00:00:00 2001 From: benskywalker Date: Fri, 22 Sep 2023 18:26:21 -0500 Subject: [PATCH 1/3] Adding BFC Calculations --- C7Engine/AI/StrategicAI/ExpansionPriority.cs | 8 ++-- .../AI/StrategicAI/UtilityCalculations.cs | 8 ++-- C7Engine/AI/StrategicAI/WarPriority.cs | 2 +- C7Engine/AI/UnitAI/SettlerLocationAI.cs | 47 +++++++++++-------- C7GameData/GameMap.cs | 26 ++++++++++ C7GameData/Tile.cs | 2 + 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/C7Engine/AI/StrategicAI/ExpansionPriority.cs b/C7Engine/AI/StrategicAI/ExpansionPriority.cs index 56af8104..4ee90a1b 100644 --- a/C7Engine/AI/StrategicAI/ExpansionPriority.cs +++ b/C7Engine/AI/StrategicAI/ExpansionPriority.cs @@ -22,11 +22,11 @@ public override void CalculateWeightAndMetadata(Player player) { if (player.cities.Count < 2) { this.calculatedWeight = 1000; } else { - int score = UtilityCalculations.CalculateAvailableLandScore(player); + double score = UtilityCalculations.CalculateAvailableLandScore(player); score = ApplyEarlyGameMultiplier(score); score = ApplyNationTraitMultiplier(score, player); - this.calculatedWeight = score; + this.calculatedWeight = (float) score; } } @@ -60,7 +60,7 @@ public override string ToString() { return "ExpansionPriority"; } - private int ApplyEarlyGameMultiplier(int score) + private double ApplyEarlyGameMultiplier(int score) { //If it's early game, multiply this score. //TODO: We haven't implemented the part for "how many turns does the game have?" yet. So this is hard-coded. @@ -72,7 +72,7 @@ private int ApplyEarlyGameMultiplier(int score) return score; } - private int ApplyNationTraitMultiplier(int score, Player player) { + private double ApplyNationTraitMultiplier(int score, Player player) { // TODO: The "Expansionist" trait should give a higher priority to this strategic priority. return score; } diff --git a/C7Engine/AI/StrategicAI/UtilityCalculations.cs b/C7Engine/AI/StrategicAI/UtilityCalculations.cs index 03c7d26e..1f34210b 100644 --- a/C7Engine/AI/StrategicAI/UtilityCalculations.cs +++ b/C7Engine/AI/StrategicAI/UtilityCalculations.cs @@ -13,12 +13,12 @@ public class UtilityCalculations { private static readonly int POSSIBLE_CITY_LOCATION_SCORE = 2; //how much weight to give to each possible city location private static readonly int TILE_SCORE_DIVIDER = 10; //how much to divide each location's tile score by - public static int CalculateAvailableLandScore(Player player) + public static double CalculateAvailableLandScore(Player player) { //Figure out if there's land to settle, and how much - Dictionary possibleLocations = SettlerLocationAI.GetScoredSettlerCandidates(player.cities[0].location, player); - int score = possibleLocations.Count * POSSIBLE_CITY_LOCATION_SCORE; - foreach (int i in possibleLocations.Values) { + Dictionary possibleLocations = SettlerLocationAI.GetScoredSettlerCandidates(player.cities[0].location, player); + double score = possibleLocations.Count * POSSIBLE_CITY_LOCATION_SCORE; + foreach (double i in possibleLocations.Values) { score += i / TILE_SCORE_DIVIDER; } return score; diff --git a/C7Engine/AI/StrategicAI/WarPriority.cs b/C7Engine/AI/StrategicAI/WarPriority.cs index 7010fdd4..fb8ce086 100644 --- a/C7Engine/AI/StrategicAI/WarPriority.cs +++ b/C7Engine/AI/StrategicAI/WarPriority.cs @@ -28,7 +28,7 @@ public override void CalculateWeightAndMetadata(Player player) { if (player.cities.Count < 2) { this.calculatedWeight = 0; } else { - int landScore = UtilityCalculations.CalculateAvailableLandScore(player); + double landScore = UtilityCalculations.CalculateAvailableLandScore(player); //N.B. Eventually this won't be an all-or-nothing proposition; if land is getting tight but not quite zero, //the AI may decide it's time for the next phrase of the game, especially if it's aggressive. if (landScore == 0) { //nowhere else to expand diff --git a/C7Engine/AI/UnitAI/SettlerLocationAI.cs b/C7Engine/AI/UnitAI/SettlerLocationAI.cs index 030b6087..2095dcc0 100644 --- a/C7Engine/AI/UnitAI/SettlerLocationAI.cs +++ b/C7Engine/AI/UnitAI/SettlerLocationAI.cs @@ -19,15 +19,15 @@ public class SettlerLocationAI { //Figures out where to plant Settlers public static Tile findSettlerLocation(Tile start, Player player) { - Dictionary scores = GetScoredSettlerCandidates(start, player); + Dictionary scores = GetScoredSettlerCandidates(start, player); if (scores.Count == 0 || scores.Values.Max() <= 0) { return Tile.NONE; //nowhere to settle } - IOrderedEnumerable > orderedScores = scores.OrderByDescending(t => t.Value); + IOrderedEnumerable > orderedScores = scores.OrderByDescending(t => t.Value); log.Debug("Top city location candidates from " + start + ":"); Tile returnValue = null; - foreach (KeyValuePair kvp in orderedScores.Take(5)) + foreach (KeyValuePair kvp in orderedScores.Take(5)) { if (returnValue == null) { returnValue = kvp.Key; @@ -39,36 +39,42 @@ public static Tile findSettlerLocation(Tile start, Player player) { return returnValue; } - public static Dictionary GetScoredSettlerCandidates(Tile start, Player player) { + public static Dictionary GetScoredSettlerCandidates(Tile start, Player player) { List playerUnits = player.units; IEnumerable candidates = player.tileKnowledge.AllKnownTiles().Where(t => !IsInvalidCityLocation(t)); - Dictionary scores = AssignTileScores(start, player, candidates, playerUnits.FindAll(u => u.unitType.name == "Settler")); + Dictionary scores = AssignTileScores(start, player, candidates, playerUnits.FindAll(u => u.unitType.name == "Settler")); return scores; } - private static Dictionary AssignTileScores(Tile startTile, Player player, IEnumerable candidates, List playerSettlers) + private static Dictionary AssignTileScores(Tile startTile, Player player, IEnumerable candidates, List playerSettlers) { - Dictionary scores = new Dictionary(); + Dictionary scores = new Dictionary(); candidates = candidates.Where(t => !SettlerAlreadyMovingTowardsTile(t, playerSettlers) && t.IsAllowCities()); foreach (Tile t in candidates) { - int score = GetTileYieldScore(t, player); + double score = GetTileYieldScore(t, player); //For simplicity's sake, I'm only going to look at immediate neighbors here, but //a lot more things should be considered over time. foreach (Tile nt in t.neighbors.Values) { - score += GetTileYieldScore(nt, player); + double yieldScore = GetTileYieldScore(nt, player); + log.Information("Neighbor tile has score of " + yieldScore); + score += yieldScore; + } + //Also look at the next ring out, with lower weights. + foreach (Tile outerTile in t.neighbors.Values) + { + double outerTileScore = GetTileYieldScore(outerTile, player) / 3; + score += outerTileScore; + log.Information("Outer ring tile has yield score of " + outerTileScore); } - //TODO: Also look at the next ring out, with lower weights. //Prefer hills for defense, and coast for boats and such. - if (t.baseTerrainType.Key == "hills") { - score += 10; - } - if (t.NeighborsWater()) { - score += 10; - } + // In this scale, the defense bonus of hills adds up to a bonus of +10, which is equivalent to the previous hardcoded bonus. This just opens up possibilities with editing terrain. + score += t.baseTerrainType.defenseBonus.amount * 20.0; + + // Need to add a way to check freshwater source, and separately to check if coast is lake or coast tile. This score would not apply if the city only borders a lake, although we still need a freshwater bonus //Lower scores if they are far away - int preDistanceScore = score; + double preDistanceScore = score; int distance = startTile.distanceTo(t); if (distance > 4) { score -= distance * 2; @@ -80,14 +86,17 @@ private static Dictionary AssignTileScores(Tile startTile, Player pla } if (score > 0) scores[t] = score; + log.Information("Tile score for settling is " + score); } return scores; } - private static int GetTileYieldScore(Tile t, Player owner) + private static double GetTileYieldScore(Tile t, Player owner) { - int score = t.foodYield(owner) * 5; + double score = t.foodYield(owner) * 5; score += t.productionYield(owner) * 3; score += t.commerceYield(owner) * 2; + + // TODO: Add multipliers for resource rarity, utility, and whether this player has a surplus if (t.Resource.Category == ResourceCategory.STRATEGIC) { score += STRATEGIC_RESOURCE_BONUS; } diff --git a/C7GameData/GameMap.cs b/C7GameData/GameMap.cs index 30fa3a88..54195301 100644 --- a/C7GameData/GameMap.cs +++ b/C7GameData/GameMap.cs @@ -57,6 +57,32 @@ public void computeNeighbors() { } } + // Compute the outer ring of the BFC of tiles. Might not be necessary for tiles which can't be settled? + public void computeOuterRing() + { + // List of all the BFC tiles NOT including direct Neighbors + // Essentially, this is every outer tile except North->North, South->South, West->West,East->East + (TileDirection direction1, TileDirection direction2) [] outerRingDirections = {(TileDirection.NORTHWEST, TileDirection.NORTH),(TileDirection.NORTHWEST, TileDirection.NORTHWEST),(TileDirection.NORTHWEST, TileDirection.WEST),(TileDirection.SOUTHWEST, TileDirection.WEST),(TileDirection.SOUTHWEST, TileDirection.SOUTHWEST),(TileDirection.SOUTHWEST, TileDirection.SOUTH),(TileDirection.SOUTHEAST, TileDirection.EAST),(TileDirection.SOUTHEAST, TileDirection.SOUTHEAST),(TileDirection.SOUTHEAST, TileDirection.SOUTH),(TileDirection.NORTHEAST, TileDirection.NORTH),(TileDirection.NORTHEAST, TileDirection.NORTHEAST),(TileDirection.NORTHEAST, TileDirection.EAST)}; + + foreach (Tile tile in tiles) + { + Dictionary<(TileDirection,TileDirection), Tile> outerRing = new Dictionary<(TileDirection,TileDirection), Tile>(); + foreach((TileDirection dir1, TileDirection dir2) directions in outerRingDirections) + { + Tile inner = tileNeighbor(tile,directions.dir1); + if(inner != Tile.NONE) + { + Tile outer = tileNeighbor(inner, directions.dir2); + if(outer != Tile.NONE) + { + outerRing[(directions.dir1,directions.dir2)] = outer; + } + } + } + tile.outerRing = outerRing; + } + } + // This method verifies that the conversion between tile index and coords is consistent for all possible valid inputs. It's not called // anywhere but I'm keeping it around in case we ever need to work on the conversion methods again. public void testTileIndexComputation() diff --git a/C7GameData/Tile.cs b/C7GameData/Tile.cs index d62b85d2..741e5bd3 100644 --- a/C7GameData/Tile.cs +++ b/C7GameData/Tile.cs @@ -37,6 +37,8 @@ public class Tile public Resource Resource { get; set; } public Dictionary neighbors { get; set; } = new Dictionary(); + + public Dictionary<(TileDirection,TileDirection), Tile> outerRing { get; set; } = new Dictionary<(TileDirection,TileDirection), Tile>(); //See discussion on page 4 of the "Babylon" thread (https://forums.civfanatics.com/threads/0-1-babylon-progress-thread.673959) about sub-terrain type and Civ3 properties. //We may well move these properties somewhere, whether that's Civ3ExtraInfo, a Civ3Tile child class, a Dictionary property, or something else, in the future. From d96d1de547f48c0b9af5d4d89fb15a2221df6cf4 Mon Sep 17 00:00:00 2001 From: benskywalker Date: Fri, 22 Sep 2023 18:57:36 -0500 Subject: [PATCH 2/3] Adding structure for tileimprovement score --- C7Engine/AI/StrategicAI/ExpansionPriority.cs | 4 ++-- C7Engine/AI/UnitAI/SettlerLocationAI.cs | 17 ++++++++++++++++- C7GameData/GameData.cs | 2 ++ C7GameData/TerrainType.cs | 8 +++++++- C7GameData/Tile.cs | 20 ++++++++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/C7Engine/AI/StrategicAI/ExpansionPriority.cs b/C7Engine/AI/StrategicAI/ExpansionPriority.cs index 4ee90a1b..c14a9350 100644 --- a/C7Engine/AI/StrategicAI/ExpansionPriority.cs +++ b/C7Engine/AI/StrategicAI/ExpansionPriority.cs @@ -60,7 +60,7 @@ public override string ToString() { return "ExpansionPriority"; } - private double ApplyEarlyGameMultiplier(int score) + private double ApplyEarlyGameMultiplier(double score) { //If it's early game, multiply this score. //TODO: We haven't implemented the part for "how many turns does the game have?" yet. So this is hard-coded. @@ -72,7 +72,7 @@ private double ApplyEarlyGameMultiplier(int score) return score; } - private double ApplyNationTraitMultiplier(int score, Player player) { + private double ApplyNationTraitMultiplier(double score, Player player) { // TODO: The "Expansionist" trait should give a higher priority to this strategic priority. return score; } diff --git a/C7Engine/AI/UnitAI/SettlerLocationAI.cs b/C7Engine/AI/UnitAI/SettlerLocationAI.cs index 2095dcc0..a04d8b82 100644 --- a/C7Engine/AI/UnitAI/SettlerLocationAI.cs +++ b/C7Engine/AI/UnitAI/SettlerLocationAI.cs @@ -55,14 +55,16 @@ private static Dictionary AssignTileScores(Tile startTile, Player //For simplicity's sake, I'm only going to look at immediate neighbors here, but //a lot more things should be considered over time. foreach (Tile nt in t.neighbors.Values) { + double improvementScore = GetTileImprovementScore(nt, player); double yieldScore = GetTileYieldScore(nt, player); log.Information("Neighbor tile has score of " + yieldScore); + log.Information("Neighbor tile has improvement score of " + improvementScore); score += yieldScore; } //Also look at the next ring out, with lower weights. foreach (Tile outerTile in t.neighbors.Values) { - double outerTileScore = GetTileYieldScore(outerTile, player) / 3; + double outerTileScore = (GetTileYieldScore(outerTile, player) + GetTileImprovementScore(outerTile, player)) / 3; score += outerTileScore; log.Information("Outer ring tile has yield score of " + outerTileScore); } @@ -106,6 +108,19 @@ private static double GetTileYieldScore(Tile t, Player owner) return score; } + private static double GetTileImprovementScore (Tile t, Player owner) + { + double irrigationBonus = t.irrigationYield(owner); + double mineBonus = t.miningYield(); + + // Food is more important than production + double irrigationValue = irrigationBonus * 5; + double mineValue = mineBonus * 3; + + // Since we can only irrigate OR mine, we just return the max of the two + return Math.Max(irrigationValue,mineValue); + } + public static bool IsInvalidCityLocation(Tile tile) { if (tile.HasCity) { log.Verbose("Tile " + tile + " is invalid due to existing city of " + tile.cityAtTile.name); diff --git a/C7GameData/GameData.cs b/C7GameData/GameData.cs index d8a92a99..dc8edeb4 100644 --- a/C7GameData/GameData.cs +++ b/C7GameData/GameData.cs @@ -81,6 +81,8 @@ public void PerformPostLoadActions() { //Let each tile know who its neighbors are. It needs to know this so its graphics can be selected appropriately. map.computeNeighbors(); + // Compute outer ring for settler ai + map.computeOuterRing(); } /** diff --git a/C7GameData/TerrainType.cs b/C7GameData/TerrainType.cs index e01fc508..db764f7d 100644 --- a/C7GameData/TerrainType.cs +++ b/C7GameData/TerrainType.cs @@ -17,7 +17,11 @@ public class TerrainType public int baseShieldProduction {get; set; } public int baseCommerceProduction {get; set; } public int movementCost {get; set; } - public bool allowCities { get; set; } = true; + public bool allowCities {get; set; } = true; + + public int miningBonus {get; set; } + + public int irrigationBonus {get; set; } public StrengthBonus defenseBonus; //some stuff about graphics would probably make sense, too @@ -51,6 +55,8 @@ public static TerrainType ImportFromCiv3(int civ3Index, TERR civ3Terrain) c7Terrain.baseFoodProduction = civ3Terrain.Food; c7Terrain.baseShieldProduction = civ3Terrain.Shields; c7Terrain.baseCommerceProduction = civ3Terrain.Commerce; + c7Terrain.irrigationBonus = civ3Terrain.IrrigationBonus; + c7Terrain.miningBonus = civ3Terrain.MiningBonus; c7Terrain.movementCost = civ3Terrain.MovementCost; c7Terrain.allowCities = civ3Terrain.AllowCities != 0; c7Terrain.defenseBonus = new StrengthBonus { diff --git a/C7GameData/Tile.cs b/C7GameData/Tile.cs index 741e5bd3..8e5a7c19 100644 --- a/C7GameData/Tile.cs +++ b/C7GameData/Tile.cs @@ -211,6 +211,26 @@ public int commerceYield(Player player) return yield; } + public int irrigationYield(Player player) + { + int yield = overlayTerrainType.irrigationBonus; + // Only applies if we border freshwater OR have unlocked irrigation everywhere + if(BordersRiver()) + { + return yield; + } + else + { + // Player can not irrigate this. We really need a more rigorous freshwater system since lakes and chain irrigation exist. That's not within the scope of this pr though so this logic will be a placeholder + return 0; + } + } + + public int miningYield() + { + return overlayTerrainType.miningBonus; + } + private bool BordersRiver() { return riverNorth || riverNortheast || riverEast || riverSoutheast || riverSouth || riverSouthwest || riverWest || riverNorthwest; } From b252df1cb75acdc0affccf9ac06c8a212f34d163 Mon Sep 17 00:00:00 2001 From: Ben Bowles Date: Sat, 23 Sep 2023 22:51:12 -0400 Subject: [PATCH 3/3] Fix Lake Logic and apply to production --- C7Engine/AI/CityProductionAI.cs | 2 +- C7Engine/AI/UnitAI/SettlerLocationAI.cs | 6 +- C7GameData/Tile.cs | 186 ++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 3 deletions(-) diff --git a/C7Engine/AI/CityProductionAI.cs b/C7Engine/AI/CityProductionAI.cs index 4907236a..86c2e66b 100644 --- a/C7Engine/AI/CityProductionAI.cs +++ b/C7Engine/AI/CityProductionAI.cs @@ -51,7 +51,7 @@ public static IProducible GetNextItemToBeProduced(City city, IProducible lastPro //Exclude naval units from land-only cities - if (unitPrototype.categories.Contains("Sea") && !city.location.NeighborsWater()) { + if (unitPrototype.categories.Contains("Sea") && !city.location.Coastal()) { flatAdjustedScore = 0.0f; } diff --git a/C7Engine/AI/UnitAI/SettlerLocationAI.cs b/C7Engine/AI/UnitAI/SettlerLocationAI.cs index a04d8b82..9b7a184e 100644 --- a/C7Engine/AI/UnitAI/SettlerLocationAI.cs +++ b/C7Engine/AI/UnitAI/SettlerLocationAI.cs @@ -113,8 +113,10 @@ private static double GetTileImprovementScore (Tile t, Player owner) double irrigationBonus = t.irrigationYield(owner); double mineBonus = t.miningYield(); - // Food is more important than production - double irrigationValue = irrigationBonus * 5; + // Food is more important than production + // Irrigation only applies to freshwater tiles, although we should add a check for electricity later. + // Also this doesn't account for the ability to chain irrigation. + double irrigationValue = irrigationBonus * 5 * (t.providesFreshWater() ? 1 : 0); double mineValue = mineBonus * 3; // Since we can only irrigate OR mine, we just return the max of the two diff --git a/C7GameData/Tile.cs b/C7GameData/Tile.cs index 8e5a7c19..666daf56 100644 --- a/C7GameData/Tile.cs +++ b/C7GameData/Tile.cs @@ -4,6 +4,7 @@ namespace C7GameData using System.Text.Json.Serialization; using System.Collections.Generic; using System.Linq; + public class Tile { // ExtraInfo will eventually be type object and use a type descriminator in JSON to determine @@ -57,12 +58,148 @@ public class Tile public TileOverlays overlays = new TileOverlays(); + + // -1 = not initialized. 0 = false, 1 = true + public int isLake = -1; + public Tile() { unitsOnTile = new List(); Resource = Resource.NONE; } + public bool IsLake() + { + // If the isLake value has been initialized, we should return that + if(isLake != -1) + { + return (isLake == 1); + } + + if(!IsWater()) + { + isLake = 0; + return false; + } + + int contiguousWater = 0; + List contiguous = new List(); + List searchedWater = new List(); + contiguous.Add(this); + + // Default is that this *is* a lake because other conditions will assure we only get water tiles. If the lake is under size 21, we want to default to it being a lake + bool lake = true; + + int i = 0; + while(i < contiguous.Count()) + { + Tile currentTile = contiguous[i]; + + // Skip land tiles and remove from the stack. This should be checked before they're added but just in case + if(!currentTile.IsWater()) + { + currentTile.isLake = 0; + contiguous.Remove(currentTile); + continue; + } + contiguousWater += 1; + if(contiguousWater > 20) + { + lake = false; + break; + } + + // If a contiguous tile is a lake, this (and all contiguous water) is too. This works because contiguousness is transitive. If it is *not* a lake (and is a water tile), then this isn't either. + if(currentTile.isLake == 1) + { + lake = true; + break; + } + if(currentTile.isLake == 0) + { + lake = false; + break; + } + // If we haven't found out one way or the other, add all of this tile's water neighbors to the stack and then remove this tile + foreach((TileDirection dir, Tile neighbor) in currentTile.neighbors) + { + if(!neighbor.IsWater()) + { + continue; + } + else if(contiguous.Contains(neighbor) || searchedWater.Contains(neighbor)) + { + continue; + } + else + { + // Add neighbor to tiles we should search as long as there's no isthmus blocking + if(currentTile.isthmusAt(dir)) + { + continue; + } + else + { + contiguous.Add(neighbor); + } + } + } + i ++; + } + if(lake) + { + // Update all the contiguous water tiles we've looked at. It won't be the whole body of water but it'll save us some repeats. + foreach(Tile currentTile in contiguous) + { + if(currentTile.IsWater()) + { + currentTile.isLake = 1; + } + } + return true; + } + else + { + // Update searched water as non-lake. + foreach(Tile currentTile in contiguous) + { + currentTile.isLake = 0; + } + /*foreach(Tile currentTile in searchedWater) + { + currentTile.isLake = 0; + }*/ + return false; + } + } + + public bool providesFreshWater () + { + return IsLake() || BordersRiver() || NeighborsLake(); + } + + // Check for an isthmus that can separate lakes from oceans, affect navigability, etc. + // Might want to store this for efficiency? Not sure how important this is in terms of runtime. Should only be run on two water tiles during IsLake + public bool isthmusAt (TileDirection dir) + { + + Tile adjacent = neighbors[dir]; + + if(adjacent != Tile.NONE && IsWater() && adjacent.IsWater() && dir.isDiagonal()) + { + (TileDirection d1, TileDirection d2) = dir.diagonalComplementary(); + Tile t1 = neighbors[d1]; + Tile t2 = neighbors[d2]; + + if(!t1.IsWater() && !t2.IsWater()) + { + // An isthmus separates diagonal water tiles with coastal tiles in both complementary directions + return true; + } + } + return false; + } + // TODO: this should be either an extension in C7Engine, or otherwise // calculated somewhere else, but it's not obvious to someone unfamiliar // with the save format that it's the overaly terrain that has actual @@ -89,6 +226,29 @@ public bool NeighborsWater() { return false; } + public bool NeighborsLake() + { + foreach (Tile neighbor in neighbors.Values) + { + if (neighbor.IsLake()) + { + return true; + } + } + return false; + } + + // Check if the tile is coastal, as differentiated from being on a lake. + public bool Coastal() + { + foreach (Tile neighbor in neighbors.Values) { + if (neighbor.IsWater() && !neighbor.IsLake()) { + return true; + } + } + return false; + } + /// /// Returns neighbors along edges only. /// This is used by some graphics algorithms. @@ -283,6 +443,32 @@ public static (int, int) toCoordDiff(this TileDirection dir) } } + // Essentially finds the two other tiles that neighbor BOTH diagonal tiles + public static (TileDirection, TileDirection) diagonalComplementary(this TileDirection dir) + { + switch(dir) + { + case TileDirection.NORTH: return (TileDirection.NORTHEAST, TileDirection.NORTHWEST); + case TileDirection.EAST: return (TileDirection.SOUTHEAST, TileDirection.NORTHWEST); + case TileDirection.SOUTH: return (TileDirection.SOUTHEAST, TileDirection.SOUTHWEST); + case TileDirection.WEST: return (TileDirection.NORTHWEST, TileDirection.SOUTHWEST); + default: throw new ArgumentOutOfRangeException("TileDirection is not Diagonal"); + } + } + + public static bool isDiagonal(this TileDirection dir) + { + switch(dir) + { + case TileDirection.NORTH: + case TileDirection.EAST: + case TileDirection.SOUTH: + case TileDirection.WEST: + return true; + default: return false; + } + } + // public static string shortName(this TileDirection dir) { // switch (dir) { // case TileDirection.NORTH: return "N";