Skip exact water checks across static regions (#8780)

This commit is contained in:
2026-07-24 22:27:17 -07:00
committed by GitHub
parent f913116132
commit 4950bd3609
2 changed files with 103 additions and 18 deletions
@@ -30,6 +30,24 @@ 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;
}
auto CanReachWithDistances(
const Coords &origin,
const Coords &destination,
@@ -163,28 +181,35 @@ auto UnitIdsRequiringWaterCrossing(
const HexMap *map = gameState->hex_map();
MapId mapId = ActionPointDistancesCache::GetMapId(map);
const bool staticRegionsCanProveDisconnection = !HasTraversableRegionBoundaryModifier(map);
// Most states have no relevant fire, so avoid copying the entire map unless it needs mutation.
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;
// Most states have no relevant fire, so avoid copying the entire map unless exact
// distances need it.
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);
@@ -200,6 +225,14 @@ auto UnitIdsRequiringWaterCrossing(
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)) {
unitIdsToReturn.push_back(unit->unit_id());
continue;
}
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
const auto *distances = distancesFor(battType);
for (const Coords &destination : destinations) {
@@ -136,7 +136,7 @@ TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_allOtherSide_r
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
}));
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_oneOtherSide_returnsOne) {
@@ -162,6 +162,12 @@ 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);
CoordsSet destinations(hexMap);
destinations.Add(acrossDestination);
const auto battalionTypeGetter = [](BattalionTypeId typeId) {
@@ -216,6 +222,30 @@ TEST_F(AIWaterCrossingCalculatorTest,
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest,
UnitsRequiringWaterCrossing_brokenBridgeUsesConclusiveRegions) {
auto& bridgeModifier = MutableTerrain(hexMap, Coords(1, 3))->mutable_modifier();
bridgeModifier.mutable_bridge().mutate_present(true);
bridgeModifier.mutable_bridge().mutate_integrity(0.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_bridgeBetweenRegions_fallsBackToDistances) {
auto& bridgeModifier = MutableTerrain(hexMap, Coords(1, 3))->mutable_modifier();
@@ -240,6 +270,28 @@ TEST_F(AIWaterCrossingCalculatorTest,
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest,
UnitsRequiringWaterCrossing_waterTraversingBattalionFallsBackToDistances) {
unit0->mutable_battalion().mutate_type(
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD);
unit1->mutable_location() = Coords(0, 5);
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,
EnoughAttackersWithoutWaterCrossing_staticRegionsShortCircuitWithoutDistances) {
unit0->mutable_location() = Coords(0, 4);