Skip to content

Commit 89fe5d0

Browse files
authored
Merge pull request #749 from C7-Game/twrner/spiral-function
Define a C7 spiral function and use it for a "neighbors of rank X" function
2 parents fbf8ac0 + c88ee06 commit 89fe5d0

File tree

5 files changed

+101
-66
lines changed

5 files changed

+101
-66
lines changed

C7/Map/TileAssignmentLayer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Linq;
23
using System.Collections.Generic;
34
using C7GameData;
45
using Godot;
@@ -21,7 +22,7 @@ public override void onBeginDraw(LooseView looseView, GameData gameData) {
2122
if (city == null) {
2223
return;
2324
}
24-
workableTiles = city.GetWorkableTiles();
25+
workableTiles = city.GetWorkableTiles().ToHashSet();
2526

2627
// Include the city center in the "workable" tiles to avoid having
2728
// a border drawn there.

C7Engine/AI/ChooseProducible.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ private static float ScoreBuilding(ProducibleStats stats, City city, Player play
248248
if (building.culturePerTurn > 0) {
249249
int unclaimedTilesInOuterRing = 0;
250250
int enemyTilesInOuterRing = 0;
251-
foreach (Tile t in city.GetTilesOfRank(2)) {
251+
foreach (Tile t in city.location.GetTilesWithinRankDistance(2)) {
252252
if (t.OwningPlayer() == null) {
253253
++unclaimedTilesInOuterRing;
254254
} else if (t.OwningPlayer() != city.owner) {

C7Engine/C7GameData/City.cs

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -644,8 +644,8 @@ public void AddUnit(UnitPrototype prototype, GameData gameData) {
644644
//
645645
// TODO: we should make this configurable to allow for the bigger cross
646646
// if a mod wants it.
647-
public HashSet<Tile> GetWorkableTiles() {
648-
HashSet<Tile> result = new();
647+
public List<Tile> GetWorkableTiles() {
648+
List<Tile> result = new();
649649
foreach (Tile t in GetTilesOfRank(2)) {
650650
// Skip tiles not owned by this player.
651651
if (t.owningCity == null || t.owningCity.owner != this.owner) {
@@ -664,41 +664,21 @@ public HashSet<Tile> GetWorkableTiles() {
664664

665665
// The list of tiles that are within the borders of this city, without
666666
// taking into account border collisions with other cities.
667-
public HashSet<Tile> GetTilesWithinBorders() {
667+
public List<Tile> GetTilesWithinBorders() {
668668
return GetTilesOfRank(GetBorderExpansionLevel());
669669
}
670670

671-
public HashSet<Tile> GetTilesOfRank(int rank) {
672-
HashSet<Tile> result = new();
673-
HashSet<Tile> knownTiles = new();
674-
675-
Stack<Tile> toCheck = new();
676-
toCheck.Push(location);
677-
knownTiles.Add(location);
678-
679-
while (toCheck.Count > 0) {
680-
Tile t = toCheck.Pop();
681-
682-
// Skip tiles that are too far away.
683-
if (location.rankDistanceTo(t) > rank) {
684-
continue;
685-
}
686-
671+
// Like GetTilesWithinRankDistance, but with the filtering of ocean
672+
// tiles for city border calculations.
673+
private List<Tile> GetTilesOfRank(int rank) {
674+
List<Tile> result = new();
675+
foreach (Tile t in location.GetTilesWithinRankDistance(rank)) {
687676
// Ocean tiles may only hold claims of rank 2.
688677
if (t.baseTerrainTypeKey == "ocean" && rank > 2) {
689678
continue;
690679
}
691-
692-
// Otherwise this tile is close enough. Check its neighbors next.
693680
result.Add(t);
694-
foreach (Tile neighbor in t.neighbors.Values) {
695-
if (!knownTiles.Contains(neighbor)) {
696-
toCheck.Push(neighbor);
697-
knownTiles.Add(t);
698-
}
699-
}
700681
}
701-
702682
return result;
703683
}
704684

C7Engine/C7GameData/Tile.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,89 @@ public static TileLocation NeighborCoordinate(TileLocation location, TileDirecti
512512
}
513513
return location;
514514
}
515+
516+
// Returns the tile at a "neighbor index", where 0 is this tile, 1 is
517+
// due north, 2 is NE, 3 is E, and so on in a clockwise spiral.
518+
// Index 9 is N+N, 10 is N+NE, etc.
519+
//
520+
// This is slightly different than the civ3 spiral, which starts with
521+
// the NE and goes clockwise. Ring 2 of the civ3 spiral is the BFC tiles,
522+
// but rings beyond that get stranger. We don't need to match the civ3
523+
// spiral exactly, and this is much simpler to understand.
524+
public Tile GetTileAtNeighborIndex(int neighborIndex) {
525+
// Special case: Index 0 is this tile.
526+
if (neighborIndex <= 0) {
527+
return this;
528+
}
529+
530+
int xDelta = 0;
531+
int yDelta = 0;
532+
533+
// Figure out which ring we're in.
534+
int ringNumber = 0;
535+
do {
536+
ringNumber++;
537+
} while (Math.Pow(2 * ringNumber + 1, 2) <= neighborIndex);
538+
539+
// Figure out how many tiles are in the previous ring.
540+
// For ring 2, we get (2*2 - 1)^2, which is 9.
541+
int cellsInInnerRings = (ringNumber * 2 - 1) * (ringNumber * 2 - 1);
542+
543+
// Figure out the index of this neighbor within our ring.
544+
int indexInRing1Based = neighborIndex - cellsInInnerRings;
545+
546+
// Our ring is a square with 4 sides, and each side has
547+
// (ringNumber*2 + 1) tiles in it. But then we have the overlap of
548+
// each corner, so excluding the overlap we have ringNumber*2 tiles
549+
// per side.
550+
//
551+
// For ring 1, the 4 sections of size 2 are
552+
// (N, NE), (E, SE), (S, SW), (W, NW)
553+
int cellsPerSquareEdge = ringNumber * 2;
554+
555+
// Define segment boundaries based on 1-based index within the ring
556+
int segment1End = cellsPerSquareEdge;
557+
int segment2End = 2 * cellsPerSquareEdge;
558+
int segment3End = 3 * cellsPerSquareEdge;
559+
int segment4End = 4 * cellsPerSquareEdge;
560+
561+
if (indexInRing1Based <= segment1End) {
562+
// This is the side that goes from N to 1 short of E.
563+
// N and NE for ring 1.
564+
xDelta = indexInRing1Based;
565+
yDelta = indexInRing1Based - cellsPerSquareEdge;
566+
} else if (indexInRing1Based <= segment2End) {
567+
// This is the side that goes from E to 1 short of S.
568+
// E and SE for ring 1.
569+
xDelta = segment2End - indexInRing1Based;
570+
yDelta = indexInRing1Based - cellsPerSquareEdge;
571+
} else if (indexInRing1Based <= segment3End) {
572+
// This is the side that goes from S to 1 short of W.
573+
// S and SW for ring 1.
574+
xDelta = segment2End - indexInRing1Based;
575+
yDelta = segment3End - indexInRing1Based;
576+
} else {
577+
// This is the side that goes from W to 1 short of N.
578+
// W and NW for ring 1.
579+
xDelta = indexInRing1Based - segment4End;
580+
yDelta = segment3End - indexInRing1Based;
581+
}
582+
583+
return map.tileAt(XCoordinate + xDelta, YCoordinate + yDelta);
584+
}
585+
586+
public List<Tile> GetTilesWithinRankDistance(int rank) {
587+
List<Tile> result = new();
588+
for (int i = 0; i < (rank * 2 + 1) * (rank * 2 + 1); ++i) {
589+
Tile t = GetTileAtNeighborIndex(i);
590+
if (rankDistanceTo(t) <= rank) {
591+
result.Add(t);
592+
}
593+
}
594+
595+
return result;
596+
}
597+
515598
public MapUnit FindTopDefender(MapUnit opponent) {
516599
if (unitsOnTile.Count > 0) {
517600
IEnumerable<MapUnit> potentialDefenders = unitsOnTile.Where(u => u.CanDefendAgainst(opponent));

C7Engine/MapGenerator.cs

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,7 +1101,7 @@ private static bool IsValidForLuxuryPlacement(GameMap m, Resource r, Tile t) {
11011101
minLuxurySpacing = Math.Max(2, minLuxurySpacing);
11021102
minLuxurySpacing = Math.Min(minLuxurySpacing, 10);
11031103

1104-
foreach (Tile x in GetTilesWithinRankDistance(t, minLuxurySpacing)) {
1104+
foreach (Tile x in t.GetTilesWithinRankDistance(minLuxurySpacing)) {
11051105
if (x.Resource != null
11061106
&& x.Resource != Resource.NONE
11071107
&& x.Resource.Category == ResourceCategory.LUXURY
@@ -1122,7 +1122,7 @@ private static bool IsValidForLuxuryPlacement(GameMap m, Resource r, Tile t) {
11221122
private static bool HasSufficientLandNeighborsForResource(Tile t) {
11231123
int landTiles = 0;
11241124

1125-
foreach (Tile x in GetTilesWithinRankDistance(t, 2)) {
1125+
foreach (Tile x in t.GetTilesWithinRankDistance(2)) {
11261126
if (x.IsLand()) {
11271127
++landTiles;
11281128
}
@@ -1131,35 +1131,6 @@ private static bool HasSufficientLandNeighborsForResource(Tile t) {
11311131
return landTiles >= 1;
11321132
}
11331133

1134-
private static HashSet<Tile> GetTilesWithinRankDistance(Tile location, int rank) {
1135-
HashSet<Tile> result = new();
1136-
HashSet<Tile> knownTiles = new();
1137-
1138-
Stack<Tile> toCheck = new();
1139-
toCheck.Push(location);
1140-
knownTiles.Add(location);
1141-
1142-
while (toCheck.Count > 0) {
1143-
Tile t = toCheck.Pop();
1144-
1145-
// Skip tiles that are too far away.
1146-
if (location.rankDistanceTo(t) > rank) {
1147-
continue;
1148-
}
1149-
1150-
// Otherwise this tile is close enough. Check its neighbors next.
1151-
result.Add(t);
1152-
foreach (Tile neighbor in t.neighbors.Values) {
1153-
if (!knownTiles.Contains(neighbor)) {
1154-
toCheck.Push(neighbor);
1155-
knownTiles.Add(t);
1156-
}
1157-
}
1158-
}
1159-
1160-
return result;
1161-
}
1162-
11631134
private static void PlaceStrategicResourceType(Random rand, WorldCharacteristics wc, GameMap m, Resource r, List<int> tileIndicies) {
11641135
int targetCount = GetAppearance(wc, rand, r, minCount:2);
11651136
int placed = 0;
@@ -1207,7 +1178,7 @@ private static bool IsValidForStrategicResourcePlacement(GameMap m, Resource r,
12071178
minSpacing = Math.Min(minSpacing, 10);
12081179

12091180
// Ensure strategic resources of the same kind don't clump up.
1210-
foreach (Tile x in GetTilesWithinRankDistance(t, minSpacing)) {
1181+
foreach (Tile x in t.GetTilesWithinRankDistance(minSpacing)) {
12111182
if (x.Resource != null
12121183
&& x.Resource != Resource.NONE
12131184
&& x.Resource.Category == ResourceCategory.STRATEGIC
@@ -1359,7 +1330,7 @@ private static bool IsValidForBarbarianCamp(GameMap m, Tile t) {
13591330
}
13601331

13611332
// No barbarian camps within the big fat cross of another camp.
1362-
foreach (Tile n in GetTilesWithinRankDistance(t, 2)) {
1333+
foreach (Tile n in t.GetTilesWithinRankDistance(2)) {
13631334
if (n.hasBarbarianCamp) {
13641335
return false;
13651336
}
@@ -1502,7 +1473,7 @@ private static int ScorePossibleCityLocation(WorldCharacteristics wc, Tile t) {
15021473

15031474
// Calculate the score for tiles in the immediate area.
15041475
int score = 0;
1505-
foreach (Tile n in GetTilesWithinRankDistance(t, 1)) {
1476+
foreach (Tile n in t.GetTilesWithinRankDistance(1)) {
15061477
score += CommercePoints * n.commerceYield(player).yield;
15071478
score += ShieldPoints * n.productionYield(player).yield;
15081479
score += FoodPoints * n.foodYield(player).yield;
@@ -1517,7 +1488,7 @@ private static int ScorePossibleCityLocation(WorldCharacteristics wc, Tile t) {
15171488

15181489
// Then do it again for the full big fat cross, effectively weighting
15191490
// the immediate neighbors at a 2x rate.
1520-
foreach (Tile n in GetTilesWithinRankDistance(t, 2)) {
1491+
foreach (Tile n in t.GetTilesWithinRankDistance(2)) {
15211492
score += CommercePoints * n.commerceYield(player).yield;
15221493
score += ShieldPoints * n.productionYield(player).yield;
15231494
score += FoodPoints * n.foodYield(player).yield;
@@ -1535,7 +1506,7 @@ private static int ScorePossibleCityLocation(WorldCharacteristics wc, Tile t) {
15351506
// Try to get a rough sense of the number of surrounding land tiles
15361507
// on the same continent, to avoid players getting stuck on a
15371508
// peninsula.
1538-
foreach (Tile n in GetTilesWithinRankDistance(t, 4)) {
1509+
foreach (Tile n in t.GetTilesWithinRankDistance(4)) {
15391510
if (n.continent == t.continent) {
15401511
score += LandTilePoints;
15411512
}

0 commit comments

Comments
 (0)