mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Short-circuit defender water-crossing checks (#8779)
This commit is contained in:
@@ -4,9 +4,6 @@
|
||||
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
|
||||
@@ -67,44 +64,14 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
canFlee && roundsRemaining > 2 &&
|
||||
defenderTroops < MAXIMUM_RATIO_FOR_DEFENDER_TO_FLEE * attackerTroops &&
|
||||
attackerNonUndeadUnitCount >= criticalTileCoords.size();
|
||||
if (waterCrossingCouldAffectStrategy) {
|
||||
vector<UnitId> attackerUnitIdsRequiringWaterCrossing{};
|
||||
attackerUnitIdsRequiringWaterCrossing.reserve(gameState->units()->size());
|
||||
for (const auto& player : *gameState->player_infos()) {
|
||||
if (!player->is_defender()) {
|
||||
const auto& unitIdsRequiringWaterCrossing = UnitIdsRequiringWaterCrossing(
|
||||
gameState,
|
||||
player->player_id(),
|
||||
criticalTileCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
attackerUnitIdsRequiringWaterCrossing.insert(
|
||||
attackerUnitIdsRequiringWaterCrossing.end(),
|
||||
unitIdsRequiringWaterCrossing.begin(),
|
||||
unitIdsRequiringWaterCrossing.end());
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t attackerNonUndeadUnitNotRequiringWaterCrossingCount = 0;
|
||||
for (const Unit* unit : *gameState->units()) {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto* pi = PlayerInfoForPid(gameState, unit->player_id());
|
||||
if (pi != nullptr && !pi->is_defender() &&
|
||||
unit->battalion().type() !=
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD &&
|
||||
!std::ranges::contains(attackerUnitIdsRequiringWaterCrossing, unit->unit_id())) {
|
||||
++attackerNonUndeadUnitNotRequiringWaterCrossingCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (attackerNonUndeadUnitNotRequiringWaterCrossingCount >= criticalTileCoords.size()) {
|
||||
return FleeStrategy;
|
||||
}
|
||||
if (waterCrossingCouldAffectStrategy && HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
gameState,
|
||||
criticalTileCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter,
|
||||
criticalTileCoords.size())) {
|
||||
return FleeStrategy;
|
||||
}
|
||||
|
||||
if (attackerNonUndeadUnitCount >= criticalTileCoords.size()) {
|
||||
|
||||
@@ -37,6 +37,15 @@ auto CanReachWithDistances(
|
||||
return distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE;
|
||||
}
|
||||
|
||||
auto CanReachAllWithDistances(
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations,
|
||||
const ActionPointDistances *distances) -> bool {
|
||||
return std::ranges::all_of(destinations, [&](const Coords &destination) {
|
||||
return CanReachWithDistances(origin, destination, distances);
|
||||
});
|
||||
}
|
||||
|
||||
auto DefinitelyReachesAllWithoutWater(
|
||||
const LandRegions &landRegions,
|
||||
const Coords &origin,
|
||||
@@ -46,6 +55,58 @@ auto DefinitelyReachesAllWithoutWater(
|
||||
});
|
||||
}
|
||||
|
||||
auto DefinitelyReachesAllWithoutWater(
|
||||
const GameStateW &gameState,
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const CoordsSet &destinations,
|
||||
const LandRegions &landRegions) -> bool {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
return DefinitelyReachesAllWithoutWater(landRegions, unit->location(), destinations);
|
||||
}
|
||||
|
||||
const auto *startingPositions = gameState->hex_map()
|
||||
->attacker_starting_positions()
|
||||
->Get(unit->starting_position_index())
|
||||
->positions();
|
||||
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
|
||||
return DefinitelyReachesAllWithoutWater(landRegions, *position, destinations);
|
||||
});
|
||||
}
|
||||
|
||||
auto CanReachAllWithoutWater(
|
||||
const GameStateW &gameState,
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const CoordsSet &destinations,
|
||||
const ActionPointDistances *distances) -> bool {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
return CanReachAllWithDistances(unit->location(), destinations, distances);
|
||||
}
|
||||
|
||||
const auto *startingPositions = gameState->hex_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 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) {
|
||||
return false;
|
||||
}
|
||||
if (unit->battalion().type() == net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::ranges::any_of(*gameState->player_infos(), [&](const auto *playerInfo) {
|
||||
return playerInfo->player_id() == unit->player_id() && !playerInfo->is_defender();
|
||||
});
|
||||
}
|
||||
|
||||
auto CanCreateWaterCrossing(
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> bool {
|
||||
@@ -176,6 +237,73 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
return unitIdsToReturn;
|
||||
}
|
||||
|
||||
auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &destinations,
|
||||
const LandRegions &landRegions,
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const std::size_t requiredUnitCount) -> bool {
|
||||
if (requiredUnitCount == 0) return true;
|
||||
|
||||
std::size_t eligibleUnitCount = 0;
|
||||
std::size_t reachableUnitCount = 0;
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
if (!IsActiveNonUndeadAttacker(gameState, unit)) continue;
|
||||
|
||||
++eligibleUnitCount;
|
||||
if (DefinitelyReachesAllWithoutWater(gameState, unit, destinations, landRegions) &&
|
||||
++reachableUnitCount >= requiredUnitCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (eligibleUnitCount < requiredUnitCount) return false;
|
||||
|
||||
const HexMap *map = gameState->hex_map();
|
||||
MapId mapId = ActionPointDistancesCache::GetMapId(map);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
std::array<const ActionPointDistances *, BattalionTypeId::BattalionTypeId_MAX + 1>
|
||||
distancesByBattalionType{};
|
||||
const auto distancesFor = [&](const BattalionTypeSPtr &battalionType) {
|
||||
auto &distances = distancesByBattalionType[battalionType->typeId];
|
||||
if (distances == nullptr) {
|
||||
distances = apdCache->GetRaw(map, mapId, battalionType, false);
|
||||
}
|
||||
return distances;
|
||||
};
|
||||
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
if (!IsActiveNonUndeadAttacker(gameState, unit)) continue;
|
||||
if (DefinitelyReachesAllWithoutWater(gameState, unit, destinations, landRegions)) continue;
|
||||
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
if (CanReachAllWithoutWater(gameState, unit, destinations, distancesFor(battalionType)) &&
|
||||
++reachableUnitCount >= requiredUnitCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto UnitIdsToCreateWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId pid,
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
@@ -40,6 +42,16 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
|
||||
|
||||
// Whether at least requiredUnitCount active, non-undead attacker units can reach every destination
|
||||
// without crossing water.
|
||||
[[nodiscard]] auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& destinations,
|
||||
const LandRegions& landRegions,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
std::size_t requiredUnitCount) -> bool;
|
||||
|
||||
// Units belonging to the player that are capable of creating water crossings
|
||||
[[nodiscard]] auto UnitIdsToCreateWaterCrossing(
|
||||
const GameStateW& gameState,
|
||||
|
||||
@@ -32,6 +32,10 @@ class AIWaterCrossingCalculatorTest : public ::testing::Test {
|
||||
AddGenericUnit(0, 1, Coords(4, 0))};
|
||||
|
||||
const auto unitsOffset = fbb.CreateVectorOfSortedStructs(&unitsVec);
|
||||
const auto playerInfoOffset = fbb.CreateVector(
|
||||
std::vector<flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>>{
|
||||
AddPlayerInfo(fbb, 0, false, 1000),
|
||||
AddPlayerInfo(fbb, 1, true, 1000)});
|
||||
|
||||
auto terrainTypes = TERRAIN_VALUES_FB;
|
||||
terrainTypes[0 * 6 + 3] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
|
||||
@@ -52,6 +56,7 @@ class AIWaterCrossingCalculatorTest : public ::testing::Test {
|
||||
net::eagle0::shardok::storage::fb::GameStateBuilder gsb(fbb);
|
||||
gsb.add_units(unitsOffset);
|
||||
gsb.add_hex_map(hexMapOffset);
|
||||
gsb.add_player_infos(playerInfoOffset);
|
||||
|
||||
fbb.Finish(gsb.Finish());
|
||||
gameState = GameStateW(fbb);
|
||||
@@ -235,6 +240,94 @@ TEST_F(AIWaterCrossingCalculatorTest,
|
||||
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
|
||||
}
|
||||
|
||||
TEST_F(AIWaterCrossingCalculatorTest,
|
||||
EnoughAttackersWithoutWaterCrossing_staticRegionsShortCircuitWithoutDistances) {
|
||||
unit0->mutable_location() = Coords(0, 4);
|
||||
unit1->mutable_location() = Coords(0, 5);
|
||||
|
||||
CoordsSet destinations(hexMap);
|
||||
destinations.Add(acrossDestination);
|
||||
|
||||
int battalionTypeLookups = 0;
|
||||
EXPECT_TRUE(HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
gameState,
|
||||
destinations,
|
||||
alCache->GetLandRegions(),
|
||||
apdCache,
|
||||
[&](BattalionTypeId typeId) {
|
||||
++battalionTypeLookups;
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
},
|
||||
1));
|
||||
EXPECT_EQ(0, battalionTypeLookups);
|
||||
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
|
||||
}
|
||||
|
||||
TEST_F(AIWaterCrossingCalculatorTest,
|
||||
EnoughAttackersWithoutWaterCrossing_exactDistancesShortCircuitAtThreshold) {
|
||||
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);
|
||||
|
||||
int battalionTypeLookups = 0;
|
||||
EXPECT_TRUE(HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
gameState,
|
||||
destinations,
|
||||
alCache->GetLandRegions(),
|
||||
apdCache,
|
||||
[&](BattalionTypeId typeId) {
|
||||
++battalionTypeLookups;
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
},
|
||||
1));
|
||||
EXPECT_EQ(1, battalionTypeLookups);
|
||||
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
|
||||
}
|
||||
|
||||
TEST_F(AIWaterCrossingCalculatorTest, EnoughAttackersWithoutWaterCrossing_noLandPathReturnsFalse) {
|
||||
CoordsSet destinations(hexMap);
|
||||
destinations.Add(acrossDestination);
|
||||
|
||||
EXPECT_FALSE(HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
gameState,
|
||||
destinations,
|
||||
alCache->GetLandRegions(),
|
||||
apdCache,
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
},
|
||||
1));
|
||||
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
|
||||
}
|
||||
|
||||
TEST_F(AIWaterCrossingCalculatorTest,
|
||||
EnoughAttackersWithoutWaterCrossing_ignoresDefendersAndUndead) {
|
||||
unit0->mutate_player_id(1);
|
||||
unit0->mutable_location() = Coords(0, 4);
|
||||
unit1->mutable_battalion().mutate_type(
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD);
|
||||
unit1->mutable_location() = Coords(0, 5);
|
||||
|
||||
CoordsSet destinations(hexMap);
|
||||
destinations.Add(acrossDestination);
|
||||
|
||||
EXPECT_FALSE(HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
gameState,
|
||||
destinations,
|
||||
alCache->GetLandRegions(),
|
||||
apdCache,
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
},
|
||||
1));
|
||||
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
|
||||
}
|
||||
|
||||
TEST_F(AIWaterCrossingCalculatorTest, CanReach_sameSide_returnsTrue) {
|
||||
EXPECT_TRUE(CanReach(origin, sameSideDestination, hexMap, mapId, apdCache, heavyCavalry));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user