Reuse effective destination connectivity checks (#8789)

This commit is contained in:
2026-07-25 19:29:17 -07:00
committed by GitHub
parent a7c8cb11fc
commit 3add561a82
2 changed files with 107 additions and 39 deletions
@@ -6,6 +6,7 @@
#include <algorithm>
#include <array>
#include <optional>
#include "AIMinimumDistanceAndTarget.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
@@ -51,34 +52,50 @@ auto CanReachAllWithDistances(
});
}
auto DefinitelyReachesAllWithoutCreatingCrossing(
const EffectiveLandRegions &effectiveLandRegions,
const Coords &origin,
const CoordsSet &destinations) -> bool {
return std::ranges::all_of(destinations, [&](const Coords &destination) {
return effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(origin, destination);
});
}
class EffectiveDestinations {
private:
const EffectiveLandRegions &effectiveLandRegions;
std::optional<Coords> representative;
bool shareRegion;
public:
EffectiveDestinations(
const EffectiveLandRegions &effectiveLandRegions,
const CoordsSet &destinations)
: effectiveLandRegions(effectiveLandRegions),
shareRegion(true) {
for (const Coords &destination : destinations) {
if (!representative.has_value()) {
representative = destination;
} else if (!effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(
*representative,
destination)) {
shareRegion = false;
break;
}
}
}
[[nodiscard]] auto DefinitelyReachableFrom(const Coords &origin) const -> bool {
return !representative.has_value() ||
(shareRegion && effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(
origin,
*representative));
}
};
auto DefinitelyReachesAllWithoutCreatingCrossing(
const HexMap *map,
const net::eagle0::shardok::storage::fb::Unit *unit,
const CoordsSet &destinations,
const EffectiveLandRegions &effectiveLandRegions) -> bool {
const EffectiveDestinations &effectiveDestinations) -> bool {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
return DefinitelyReachesAllWithoutCreatingCrossing(
effectiveLandRegions,
unit->location(),
destinations);
return effectiveDestinations.DefinitelyReachableFrom(unit->location());
}
const auto *startingPositions =
map->attacker_starting_positions()->Get(unit->starting_position_index())->positions();
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
return DefinitelyReachesAllWithoutCreatingCrossing(
effectiveLandRegions,
*position,
destinations);
return effectiveDestinations.DefinitelyReachableFrom(*position);
});
}
@@ -140,20 +157,21 @@ auto UnitIdsRequiringWaterCrossing(
const auto *units = state->units();
const HexMap *map = state->hex_map();
const EffectiveLandRegions effectiveLandRegions(landRegions, map);
const EffectiveDestinations effectiveDestinations(effectiveLandRegions, destinations);
std::vector<const net::eagle0::shardok::storage::fb::Unit *> unitsToCheck{};
unitsToCheck.reserve(units->size());
const auto queueForExactCheck = [&](const auto *unit) {
if (unitsToCheck.empty()) { unitsToCheck.reserve(units->size()); }
unitsToCheck.push_back(unit);
};
std::array<int8_t, 9> startingPositionIndices{};
startingPositionIndices.fill(-1);
for (const auto *unit : *units) {
if (unit->player_id() != pid) continue;
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
if (!DefinitelyReachesAllWithoutCreatingCrossing(
effectiveLandRegions,
unit->location(),
destinations)) {
unitsToCheck.push_back(unit);
if (!effectiveDestinations.DefinitelyReachableFrom(unit->location())) {
queueForExactCheck(unit);
}
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
if (startingPositionIndices[unit->starting_position_index()] == -1) {
@@ -162,10 +180,7 @@ auto UnitIdsRequiringWaterCrossing(
->Get(unit->starting_position_index())
->positions();
for (const Coords *position : *startingPositions) {
if (!DefinitelyReachesAllWithoutCreatingCrossing(
effectiveLandRegions,
*position,
destinations)) {
if (!effectiveDestinations.DefinitelyReachableFrom(*position)) {
startingPositionIndices[unit->starting_position_index()] = 1;
break;
}
@@ -173,7 +188,7 @@ auto UnitIdsRequiringWaterCrossing(
}
if (startingPositionIndices[unit->starting_position_index()] == 1) {
unitsToCheck.push_back(unit);
queueForExactCheck(unit);
}
}
}
@@ -279,6 +294,7 @@ auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
const auto *units = state->units();
const HexMap *baseMap = state->hex_map();
const EffectiveLandRegions effectiveLandRegions(landRegions, baseMap);
const EffectiveDestinations effectiveDestinations(effectiveLandRegions, destinations);
std::size_t eligibleUnitCount = 0;
std::size_t reachableUnitCount = 0;
@@ -286,11 +302,7 @@ auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
++eligibleUnitCount;
if (DefinitelyReachesAllWithoutCreatingCrossing(
baseMap,
unit,
destinations,
effectiveLandRegions) &&
if (DefinitelyReachesAllWithoutCreatingCrossing(baseMap, unit, effectiveDestinations) &&
++reachableUnitCount >= requiredUnitCount) {
return true;
}
@@ -334,11 +346,7 @@ auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
for (const auto *unit : *units) {
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
if (DefinitelyReachesAllWithoutCreatingCrossing(
baseMap,
unit,
destinations,
effectiveLandRegions)) {
if (DefinitelyReachesAllWithoutCreatingCrossing(baseMap, unit, effectiveDestinations)) {
continue;
}
@@ -118,6 +118,43 @@ TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_allSameSide_re
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_emptyDestinations_returnsZero) {
const CoordsSet destinations(hexMap);
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_destinationsInDifferentRegions_returnsTwo) {
CoordsSet destinations(hexMap);
destinations.Add(sameSideDestination);
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_allOtherSide_returnsTwo) {
unit0->mutable_location() = Coords(5, 4);
unit1->mutable_location() = Coords(5, 0);
@@ -270,6 +307,29 @@ TEST_F(AIWaterCrossingCalculatorTest,
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
}
TEST_F(AIWaterCrossingCalculatorTest,
UnitsRequiringWaterCrossing_bridgeConnectsAllDestinationRegions_skipsDistances) {
auto& bridgeModifier = MutableTerrain(hexMap, Coords(1, 3))->mutable_modifier();
bridgeModifier.mutable_bridge().mutate_present(true);
bridgeModifier.mutable_bridge().mutate_integrity(100.0);
CoordsSet destinations(hexMap);
destinations.Add(sameSideDestination);
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_irrelevantBridgeStillSkipsDistances) {
auto& bridgeModifier = MutableTerrain(hexMap, Coords(3, 5))->mutable_modifier();