mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Skip exact water checks across static regions (#8780)
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user