Precompute land regions for water crossing (#8777)

This commit is contained in:
2026-07-24 11:04:43 -07:00
committed by GitHub
parent c7d5f36ad5
commit af1e467cb8
19 changed files with 431 additions and 24 deletions
@@ -40,7 +40,8 @@ auto AttackLocations::LocationsWithEnemyInRange(const Unit *unit) const -> Coord
AttackLocationsCache::AttackLocationsCache(const HexMap *hexMap, const SettingsGetter &settings)
: rowCount(hexMap->row_count()),
columnCount(hexMap->column_count()) {
columnCount(hexMap->column_count()),
landRegions(hexMap) {
const int mageRange =
std::max(settings.Backing().meteor_range(), settings.Backing().lightning_range());
const auto fearRange = settings.Backing().fear_range();
@@ -11,6 +11,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
@@ -151,6 +152,8 @@ struct alignas(128) AttackLocationsCache {
private:
const MapIndex rowCount;
const MapIndex columnCount;
// Immutable static-map analysis shared by every speculative state evaluated by this AI.
const LandRegions landRegions;
// For each of these vectors, the index is the map index of the tile *that can be attacked*,
// and the entry is the CoordsSet of the tiles that can attack that location.
@@ -161,6 +164,7 @@ private:
public:
AttackLocationsCache(const HexMap* hexMap, const SettingsGetter& settings);
[[nodiscard]] auto GetLandRegions() const -> const LandRegions& { return landRegions; }
auto CachedLocations(const Coords& singleTarget, bool lateGame) -> const AttackLocations&;
auto CachedLocations(const CoordsSet& targets, bool lateGame) -> AttackLocations;
auto CachedLocations(const std::vector<const Unit*>& enemies, bool lateGame) -> AttackLocations;
@@ -23,6 +23,7 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
const CoordsSet& criticalTileCoords,
int maxRounds,
const APDCache& apdCache,
const LandRegions& landRegions,
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy {
uint32_t attackerNonUndeadUnitCount = 0;
uint32_t attackerNonUndeadUnitNotRequiringWaterCrossingCount = 0;
@@ -39,6 +40,7 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
gameState,
player->player_id(),
criticalTileCoords,
landRegions,
apdCache,
battalionTypeGetter);
attackerUnitIdsRequiringWaterCrossing.insert(
@@ -22,6 +22,7 @@ public:
const CoordsSet& criticalTileCoords,
int maxRounds,
const APDCache& apdCache,
const LandRegions& landRegions,
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy;
};
} // namespace shardok
@@ -10,6 +10,7 @@
#include "AIMinimumDistanceAndTarget.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
@@ -36,6 +37,15 @@ auto CanReachWithDistances(
return distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE;
}
auto DefinitelyReachesAllWithoutWater(
const LandRegions &landRegions,
const Coords &origin,
const CoordsSet &destinations) -> bool {
return std::ranges::all_of(destinations, [&](const Coords &destination) {
return landRegions.DefinitelyConnectedWithoutWater(origin, destination);
});
}
auto CanCreateWaterCrossing(
const net::eagle0::shardok::storage::fb::Unit *unit,
const BattalionTypeGetter &battalionTypeGetter) -> bool {
@@ -55,8 +65,41 @@ auto UnitIdsRequiringWaterCrossing(
const GameStateW &gameState,
const PlayerId pid,
const CoordsSet &destinations,
const LandRegions &landRegions,
const APDCache &apdCache,
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
std::vector<const net::eagle0::shardok::storage::fb::Unit *> unitsToCheck{};
std::array<int8_t, 9> startingPositionIndices{};
startingPositionIndices.fill(-1);
for (const auto *unit : *gameState->units()) {
if (unit->player_id() != pid) continue;
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
if (!DefinitelyReachesAllWithoutWater(landRegions, unit->location(), destinations)) {
unitsToCheck.push_back(unit);
}
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
if (startingPositionIndices[unit->starting_position_index()] == -1) {
startingPositionIndices[unit->starting_position_index()] = 0;
const auto *startingPositions = gameState->hex_map()
->attacker_starting_positions()
->Get(unit->starting_position_index())
->positions();
for (const Coords *position : *startingPositions) {
if (!DefinitelyReachesAllWithoutWater(landRegions, *position, destinations)) {
startingPositionIndices[unit->starting_position_index()] = 1;
break;
}
}
}
if (startingPositionIndices[unit->starting_position_index()] == 1) {
unitsToCheck.push_back(unit);
}
}
}
if (unitsToCheck.empty()) return {};
const HexMap *map = gameState->hex_map();
MapId mapId = ActionPointDistancesCache::GetMapId(map);
@@ -90,11 +133,10 @@ auto UnitIdsRequiringWaterCrossing(
vector<UnitId> unitIdsToReturn{};
// Set this to -1 for unchecked, 0 for does not need crossing, 1 for does need crossing
std::vector<int> startingPositionIndices(9, -1);
for (const auto *unit : *gameState->units()) {
if (unit->player_id() != pid) continue;
// The static region pass above removed every unit that is definitely able to reach every
// destination without water. Resolve full APDs only for the inconclusive units.
startingPositionIndices.fill(-1);
for (const auto *unit : unitsToCheck) {
const auto &battType = battalionTypeGetter(unit->battalion().type());
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
@@ -319,6 +361,7 @@ auto WaterCrossingScore(
const GameStateW &gameState,
const CoordsSet &castleCoords,
const CoordsSet &startCrossingFrom,
const LandRegions &landRegions,
const APDCache &apdCache) -> double {
uint32_t castleClaimCount = 0;
for (const auto *unit : *gameState->units()) {
@@ -348,6 +391,7 @@ auto WaterCrossingScore(
gameState,
playerId,
castleCoords,
landRegions,
apdCache,
battalionTypeGetter);
if (unitIdsRequiringCrossing.empty()) return kNoRequiredCrossingScore;
@@ -9,6 +9,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
@@ -35,6 +36,7 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
const GameStateW& gameState,
PlayerId pid,
const CoordsSet& destinations,
const LandRegions& landRegions,
const APDCache& apdCache,
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
@@ -92,6 +94,7 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
const GameStateW& gameState,
const CoordsSet& castleCoords,
const CoordsSet& startCrossingFrom,
const LandRegions& landRegions,
const APDCache& apdCache) -> double;
} // namespace shardok
@@ -44,6 +44,7 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
gameState,
playerId,
castleCoords,
landRegions,
apdCache,
battalionTypeGetter);
if (unitIdsRequiringCrossing.empty()) return startCrossingFrom;
@@ -11,6 +11,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
@@ -23,11 +24,16 @@ class AIWaterCrossingCommandChooser {
private:
const PlayerId playerId;
const APDCache apdCache;
const LandRegions &landRegions;
public:
AIWaterCrossingCommandChooser(const PlayerId pid, APDCache apdCache)
AIWaterCrossingCommandChooser(
const PlayerId pid,
APDCache apdCache,
const LandRegions &landRegions)
: playerId(pid),
apdCache(std::move(apdCache)) {}
apdCache(std::move(apdCache)),
landRegions(landRegions) {}
[[nodiscard]] auto StartCrossingFrom(
const BattalionTypeGetter &battalionTypeGetter,
@@ -73,6 +73,7 @@ cc_library(
],
deps = [
":ai_score_utilities",
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
"//src/main/cpp/net/eagle0/shardok/library/map:terrain",
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
@@ -317,6 +318,7 @@ cc_library(
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:hex_map_helpers",
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/flatbuffer/net/eagle0/shardok/storage:hex_map_cc_fbs",
],
@@ -334,6 +336,7 @@ cc_library(
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
],
)
@@ -72,7 +72,7 @@ ShardokAIClient::ShardokAIClient(
aiAlgorithmType(aiAlgorithmType),
scoringCalculatorType(scoringCalculatorType),
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
waterCrossingCommandChooser(playerId, apdCache),
waterCrossingCommandChooser(playerId, apdCache, alCache->GetLandRegions()),
mctsConfig(mctsConfig) {
// Pre-generate the most common cache entries for better performance
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
@@ -302,6 +302,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
castleCoords,
maxRounds,
apdCache,
alCache->GetLandRegions(),
battalionTypeGetter)
: AIAttackerStrategySelector::BestAttackerStrategy(
playerId,
@@ -309,6 +309,7 @@ auto AbstractAIScoreCalculator::CalculateAttackerVictoryConditionScore(
gameState,
castleCoords,
attackerStrategy.targetLocations,
GetAlCache()->GetLandRegions(),
GetApdCache());
break;
@@ -59,6 +59,20 @@ cc_library(
],
)
cc_library(
name = "land_regions",
srcs = ["LandRegions.cpp"],
hdrs = ["LandRegions.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
deps = [
":coordinates",
":terrain",
"//src/main/flatbuffer/net/eagle0/shardok/storage:hex_map_cc_fbs",
"//src/main/flatbuffer/net/eagle0/shardok/storage:terrain_cc_fbs",
],
)
cc_library(
name = "terrain",
srcs = ["Terrain.cpp"],
@@ -0,0 +1,132 @@
//
// Copyright 2026 Dan Crosby
//
#include "LandRegions.hpp"
#include <array>
#include <limits>
#include <stdexcept>
#include "src/main/cpp/net/eagle0/shardok/library/map/Terrain.hpp"
namespace shardok {
namespace {
using TerrainType = net::eagle0::shardok::storage::fb::Terrain_::Type;
constexpr std::array<std::array<int, 2>, 6> kEvenRowNeighborOffsets = {
std::array{-1, 0},
std::array{0, 1},
std::array{1, 0},
std::array{1, -1},
std::array{0, -1},
std::array{-1, -1}};
constexpr std::array<std::array<int, 2>, 6> kOddRowNeighborOffsets = {
std::array{-1, 1},
std::array{0, 1},
std::array{1, 1},
std::array{1, 0},
std::array{0, -1},
std::array{-1, 0}};
auto IsGuaranteedLand(const TerrainType type) -> bool {
return !IsWater(type) && type != TerrainType::Type_MOUNTAIN;
}
auto ToIndex(const int row, const int column, const int columnCount) -> int {
return row * columnCount + column;
}
} // namespace
auto ComputeLandRegions(
const int rowCount,
const int columnCount,
const std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
-> std::vector<LandRegionId> {
const int tileCount = rowCount * columnCount;
if (tileCount < 0 || terrain.size() != static_cast<size_t>(tileCount)) {
throw std::invalid_argument("Land region terrain size does not match map dimensions");
}
std::vector<LandRegionId> regions(static_cast<size_t>(tileCount), kNoLandRegion);
std::vector<int> pending;
pending.reserve(static_cast<size_t>(tileCount));
int nextRegion = 0;
for (int startIndex = 0; startIndex < tileCount; ++startIndex) {
if (regions[startIndex] != kNoLandRegion || !IsGuaranteedLand(terrain[startIndex].type())) {
continue;
}
if (nextRegion > std::numeric_limits<LandRegionId>::max()) {
throw std::overflow_error("Map has too many distinct land regions");
}
regions[startIndex] = static_cast<LandRegionId>(nextRegion);
pending.push_back(startIndex);
while (!pending.empty()) {
const int index = pending.back();
pending.pop_back();
const int row = index / columnCount;
const int column = index % columnCount;
const auto& offsets = row % 2 == 0 ? kEvenRowNeighborOffsets : kOddRowNeighborOffsets;
for (const auto& [rowOffset, columnOffset] : offsets) {
const int neighborRow = row + rowOffset;
const int neighborColumn = column + columnOffset;
if (neighborRow < 0 || neighborRow >= rowCount || neighborColumn < 0 ||
neighborColumn >= columnCount) {
continue;
}
const int neighborIndex = ToIndex(neighborRow, neighborColumn, columnCount);
if (regions[neighborIndex] != kNoLandRegion ||
!IsGuaranteedLand(terrain[neighborIndex].type())) {
continue;
}
regions[neighborIndex] = static_cast<LandRegionId>(nextRegion);
pending.push_back(neighborIndex);
}
}
++nextRegion;
}
return regions;
}
LandRegions::LandRegions(const net::eagle0::shardok::storage::fb::HexMap* map)
: rowCount(map->row_count()),
columnCount(map->column_count()) {
std::vector<net::eagle0::shardok::storage::fb::Terrain> terrain;
terrain.reserve(map->terrain()->size());
for (const auto* tile : *map->terrain()) { terrain.push_back(*tile); }
regions = ComputeLandRegions(rowCount, columnCount, terrain);
}
LandRegions::LandRegions(
const int rowCount,
const int columnCount,
const std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
: rowCount(rowCount),
columnCount(columnCount),
regions(ComputeLandRegions(rowCount, columnCount, terrain)) {}
auto LandRegions::RegionAt(const Coords& coords) const -> LandRegionId {
if (coords.row() < 0 || coords.column() < 0 || coords.row() >= rowCount ||
coords.column() >= columnCount) {
return kNoLandRegion;
}
return regions[ToIndex(coords.row(), coords.column(), columnCount)];
}
auto LandRegions::DefinitelyConnectedWithoutWater(const Coords& origin, const Coords& destination)
const -> bool {
const auto originRegion = RegionAt(origin);
const auto destinationRegion = RegionAt(destination);
return originRegion != kNoLandRegion && originRegion == destinationRegion;
}
} // namespace shardok
@@ -0,0 +1,55 @@
//
// Copyright 2026 Dan Crosby
//
#ifndef EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
#define EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
#include <cstdint>
#include <span>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/terrain.hpp"
namespace shardok {
using LandRegionId = int8_t;
constexpr LandRegionId kNoLandRegion = -1;
[[nodiscard]] auto ComputeLandRegions(
int rowCount,
int columnCount,
std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
-> std::vector<LandRegionId>;
class LandRegions {
private:
int rowCount;
int columnCount;
std::vector<LandRegionId> regions;
public:
explicit LandRegions(const net::eagle0::shardok::storage::fb::HexMap* map);
LandRegions(
int rowCount,
int columnCount,
std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain);
// A true result is conclusive: the two locations have a static path that uses neither water nor
// mountains. False is inconclusive because dynamic bridges, ice, or unit capabilities may
// connect different static regions.
[[nodiscard]] auto DefinitelyConnectedWithoutWater(
const Coords& origin,
const Coords& destination) const -> bool;
[[nodiscard]] auto RegionAt(const Coords& coords) const -> LandRegionId;
[[nodiscard]] auto size() const -> size_t { return regions.size(); }
};
} // namespace shardok
#endif // EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
@@ -59,7 +59,10 @@ class AIAttackerStrategySelectorTest : public ::testing::Test {
apdCache = std::make_shared<ActionPointDistancesCache>();
alCache = std::make_unique<AttackLocationsCache>(hexMap, GetGameSettingsGetter());
waterCrossingCommandChooser = std::make_unique<AIWaterCrossingCommandChooser>(0, apdCache);
waterCrossingCommandChooser = std::make_unique<AIWaterCrossingCommandChooser>(
0,
apdCache,
alCache->GetLandRegions());
}
protected:
@@ -92,6 +92,7 @@ TEST_F(AIStrategySelectorTest, NotEnoughAttackerUnits_returnsScatterStrategy) {
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
alCache->GetLandRegions(),
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
@@ -117,6 +118,7 @@ TEST_F(AIStrategySelectorTest, EnoughAttackerUnitsOnlyIncludingUndead_returnsSca
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
alCache->GetLandRegions(),
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
@@ -142,6 +144,7 @@ TEST_F(AIStrategySelectorTest, EnoughAttackerUnits_returnsHoldCastlesStrategy) {
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
alCache->GetLandRegions(),
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
@@ -171,6 +174,7 @@ TEST_F(AIStrategySelectorTest,
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
alCache->GetLandRegions(),
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
@@ -199,6 +203,7 @@ TEST_F(AIStrategySelectorTest,
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
alCache->GetLandRegions(),
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
@@ -228,6 +233,7 @@ TEST_F(AIStrategySelectorTest,
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
alCache->GetLandRegions(),
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
@@ -257,6 +263,7 @@ TEST_F(AIStrategySelectorTest,
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
alCache->GetLandRegions(),
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
@@ -287,6 +294,7 @@ TEST_F(AIStrategySelectorTest,
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
alCache->GetLandRegions(),
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
@@ -33,7 +33,21 @@ class AIWaterCrossingCalculatorTest : public ::testing::Test {
const auto unitsOffset = fbb.CreateVectorOfSortedStructs(&unitsVec);
const auto hexMapOffset = AddHexMap(fbb, BASIC_MAP_FB);
auto terrainTypes = TERRAIN_VALUES_FB;
terrainTypes[0 * 6 + 3] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
terrainTypes[1 * 6 + 3] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
terrainTypes[2 * 6 + 3] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
terrainTypes[2 * 6 + 4] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
terrainTypes[2 * 6 + 5] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
terrainTypes[3 * 6 + 5] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
const auto hexMapOffset = AddHexMap(
fbb,
6,
6,
terrainTypes,
EMPTY_STARTING_POSITIONS,
EMPTY_POSITIONS_LIST,
SUNNY_MONTHLY_WEATHERS_FB);
net::eagle0::shardok::storage::fb::GameStateBuilder gsb(fbb);
gsb.add_units(unitsOffset);
@@ -55,19 +69,6 @@ class AIWaterCrossingCalculatorTest : public ::testing::Test {
// P P P P P P
// * P P P P P
MutableTerrain(hexMap, Coords(0, 3))
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
MutableTerrain(hexMap, Coords(1, 3))
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
MutableTerrain(hexMap, Coords(2, 3))
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
MutableTerrain(hexMap, Coords(2, 4))
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
MutableTerrain(hexMap, Coords(2, 5))
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
MutableTerrain(hexMap, Coords(3, 5))
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
mapId = ActionPointDistancesCache::GetMapId(hexMap);
alCache = std::make_unique<AttackLocationsCache>(hexMap, GetGameSettingsGetter());
@@ -103,10 +104,12 @@ TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_allSameSide_re
gameState,
0,
destinations,
alCache->GetLandRegions(),
apdCache,
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
}));
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_allOtherSide_returnsTwo) {
@@ -123,10 +126,12 @@ TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_allOtherSide_r
gameState,
0,
destinations,
alCache->GetLandRegions(),
apdCache,
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
}));
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_oneOtherSide_returnsOne) {
@@ -143,6 +148,7 @@ TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_oneOtherSide_r
gameState,
0,
destinations,
alCache->GetLandRegions(),
apdCache,
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
@@ -161,6 +167,7 @@ TEST_F(AIWaterCrossingCalculatorTest,
gameState,
0,
destinations,
alCache->GetLandRegions(),
apdCache,
battalionTypeGetter);
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
@@ -174,6 +181,7 @@ TEST_F(AIWaterCrossingCalculatorTest,
gameState,
0,
destinations,
alCache->GetLandRegions(),
apdCache,
battalionTypeGetter);
EXPECT_EQ(beforeFire, afterFire);
@@ -181,6 +189,52 @@ TEST_F(AIWaterCrossingCalculatorTest,
EXPECT_TRUE(fireTerrain->modifier().fire().present());
}
TEST_F(AIWaterCrossingCalculatorTest,
UnitsRequiringWaterCrossing_sameRegionWithFire_skipsDistances) {
unit0->mutable_location() = Coords(0, 4);
unit1->mutable_location() = Coords(0, 5);
MutableTerrain(hexMap, Coords(0, 4))->mutable_modifier().mutable_fire().mutate_present(true);
CoordsSet destinations(hexMap);
destinations.Add(acrossDestination);
EXPECT_TRUE(UnitIdsRequiringWaterCrossing(
gameState,
0,
destinations,
alCache->GetLandRegions(),
apdCache,
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
.empty());
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest,
UnitsRequiringWaterCrossing_bridgeBetweenRegions_fallsBackToDistances) {
auto& bridgeModifier = MutableTerrain(hexMap, Coords(1, 3))->mutable_modifier();
bridgeModifier.mutable_bridge().mutate_present(true);
bridgeModifier.mutable_bridge().mutate_integrity(100.0);
hexMap->mutate_modifier_hash(0);
hexMap->mutate_modifier_hash(ActionPointDistancesCache::GetMapId(hexMap).modifierId);
CoordsSet destinations(hexMap);
destinations.Add(acrossDestination);
EXPECT_TRUE(UnitIdsRequiringWaterCrossing(
gameState,
0,
destinations,
alCache->GetLandRegions(),
apdCache,
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
.empty());
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest, CanReach_sameSide_returnsTrue) {
EXPECT_TRUE(CanReach(origin, sameSideDestination, hexMap, mapId, apdCache, heavyCavalry));
}
@@ -15,6 +15,18 @@ cc_test(
],
)
cc_test(
name = "land_regions_test",
srcs = ["LandRegions_test.cpp"],
copts = TEST_COPTS,
linkstatic = True,
deps = [
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
"@googletest//:gtest",
"@googletest//:gtest_main",
],
)
cc_test(
name = "tile_modifier_test",
srcs = ["TileModifier_test.cpp"],
@@ -0,0 +1,62 @@
//
// Copyright 2026 Dan Crosby
//
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
#include <gtest/gtest.h>
#include <vector>
namespace shardok {
namespace {
using Terrain = net::eagle0::shardok::storage::fb::Terrain;
using TerrainType = net::eagle0::shardok::storage::fb::Terrain_::Type;
using TileModifier = net::eagle0::shardok::storage::fb::TileModifier;
auto TerrainGrid(const int size, const TerrainType type = TerrainType::Type_PLAINS)
-> std::vector<Terrain> {
return std::vector<Terrain>(static_cast<size_t>(size), Terrain(type, TileModifier()));
}
TEST(LandRegionsTest, ConnectedLandSharesOneRegion) {
const auto terrain = TerrainGrid(9);
const LandRegions regions(3, 3, terrain);
ASSERT_EQ(9, regions.size());
EXPECT_NE(kNoLandRegion, regions.RegionAt(Coords(0, 0)));
EXPECT_TRUE(regions.DefinitelyConnectedWithoutWater(Coords(0, 0), Coords(2, 2)));
}
TEST(LandRegionsTest, WaterSeparatesRegionsAndHasNoRegion) {
auto terrain = TerrainGrid(9);
terrain[1].mutate_type(TerrainType::Type_RIVER);
terrain[4].mutate_type(TerrainType::Type_RIVER);
terrain[7].mutate_type(TerrainType::Type_RIVER);
const LandRegions regions(3, 3, terrain);
EXPECT_EQ(kNoLandRegion, regions.RegionAt(Coords(0, 1)));
EXPECT_EQ(kNoLandRegion, regions.RegionAt(Coords(1, 1)));
EXPECT_EQ(kNoLandRegion, regions.RegionAt(Coords(2, 1)));
EXPECT_NE(kNoLandRegion, regions.RegionAt(Coords(1, 0)));
EXPECT_NE(kNoLandRegion, regions.RegionAt(Coords(1, 2)));
EXPECT_FALSE(regions.DefinitelyConnectedWithoutWater(Coords(1, 0), Coords(1, 2)));
}
TEST(LandRegionsTest, MountainsAlsoSeparateGuaranteedLandRegions) {
auto terrain = TerrainGrid(9);
terrain[1].mutate_type(TerrainType::Type_MOUNTAIN);
terrain[4].mutate_type(TerrainType::Type_MOUNTAIN);
terrain[7].mutate_type(TerrainType::Type_MOUNTAIN);
const LandRegions regions(3, 3, terrain);
EXPECT_EQ(kNoLandRegion, regions.RegionAt(Coords(1, 1)));
EXPECT_FALSE(regions.DefinitelyConnectedWithoutWater(Coords(1, 0), Coords(1, 2)));
}
} // namespace
} // namespace shardok