Account for bridges in AI land regions (#8785)

This commit is contained in:
2026-07-25 11:53:55 -07:00
committed by GitHub
parent 98eb64749f
commit 154702ad35
7 changed files with 501 additions and 97 deletions
@@ -30,19 +30,6 @@ auto HasNonWaterFire(const HexMap *hexMap) -> bool {
});
}
auto HasTraversableRegionBoundaryModifier(const HexMap *hexMap) -> bool {
return std::ranges::any_of(*hexMap->terrain(), [](const auto *terrain) {
if (!IsWater(terrain->type()) &&
terrain->type() != net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN) {
return false;
}
const auto &modifier = terrain->modifier();
return (modifier.bridge().present() && modifier.bridge().integrity() > 0.0) ||
modifier.ice().present();
});
}
auto StaticRegionsAreConclusiveFor(const BattalionType &battalionType) -> bool {
return battalionType.costToEnterWater.type == ActionCost::impossible &&
battalionType.costToEnterMountain.type == ActionCost::impossible;
@@ -64,35 +51,39 @@ auto CanReachAllWithDistances(
});
}
auto DefinitelyReachesAllWithoutWater(
const LandRegions &landRegions,
auto DefinitelyReachesAllWithoutCreatingCrossing(
const EffectiveLandRegions &effectiveLandRegions,
const Coords &origin,
const CoordsSet &destinations) -> bool {
return std::ranges::all_of(destinations, [&](const Coords &destination) {
return landRegions.DefinitelyConnectedWithoutWater(origin, destination);
return effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(origin, destination);
});
}
auto DefinitelyReachesAllWithoutWater(
const GameStateW &gameState,
auto DefinitelyReachesAllWithoutCreatingCrossing(
const HexMap *map,
const net::eagle0::shardok::storage::fb::Unit *unit,
const CoordsSet &destinations,
const LandRegions &landRegions) -> bool {
const EffectiveLandRegions &effectiveLandRegions) -> bool {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
return DefinitelyReachesAllWithoutWater(landRegions, unit->location(), destinations);
return DefinitelyReachesAllWithoutCreatingCrossing(
effectiveLandRegions,
unit->location(),
destinations);
}
const auto *startingPositions = gameState->hex_map()
->attacker_starting_positions()
->Get(unit->starting_position_index())
->positions();
const auto *startingPositions =
map->attacker_starting_positions()->Get(unit->starting_position_index())->positions();
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
return DefinitelyReachesAllWithoutWater(landRegions, *position, destinations);
return DefinitelyReachesAllWithoutCreatingCrossing(
effectiveLandRegions,
*position,
destinations);
});
}
auto CanReachAllWithoutWater(
const GameStateW &gameState,
const HexMap *map,
const net::eagle0::shardok::storage::fb::Unit *unit,
const CoordsSet &destinations,
const ActionPointDistances *distances) -> bool {
@@ -100,17 +91,15 @@ auto CanReachAllWithoutWater(
return CanReachAllWithDistances(unit->location(), destinations, distances);
}
const auto *startingPositions = gameState->hex_map()
->attacker_starting_positions()
->Get(unit->starting_position_index())
->positions();
const auto *startingPositions =
map->attacker_starting_positions()->Get(unit->starting_position_index())->positions();
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
return CanReachAllWithDistances(*position, destinations, distances);
});
}
auto IsActiveNonUndeadAttacker(
const GameStateW &gameState,
const GameState *gameState,
const net::eagle0::shardok::storage::fb::Unit *unit) -> bool {
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
@@ -150,6 +139,7 @@ auto UnitIdsRequiringWaterCrossing(
const GameState *state = gameState.Get();
const auto *units = state->units();
const HexMap *map = state->hex_map();
const EffectiveLandRegions effectiveLandRegions(landRegions, map);
std::vector<const net::eagle0::shardok::storage::fb::Unit *> unitsToCheck{};
unitsToCheck.reserve(units->size());
@@ -159,7 +149,10 @@ auto UnitIdsRequiringWaterCrossing(
if (unit->player_id() != pid) continue;
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
if (!DefinitelyReachesAllWithoutWater(landRegions, unit->location(), destinations)) {
if (!DefinitelyReachesAllWithoutCreatingCrossing(
effectiveLandRegions,
unit->location(),
destinations)) {
unitsToCheck.push_back(unit);
}
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
@@ -169,7 +162,10 @@ auto UnitIdsRequiringWaterCrossing(
->Get(unit->starting_position_index())
->positions();
for (const Coords *position : *startingPositions) {
if (!DefinitelyReachesAllWithoutWater(landRegions, *position, destinations)) {
if (!DefinitelyReachesAllWithoutCreatingCrossing(
effectiveLandRegions,
*position,
destinations)) {
startingPositionIndices[unit->starting_position_index()] = 1;
break;
}
@@ -184,7 +180,6 @@ auto UnitIdsRequiringWaterCrossing(
if (unitsToCheck.empty()) return {};
MapId mapId = ActionPointDistancesCache::GetMapId(map);
const bool staticRegionsCanProveDisconnection = !HasTraversableRegionBoundaryModifier(map);
fb::HexMapW fireClearedMap;
bool mapPreparedForDistances = false;
@@ -223,16 +218,14 @@ auto UnitIdsRequiringWaterCrossing(
vector<UnitId> unitIdsToReturn{};
unitIdsToReturn.reserve(unitsToCheck.size());
// 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.
// The effective-region pass above removed every unit that can reach every destination through
// static land and current bridges. Resolve full APDs only for battalions that can traverse raw
// water or mountains.
startingPositionIndices.fill(-1);
for (const auto *unit : unitsToCheck) {
const auto &battType = battalionTypeGetter(unit->battalion().type());
// If this battalion cannot traverse either kind of static region boundary, and no bridge
// or ice currently makes one traversable, the failed region check above conclusively means
// that at least one destination is unreachable without creating a water crossing.
if (staticRegionsCanProveDisconnection && StaticRegionsAreConclusiveFor(*battType)) {
if (StaticRegionsAreConclusiveFor(*battType)) {
unitIdsToReturn.push_back(unit->unit_id());
continue;
}
@@ -282,13 +275,22 @@ auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
const std::size_t requiredUnitCount) -> bool {
if (requiredUnitCount == 0) return true;
const GameState *state = gameState.Get();
const auto *units = state->units();
const HexMap *baseMap = state->hex_map();
const EffectiveLandRegions effectiveLandRegions(landRegions, baseMap);
std::size_t eligibleUnitCount = 0;
std::size_t reachableUnitCount = 0;
for (const auto *unit : *gameState->units()) {
if (!IsActiveNonUndeadAttacker(gameState, unit)) continue;
for (const auto *unit : *units) {
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
++eligibleUnitCount;
if (DefinitelyReachesAllWithoutWater(gameState, unit, destinations, landRegions) &&
if (DefinitelyReachesAllWithoutCreatingCrossing(
baseMap,
unit,
destinations,
effectiveLandRegions) &&
++reachableUnitCount >= requiredUnitCount) {
return true;
}
@@ -296,29 +298,33 @@ auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
if (eligibleUnitCount < requiredUnitCount) return false;
const HexMap *map = gameState->hex_map();
MapId mapId = ActionPointDistancesCache::GetMapId(map);
const HexMap *map = baseMap;
MapId mapId = ActionPointDistancesCache::GetMapId(baseMap);
fb::HexMapW fireClearedMap;
if (HasNonWaterFire(map)) {
fireClearedMap = fb::CopyHexMap(map);
for (uint32_t index = 0; index < fireClearedMap->terrain()->size(); ++index) {
if (IsWater(fireClearedMap->terrain()->Get(index)->type())) continue;
// const_cast is safe because we own the mutable buffer (fireClearedMap)
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
fireClearedMap->mutable_terrain()->GetMutableObject(index))
->mutable_modifier()
.mutable_fire()
.mutate_present(false);
}
map = fireClearedMap.Get();
mapId = RecomputeModifierHash(fireClearedMap.Get());
}
bool mapPreparedForDistances = false;
std::array<const ActionPointDistances *, BattalionTypeId::BattalionTypeId_MAX + 1>
distancesByBattalionType{};
const auto distancesFor = [&](const BattalionTypeSPtr &battalionType) {
if (!mapPreparedForDistances) {
mapPreparedForDistances = true;
if (HasNonWaterFire(map)) {
fireClearedMap = fb::CopyHexMap(map);
for (uint32_t index = 0; index < fireClearedMap->terrain()->size(); ++index) {
if (IsWater(fireClearedMap->terrain()->Get(index)->type())) continue;
// const_cast is safe because we own the mutable buffer (fireClearedMap)
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
fireClearedMap->mutable_terrain()->GetMutableObject(index))
->mutable_modifier()
.mutable_fire()
.mutate_present(false);
}
map = fireClearedMap.Get();
mapId = RecomputeModifierHash(fireClearedMap.Get());
}
}
auto &distances = distancesByBattalionType[battalionType->typeId];
if (distances == nullptr) {
distances = apdCache->GetRaw(map, mapId, battalionType, false);
@@ -326,12 +332,20 @@ auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
return distances;
};
for (const auto *unit : *gameState->units()) {
if (!IsActiveNonUndeadAttacker(gameState, unit)) continue;
if (DefinitelyReachesAllWithoutWater(gameState, unit, destinations, landRegions)) continue;
for (const auto *unit : *units) {
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
if (DefinitelyReachesAllWithoutCreatingCrossing(
baseMap,
unit,
destinations,
effectiveLandRegions)) {
continue;
}
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
if (CanReachAllWithoutWater(gameState, unit, destinations, distancesFor(battalionType)) &&
if (StaticRegionsAreConclusiveFor(*battalionType)) continue;
if (CanReachAllWithoutWater(map, unit, destinations, distancesFor(battalionType)) &&
++reachableUnitCount >= requiredUnitCount) {
return true;
}
@@ -4,6 +4,7 @@
#include "LandRegions.hpp"
#include <algorithm>
#include <array>
#include <limits>
#include <stdexcept>
@@ -38,6 +39,19 @@ auto ToIndex(const int row, const int column, const int columnCount) -> int {
return row * columnCount + column;
}
auto CountRegions(const std::span<const LandRegionId> regions) -> int {
int count = 0;
for (const LandRegionId region : regions) {
if (region != kNoLandRegion) { count = std::max(count, static_cast<int>(region) + 1); }
}
return count;
}
auto HasUsableBridge(const net::eagle0::shardok::storage::fb::Terrain& terrain) -> bool {
const auto& bridge = terrain.modifier().bridge();
return bridge.present() && bridge.integrity() > 0.0;
}
} // namespace
auto ComputeLandRegions(
@@ -99,11 +113,13 @@ auto ComputeLandRegions(
LandRegions::LandRegions(const net::eagle0::shardok::storage::fb::HexMap* map)
: rowCount(map->row_count()),
columnCount(map->column_count()) {
columnCount(map->column_count()),
regionCount(0) {
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);
regionCount = CountRegions(regions);
}
LandRegions::LandRegions(
@@ -112,7 +128,10 @@ LandRegions::LandRegions(
const std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
: rowCount(rowCount),
columnCount(columnCount),
regions(ComputeLandRegions(rowCount, columnCount, terrain)) {}
regionCount(0),
regions(ComputeLandRegions(rowCount, columnCount, terrain)) {
regionCount = CountRegions(regions);
}
auto LandRegions::RegionAt(const Coords& coords) const -> LandRegionId {
if (coords.row() < 0 || coords.column() < 0 || coords.row() >= rowCount ||
@@ -129,4 +148,137 @@ auto LandRegions::DefinitelyConnectedWithoutWater(const Coords& origin, const Co
return originRegion != kNoLandRegion && originRegion == destinationRegion;
}
template<typename TerrainAt>
void EffectiveLandRegions::Initialize(
const int rowCount,
const int columnCount,
TerrainAt&& terrainAt) {
const int tileCount = rowCount * columnCount;
if (rowCount != landRegions.rowCount || columnCount != landRegions.columnCount ||
tileCount < 0 || tileCount > static_cast<int>(kMaximumLandRegionTileCount)) {
throw std::invalid_argument("Effective land region map dimensions do not match");
}
nodeCount = landRegions.regionCount + tileCount;
for (int node = 0; node < nodeCount; ++node) {
parents[static_cast<size_t>(node)] = static_cast<int16_t>(node);
}
for (int index = 0; index < tileCount; ++index) {
bridgeTiles[static_cast<size_t>(index)] =
landRegions.regions[static_cast<size_t>(index)] == kNoLandRegion &&
HasUsableBridge(terrainAt(index));
}
for (int index = 0; index < tileCount; ++index) {
if (!bridgeTiles[static_cast<size_t>(index)]) continue;
const int row = index / columnCount;
const int column = index % columnCount;
const auto& offsets = row % 2 == 0 ? kEvenRowNeighborOffsets : kOddRowNeighborOffsets;
const int bridgeNode = landRegions.regionCount + index;
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);
const LandRegionId neighborRegion =
landRegions.regions[static_cast<size_t>(neighborIndex)];
if (neighborRegion != kNoLandRegion) {
Union(bridgeNode, neighborRegion);
} else if (bridgeTiles[static_cast<size_t>(neighborIndex)]) {
Union(bridgeNode, landRegions.regionCount + neighborIndex);
}
}
}
for (int node = 0; node < nodeCount; ++node) {
parents[static_cast<size_t>(node)] = static_cast<int16_t>(Root(node));
}
}
EffectiveLandRegions::EffectiveLandRegions(
const LandRegions& landRegions,
const net::eagle0::shardok::storage::fb::HexMap* map)
: landRegions(landRegions),
nodeCount(0) {
if (map == nullptr) { throw std::invalid_argument("Effective land region map is null"); }
Initialize(
map->row_count(),
map->column_count(),
[&](const int index) -> const auto& { return *map->terrain()->Get(index); });
}
EffectiveLandRegions::EffectiveLandRegions(
const LandRegions& landRegions,
const int rowCount,
const int columnCount,
const std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
: landRegions(landRegions),
nodeCount(0) {
if (terrain.size() != static_cast<size_t>(rowCount * columnCount)) {
throw std::invalid_argument("Effective land region terrain size does not match");
}
Initialize(
rowCount,
columnCount,
[&](const int index) -> const auto& { return terrain[static_cast<size_t>(index)]; });
}
auto EffectiveLandRegions::Root(const int node) const -> int {
int current = node;
while (parents[static_cast<size_t>(current)] != current) {
current = parents[static_cast<size_t>(current)];
}
return current;
}
auto EffectiveLandRegions::Root(const int node) -> int {
int root = node;
while (parents[static_cast<size_t>(root)] != root) {
root = parents[static_cast<size_t>(root)];
}
int current = node;
while (parents[static_cast<size_t>(current)] != current) {
const int next = parents[static_cast<size_t>(current)];
parents[static_cast<size_t>(current)] = static_cast<int16_t>(root);
current = next;
}
return root;
}
void EffectiveLandRegions::Union(const int first, const int second) {
const int firstRoot = Root(first);
const int secondRoot = Root(second);
if (firstRoot != secondRoot) {
parents[static_cast<size_t>(secondRoot)] = static_cast<int16_t>(firstRoot);
}
}
auto EffectiveLandRegions::NodeAt(const Coords& coords) const -> int {
if (coords.row() < 0 || coords.column() < 0 || coords.row() >= landRegions.rowCount ||
coords.column() >= landRegions.columnCount) {
return -1;
}
const int index = ToIndex(coords.row(), coords.column(), landRegions.columnCount);
const LandRegionId region = landRegions.regions[static_cast<size_t>(index)];
if (region != kNoLandRegion) return region;
if (bridgeTiles[static_cast<size_t>(index)]) return landRegions.regionCount + index;
return -1;
}
auto EffectiveLandRegions::DefinitelyConnectedWithoutCreatingCrossing(
const Coords& origin,
const Coords& destination) const -> bool {
const int originNode = NodeAt(origin);
const int destinationNode = NodeAt(destination);
return originNode >= 0 && destinationNode >= 0 && Root(originNode) == Root(destinationNode);
}
} // namespace shardok
@@ -5,6 +5,8 @@
#ifndef EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
#define EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
@@ -18,6 +20,7 @@ namespace shardok {
using LandRegionId = int8_t;
constexpr LandRegionId kNoLandRegion = -1;
constexpr size_t kMaximumLandRegionTileCount = size_t{12} * 14;
[[nodiscard]] auto ComputeLandRegions(
int rowCount,
@@ -29,8 +32,11 @@ class LandRegions {
private:
int rowCount;
int columnCount;
int regionCount;
std::vector<LandRegionId> regions;
friend class EffectiveLandRegions;
public:
explicit LandRegions(const net::eagle0::shardok::storage::fb::HexMap* map);
@@ -50,6 +56,42 @@ public:
[[nodiscard]] auto size() const -> size_t { return regions.size(); }
};
// Dynamic connectivity derived from immutable land regions plus the usable bridges in one
// speculative map state. Ice is intentionally excluded because the AI treats frozen crossings as
// crossing-strategy targets rather than ordinary land paths.
class EffectiveLandRegions {
private:
static constexpr size_t kMaximumNodeCount = kMaximumLandRegionTileCount * 2;
const LandRegions& landRegions;
int nodeCount;
std::array<int16_t, kMaximumNodeCount> parents{};
std::array<bool, kMaximumLandRegionTileCount> bridgeTiles{};
template<typename TerrainAt>
void Initialize(int rowCount, int columnCount, TerrainAt&& terrainAt);
[[nodiscard]] auto Root(int node) const -> int;
auto Root(int node) -> int;
void Union(int first, int second);
[[nodiscard]] auto NodeAt(const Coords& coords) const -> int;
public:
EffectiveLandRegions(
const LandRegions& landRegions,
const net::eagle0::shardok::storage::fb::HexMap* map);
EffectiveLandRegions(
const LandRegions& landRegions,
int rowCount,
int columnCount,
std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain);
[[nodiscard]] auto DefinitelyConnectedWithoutCreatingCrossing(
const Coords& origin,
const Coords& destination) const -> bool;
};
} // namespace shardok
#endif // EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
@@ -13,18 +13,6 @@
#include "src/test/cpp/net/eagle0/shardok/library/GameStart_test_data.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/HexMap_test_data.hpp"
namespace {
auto AllWaterLandRegions(const HexMap* map) -> shardok::LandRegions {
std::vector<Terrain> terrain(map->terrain()->size());
for (auto& tile : terrain) {
tile.mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_STILL_WATER);
}
return shardok::LandRegions(map->row_count(), map->column_count(), terrain);
}
} // namespace
class AIStrategySelectorTest : public ::testing::Test {
void SetUp() override {
InitializeGameSettings();
@@ -194,7 +182,7 @@ TEST_F(AIStrategySelectorTest,
}
TEST_F(AIStrategySelectorTest,
DefenderUnder15PercentOfAttacker_enoughAttackerUnits_checksWaterCrossingAndReturnsFlee) {
DefenderUnder15PercentOfAttacker_enoughConnectedAttackerUnitsReturnsFleeWithoutPathLookup) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
@@ -208,7 +196,6 @@ TEST_F(AIStrategySelectorTest,
defender->mutable_battalion().mutate_size(134);
const auto disconnectedLandRegions = AllWaterLandRegions(hexMap);
int battalionTypeLookups = 0;
EXPECT_EQ(
@@ -218,13 +205,13 @@ TEST_F(AIStrategySelectorTest,
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
disconnectedLandRegions,
alCache->GetLandRegions(),
[&battalionTypeLookups](BattalionTypeId typeId) {
++battalionTypeLookups;
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
.strategyType);
EXPECT_GT(battalionTypeLookups, 0);
EXPECT_EQ(battalionTypeLookups, 0);
}
TEST_F(AIStrategySelectorTest,
@@ -273,7 +260,6 @@ TEST_F(AIStrategySelectorTest,
defender->mutable_battalion().mutate_size(134);
defender->mutate_can_flee(false);
const auto disconnectedLandRegions = AllWaterLandRegions(hexMap);
int battalionTypeLookups = 0;
EXPECT_EQ(
@@ -283,7 +269,7 @@ TEST_F(AIStrategySelectorTest,
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
disconnectedLandRegions,
alCache->GetLandRegions(),
[&battalionTypeLookups](BattalionTypeId typeId) {
++battalionTypeLookups;
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
@@ -10,6 +10,7 @@
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/GameSettings_test_utils.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/GameStart_test_data.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/HexMap_test_data.hpp"
@@ -162,11 +163,10 @@ TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_oneOtherSide_r
TEST_F(AIWaterCrossingCalculatorTest,
UnitsRequiringWaterCrossing_fireChanges_reusesFireClearedDistances) {
auto& bridgeModifier = MutableTerrain(hexMap, Coords(0, 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);
unit0->mutable_battalion().mutate_type(
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD);
unit1->mutable_battalion().mutate_type(
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD);
CoordsSet destinations(hexMap);
destinations.Add(acrossDestination);
@@ -247,7 +247,7 @@ TEST_F(AIWaterCrossingCalculatorTest,
}
TEST_F(AIWaterCrossingCalculatorTest,
UnitsRequiringWaterCrossing_bridgeBetweenRegions_fallsBackToDistances) {
UnitsRequiringWaterCrossing_bridgeBetweenRegions_skipsDistances) {
auto& bridgeModifier = MutableTerrain(hexMap, Coords(1, 3))->mutable_modifier();
bridgeModifier.mutable_bridge().mutate_present(true);
bridgeModifier.mutable_bridge().mutate_integrity(100.0);
@@ -267,7 +267,55 @@ TEST_F(AIWaterCrossingCalculatorTest,
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
})
.empty());
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest,
UnitsRequiringWaterCrossing_irrelevantBridgeStillSkipsDistances) {
auto& bridgeModifier = MutableTerrain(hexMap, Coords(3, 5))->mutable_modifier();
bridgeModifier.mutable_bridge().mutate_present(true);
bridgeModifier.mutable_bridge().mutate_integrity(100.0);
CoordsSet destinations(hexMap);
destinations.Add(acrossDestination);
const auto expected = std::vector<UnitId>{0, 1};
EXPECT_EQ(
expected,
UnitIdsRequiringWaterCrossing(
gameState,
0,
destinations,
alCache->GetLandRegions(),
apdCache,
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
}));
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest,
UnitsRequiringWaterCrossing_iceRemainsACrossingStrategyTarget) {
auto& iceModifier = MutableTerrain(hexMap, Coords(1, 3))->mutable_modifier();
iceModifier.mutable_ice().mutate_present(true);
iceModifier.mutable_ice().mutate_integrity(100.0);
CoordsSet destinations(hexMap);
destinations.Add(acrossDestination);
const auto expected = std::vector<UnitId>{0, 1};
EXPECT_EQ(
expected,
UnitIdsRequiringWaterCrossing(
gameState,
0,
destinations,
alCache->GetLandRegions(),
apdCache,
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
}));
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest,
@@ -292,6 +340,73 @@ TEST_F(AIWaterCrossingCalculatorTest,
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest,
EffectiveRegionsMatchExactDistancesForEverySingleBridgePlacement) {
const auto terrainAt = [&](const Coords& coords) {
return hexMap->terrain()->Get(coords.row() * hexMap->column_count() + coords.column());
};
std::vector<std::optional<Coords>> bridgePlacements{std::nullopt};
for (int row = 0; row < hexMap->row_count(); ++row) {
for (int column = 0; column < hexMap->column_count(); ++column) {
const Coords coords(row, column);
if (IsWater(terrainAt(coords)->type())) bridgePlacements.emplace_back(coords);
}
}
for (const auto& bridgePlacement : bridgePlacements) {
for (uint32_t index = 0; index < hexMap->terrain()->size(); ++index) {
auto* terrain = const_cast<net::eagle0::shardok::storage::fb::Terrain*>(
hexMap->mutable_terrain()->GetMutableObject(index));
terrain->mutable_modifier().mutable_bridge().mutate_present(false);
}
if (bridgePlacement.has_value()) {
auto& bridge =
MutableTerrain(hexMap, *bridgePlacement)->mutable_modifier().mutable_bridge();
bridge.mutate_present(true);
bridge.mutate_integrity(100.0);
}
hexMap->mutate_modifier_hash(GetModifierHash(hexMap));
const EffectiveLandRegions effectiveRegions(alCache->GetLandRegions(), hexMap);
const auto* distances = apdCache->GetRaw(
hexMap,
ActionPointDistancesCache::GetMapId(hexMap),
heavyCavalry,
false);
for (int originRow = 0; originRow < hexMap->row_count(); ++originRow) {
for (int originColumn = 0; originColumn < hexMap->column_count(); ++originColumn) {
const Coords originCoords(originRow, originColumn);
const auto* originTerrain = terrainAt(originCoords);
if (IsWater(originTerrain->type()) &&
!originTerrain->modifier().bridge().present()) {
continue;
}
for (int destinationRow = 0; destinationRow < hexMap->row_count();
++destinationRow) {
for (int destinationColumn = 0; destinationColumn < hexMap->column_count();
++destinationColumn) {
const Coords destinationCoords(destinationRow, destinationColumn);
const auto* destinationTerrain = terrainAt(destinationCoords);
if (IsWater(destinationTerrain->type()) &&
!destinationTerrain->modifier().bridge().present()) {
continue;
}
EXPECT_EQ(
distances->Distance(originCoords, destinationCoords) !=
ActionPointDistances::IMPOSSIBLE,
effectiveRegions.DefinitelyConnectedWithoutCreatingCrossing(
originCoords,
destinationCoords));
}
}
}
}
}
}
TEST_F(AIWaterCrossingCalculatorTest,
EnoughAttackersWithoutWaterCrossing_staticRegionsShortCircuitWithoutDistances) {
unit0->mutable_location() = Coords(0, 4);
@@ -316,7 +431,7 @@ TEST_F(AIWaterCrossingCalculatorTest,
}
TEST_F(AIWaterCrossingCalculatorTest,
EnoughAttackersWithoutWaterCrossing_exactDistancesShortCircuitAtThreshold) {
EnoughAttackersWithoutWaterCrossing_bridgeShortCircuitsWithoutDistances) {
auto& bridgeModifier = MutableTerrain(hexMap, Coords(1, 3))->mutable_modifier();
bridgeModifier.mutable_bridge().mutate_present(true);
bridgeModifier.mutable_bridge().mutate_integrity(100.0);
@@ -337,8 +452,8 @@ TEST_F(AIWaterCrossingCalculatorTest,
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
},
1));
EXPECT_EQ(1, battalionTypeLookups);
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
EXPECT_EQ(0, battalionTypeLookups);
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest, EnoughAttackersWithoutWaterCrossing_noLandPathReturnsFalse) {
@@ -354,7 +469,7 @@ TEST_F(AIWaterCrossingCalculatorTest, EnoughAttackersWithoutWaterCrossing_noLand
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
},
1));
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest,
@@ -177,6 +177,30 @@ TEST_F(RealBattleDecisionRegressionTest, ChipingiaWaterCrossingFindsUsefulBridge
EXPECT_FALSE(crossingTiles.empty());
}
TEST_F(RealBattleDecisionRegressionTest, AlahBridgeConnectsAttackersToEveryCastle) {
flatbuffers::FlatBufferBuilder fbb;
fbb.ForceDefaults(true);
const auto mapOffset = fb::ConvertHexMapProto(fbb, LoadMap("Alah"));
const auto* map = flatbuffers::GetTemporaryPointer(fbb, mapOffset);
const LandRegions staticRegions(map);
const EffectiveLandRegions effectiveRegions(staticRegions, map);
const CoordsSet castles = AllCastleCoords(map);
const auto* attackerPositions = map->attacker_starting_positions()->Get(0)->positions();
ASSERT_FALSE(attackerPositions->empty());
ASSERT_FALSE(castles.empty());
EXPECT_FALSE(staticRegions.DefinitelyConnectedWithoutWater(
**attackerPositions->begin(),
*castles.begin()));
for (const Coords* attackerPosition : *attackerPositions) {
for (const Coords& castle : castles) {
EXPECT_TRUE(effectiveRegions.DefinitelyConnectedWithoutCreatingCrossing(
*attackerPosition,
castle));
}
}
}
TEST_F(RealBattleDecisionRegressionTest, RegiaAtSigkarlFirstAttackerTurnDoesNotFlee) {
std::vector<Unit> units{
MakeUnit(
@@ -20,6 +20,11 @@ auto TerrainGrid(const int size, const TerrainType type = TerrainType::Type_PLAI
return std::vector<Terrain>(static_cast<size_t>(size), Terrain(type, TileModifier()));
}
void AddBridge(Terrain& terrain, const double integrity = 100.0) {
terrain.mutable_modifier().mutable_bridge().mutate_present(true);
terrain.mutable_modifier().mutable_bridge().mutate_integrity(integrity);
}
TEST(LandRegionsTest, ConnectedLandSharesOneRegion) {
const auto terrain = TerrainGrid(9);
@@ -58,5 +63,71 @@ TEST(LandRegionsTest, MountainsAlsoSeparateGuaranteedLandRegions) {
EXPECT_FALSE(regions.DefinitelyConnectedWithoutWater(Coords(1, 0), Coords(1, 2)));
}
TEST(EffectiveLandRegionsTest, BridgeMergesStaticRegions) {
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 staticRegions(3, 3, terrain);
AddBridge(terrain[4]);
const EffectiveLandRegions effectiveRegions(staticRegions, 3, 3, terrain);
EXPECT_TRUE(effectiveRegions.DefinitelyConnectedWithoutCreatingCrossing(
Coords(1, 0),
Coords(1, 2)));
EXPECT_TRUE(effectiveRegions.DefinitelyConnectedWithoutCreatingCrossing(
Coords(1, 1),
Coords(1, 2)));
}
TEST(EffectiveLandRegionsTest, AdjacentBridgesCanSpanWideWater) {
auto terrain = TerrainGrid(12);
for (int row = 0; row < 3; ++row) {
terrain[static_cast<size_t>(row * 4 + 1)].mutate_type(TerrainType::Type_RIVER);
terrain[static_cast<size_t>(row * 4 + 2)].mutate_type(TerrainType::Type_RIVER);
}
const LandRegions staticRegions(3, 4, terrain);
AddBridge(terrain[5]);
AddBridge(terrain[6]);
const EffectiveLandRegions effectiveRegions(staticRegions, 3, 4, terrain);
EXPECT_TRUE(effectiveRegions.DefinitelyConnectedWithoutCreatingCrossing(
Coords(1, 0),
Coords(1, 3)));
}
TEST(EffectiveLandRegionsTest, BrokenBridgeDoesNotMergeRegions) {
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 staticRegions(3, 3, terrain);
AddBridge(terrain[4], 0.0);
const EffectiveLandRegions effectiveRegions(staticRegions, 3, 3, terrain);
EXPECT_FALSE(effectiveRegions.DefinitelyConnectedWithoutCreatingCrossing(
Coords(1, 0),
Coords(1, 2)));
}
TEST(EffectiveLandRegionsTest, IceDoesNotMergeRegions) {
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 staticRegions(3, 3, terrain);
terrain[4].mutable_modifier().mutable_ice().mutate_present(true);
terrain[4].mutable_modifier().mutable_ice().mutate_integrity(100.0);
const EffectiveLandRegions effectiveRegions(staticRegions, 3, 3, terrain);
EXPECT_FALSE(effectiveRegions.DefinitelyConnectedWithoutCreatingCrossing(
Coords(1, 0),
Coords(1, 2)));
}
} // namespace
} // namespace shardok