diff --git a/C7/MapView.cs b/C7/MapView.cs index badb5f41..2e1de2f3 100644 --- a/C7/MapView.cs +++ b/C7/MapView.cs @@ -271,7 +271,7 @@ public override void drawObject(LooseView looseView, GameData gameData, Tile til int randomJungleRow = tile.YCoordinate % 2; int randomJungleColumn; ImageTexture jungleTexture; - if (tile.getEdgeNeighbors().Any(t => t.IsWater())) { + if (tile.GetEdgeNeighbors().Any(t => t.IsWater())) { randomJungleColumn = tile.XCoordinate % 6; jungleTexture = smallJungleTexture; } else { @@ -298,7 +298,7 @@ public override void drawObject(LooseView looseView, GameData gameData, Tile til } } else { forestRow = tile.YCoordinate % 2; - if (tile.getEdgeNeighbors().Any(t => t.IsWater())) { + if (tile.GetEdgeNeighbors().Any(t => t.IsWater())) { forestColumn = tile.XCoordinate % 5; if (tile.baseTerrainType.Key == "grassland") { forestTexture = smallForestTexture; @@ -343,7 +343,7 @@ public override void drawObject(LooseView looseView, GameData gameData, Tile til int randomJungleRow = tile.YCoordinate % 2; int randomMarshColumn; ImageTexture marshTexture; - if (tile.getEdgeNeighbors().Any(t => t.IsWater())) { + if (tile.GetEdgeNeighbors().Any(t => t.IsWater())) { randomMarshColumn = tile.XCoordinate % 5; marshTexture = smallMarshTexture; } else { diff --git a/C7Engine/C7GameData/City.cs b/C7Engine/C7GameData/City.cs index 2941a95f..d05c2bb2 100644 --- a/C7Engine/C7GameData/City.cs +++ b/C7Engine/C7GameData/City.cs @@ -670,8 +670,9 @@ public List GetTilesWithinBorders() { private List GetTilesOfRank(int rank) { List result = new(); foreach (Tile t in location.GetTilesWithinRankDistance(rank)) { + // Law II // Ocean tiles may only hold claims of rank 2. - if (t.baseTerrainType.Key == "ocean" && rank > 2) { + if (t.baseTerrainType.Key == "ocean" && t.rankDistanceTo(location) > 2) { continue; } result.Add(t); diff --git a/C7Engine/C7GameData/GameData.cs b/C7Engine/C7GameData/GameData.cs index b114bb47..a1aa392b 100644 --- a/C7Engine/C7GameData/GameData.cs +++ b/C7Engine/C7GameData/GameData.cs @@ -133,8 +133,8 @@ public void UpdateTileOwners() { foreach (Tile t in city.GetTilesWithinBorders()) { // If another city has claim to this tile, we need to resolve // that conflict. - if (t.owningCity != null) { - t.owningCity = ResolveTileOwnershipConflict(t.owningCity, city, t); + if (t.owningCity != null && ResolveTileOwnershipConflict(t.owningCity, city, t, out City winnerCity)) { + t.owningCity = winnerCity; t.owningCity.owner.tileKnowledge.AddTilesToKnown(t, recomputeActiveTiles); continue; } @@ -147,7 +147,30 @@ public void UpdateTileOwners() { foreach (Player player in players) { player.tileKnowledge.RecomputeActiveTiles(); player.UpdateResourcesInBorders(map.tiles.Where(t => t.owningCity?.owner == player)); + + foreach (Tile t in player.tileKnowledge.knownTiles.Where(t => t.owningCity == null && t.GetEdgeNeighbors().Any(e => e.owningCity != null)).ToList()) { + // Law VII + TryResolveOpposingNeighbors(t, TileDirection.NORTHWEST, TileDirection.SOUTHEAST); + if (t.owningCity != null) continue; + // Law VIII + TryResolveOpposingNeighbors(t, TileDirection.NORTHEAST, TileDirection.SOUTHWEST); + } + } + } + + private void TryResolveOpposingNeighbors(Tile t, TileDirection dirA, TileDirection dirB) { + if (!t.neighbors.TryGetValue(dirA, out Tile a) || !t.neighbors.TryGetValue(dirB, out Tile b)) return; + if (a.owningCity == null || b.owningCity == null) return; + if (a.owningCity.owner != b.owningCity.owner) return; + if (!ResolveTileOwnershipConflict(a.owningCity, b.owningCity, t, out City winnerCity)) return; + + // Law II + if (t.baseTerrainType.Key == "ocean" && t.rankDistanceTo(winnerCity.location) > 2) { + t.owningCity = null; + return; } + t.owningCity = winnerCity; + winnerCity.owner.tileKnowledge.AddTilesToKnown(t); } public void UpdateTileOwnersOnCityDestruction(City city) { @@ -255,27 +278,48 @@ public void InvalidateCachedTradeNetwork() { } // Rules taken from https://forums.civfanatics.com/threads/the-eight-laws-of-border-dynamics.106882/ - private City ResolveTileOwnershipConflict(City a, City b, Tile t) { + private bool ResolveTileOwnershipConflict(City a, City b, Tile t, out City owner) { + owner = null; + if (a.Equals(b)) { owner = a; return true; } + int aRank = a.location.rankDistanceTo(t); int bRank = b.location.rankDistanceTo(t); + // Law I + // Cities can claim tiles of rank n+1, where n is the city's expansion level + if (a.GetBorderExpansionLevel() + 1 < aRank && b.GetBorderExpansionLevel() + 1 >= bRank) { owner = b; return true; } + if (b.GetBorderExpansionLevel() + 1 < bRank && a.GetBorderExpansionLevel() + 1 >= aRank) { owner = a; return true; } + + // Law III // The city with the lowest rank claim gets the tile. - if (aRank > bRank) { - return b; - } else if (aRank < bRank) { - return a; - } + if (aRank > bRank) { owner = b; return true; } + if (aRank < bRank) { owner = a; return true; } + // Law IV // If the ranks are equal, the city with more culture gets the tile. - if (a.GetCulture() < b.GetCulture()) { - return b; - } else if (a.GetCulture() > b.GetCulture()) { - return a; - } + if (a.GetCulture() + a.GetCulturePerTurn() < b.GetCulture() + b.GetCulturePerTurn()) { owner = b; return true; } + if (a.GetCulture() + a.GetCulturePerTurn() > b.GetCulture() + b.GetCulturePerTurn()) { owner = a; return true; } + // Law V // If the cultures are equal the oldest city gets the tile. - // TODO: track city age - for now we just return the first. - return a; + // TODO: track city age - for now we are going to skip this. + // return a; + + // Law VI + // Starting North of the disputed tile, we go counter-clockwise + // trying to find the first tile that has one of the competing cities. + // We start at (rank - 1) because the rank distance does not necessarily reflect the actual "ring" + // the city tile is in, so a tile at rank 3, could well mean it's in the 2nd ring. + for (int r = aRank - 1; r <= aRank; r++) { + if (r <= 0) continue; + Tile winner = t.FindInRing(r, tile => tile.HasCity && (tile.cityAtTile == a || tile.cityAtTile == b), false); + if (winner == null) continue; + owner = winner.owningCity; + return true; + } + + // should never happen, if it does some part of the algorithm has gone wrong + throw new Exception($"Failed to resolve ownership of {t} between {a.name} and {b.name}, something went wrong"); } } } diff --git a/C7Engine/C7GameData/Tile.cs b/C7Engine/C7GameData/Tile.cs index a7faa166..f6c9bca7 100644 --- a/C7Engine/C7GameData/Tile.cs +++ b/C7Engine/C7GameData/Tile.cs @@ -186,9 +186,13 @@ public bool NeighborsOcean() { /// This is used by some graphics algorithms. /// /// - public Tile[] getEdgeNeighbors() { - Tile[] edgeNeighbors = { neighbors[TileDirection.NORTHEAST], neighbors[TileDirection.NORTHWEST], neighbors[TileDirection.SOUTHEAST], neighbors[TileDirection.SOUTHWEST]}; - return edgeNeighbors; + public Tile[] GetEdgeNeighbors() { + List edgeNeighbors = new(); + if (neighbors.TryGetValue(TileDirection.NORTHEAST, out Tile ne)) edgeNeighbors.Add(ne); + if (neighbors.TryGetValue(TileDirection.NORTHWEST, out Tile nw)) edgeNeighbors.Add(nw); + if (neighbors.TryGetValue(TileDirection.SOUTHEAST, out Tile se)) edgeNeighbors.Add(se); + if (neighbors.TryGetValue(TileDirection.SOUTHWEST, out Tile sw)) edgeNeighbors.Add(sw); + return edgeNeighbors.ToArray(); } public override string ToString() { @@ -612,6 +616,66 @@ public Tile GetTileAtNeighborIndex(int neighborIndex) { return map.tileAt(XCoordinate + xDelta, YCoordinate + yDelta); } + + /// + /// + /// Walks clockwise/counter-clockwise the nth ring around + /// the specified tile starting on the northmost tile + /// and tries to find the first tile that matches our boolean criterion. + /// + /// + /// This differs from , + /// because it includes all the tiles regardless of the distance. + /// An example would be that GetTilesWithinRankDistance() with a rank of 2 + /// will not return a NN, SS, WW, or EE tile, whereas this method will. + /// + /// + /// It is mostly used to calculate to whom we should assign tiles + /// that are being claimed by more than 1 city or civilization. + /// + /// + /// + /// + /// + /// + public Tile FindInRing(int rank, Func predicate, bool clockwise = true) { + int x = this.XCoordinate; + int y = this.YCoordinate - (2 * rank); + + Tile currentTile = map.tileAt(x, y); + if (currentTile != Tile.NONE && predicate(currentTile)) return currentTile; + + // Going SW(counter-clockwise) or SE(clockwise) + for (int _ = 1; _ < (2 * rank) + 1; _++) { + if (clockwise) { x++; y++; } else { x--; y++; } + currentTile = map.tileAt(x, y); + if (currentTile == Tile.NONE || !predicate(currentTile)) continue; + return currentTile; + } + // Going SE(counter-clockwise) or SW(clockwise) + for (int _ = 1; _ < (2 * rank) + 1; _++) { + if (clockwise) { x--; y++; } else { x++; y++; } + currentTile = map.tileAt(x, y); + if (currentTile == Tile.NONE || !predicate(currentTile)) continue; + return currentTile; + } + // Going NE(counter-clockwise) or NW(clockwise) + for (int _ = 1; _ < (2 * rank) + 1; _++) { + if (clockwise) { x--; y--; } else { x++; y--; } + currentTile = map.tileAt(x, y); + if (currentTile == Tile.NONE || !predicate(currentTile)) continue; + return currentTile; + } + // Going NW(counter-clockwise) or NE(clockwise) + for (int _ = 1; _ < (2 * rank); _++) { + if (clockwise) { x++; y--; } else { x--; y--; } + currentTile = map.tileAt(x, y); + if (currentTile == Tile.NONE || !predicate(currentTile)) continue; + return currentTile; + } + return null; + } + // Returns the tiles in the spiral ordering defined by // GetTileAtNeighborIndex(i). public List GetTilesWithinRankDistance(int rank) {