mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Account for bridges in AI land regions (#8785)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user