mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Precompute land regions for water crossing (#8777)
This commit is contained in:
@@ -40,7 +40,8 @@ auto AttackLocations::LocationsWithEnemyInRange(const Unit *unit) const -> Coord
|
||||
|
||||
AttackLocationsCache::AttackLocationsCache(const HexMap *hexMap, const SettingsGetter &settings)
|
||||
: rowCount(hexMap->row_count()),
|
||||
columnCount(hexMap->column_count()) {
|
||||
columnCount(hexMap->column_count()),
|
||||
landRegions(hexMap) {
|
||||
const int mageRange =
|
||||
std::max(settings.Backing().meteor_range(), settings.Backing().lightning_range());
|
||||
const auto fearRange = settings.Backing().fear_range();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
|
||||
@@ -151,6 +152,8 @@ struct alignas(128) AttackLocationsCache {
|
||||
private:
|
||||
const MapIndex rowCount;
|
||||
const MapIndex columnCount;
|
||||
// Immutable static-map analysis shared by every speculative state evaluated by this AI.
|
||||
const LandRegions landRegions;
|
||||
|
||||
// For each of these vectors, the index is the map index of the tile *that can be attacked*,
|
||||
// and the entry is the CoordsSet of the tiles that can attack that location.
|
||||
@@ -161,6 +164,7 @@ private:
|
||||
public:
|
||||
AttackLocationsCache(const HexMap* hexMap, const SettingsGetter& settings);
|
||||
|
||||
[[nodiscard]] auto GetLandRegions() const -> const LandRegions& { return landRegions; }
|
||||
auto CachedLocations(const Coords& singleTarget, bool lateGame) -> const AttackLocations&;
|
||||
auto CachedLocations(const CoordsSet& targets, bool lateGame) -> AttackLocations;
|
||||
auto CachedLocations(const std::vector<const Unit*>& enemies, bool lateGame) -> AttackLocations;
|
||||
|
||||
@@ -23,6 +23,7 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const LandRegions& landRegions,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy {
|
||||
uint32_t attackerNonUndeadUnitCount = 0;
|
||||
uint32_t attackerNonUndeadUnitNotRequiringWaterCrossingCount = 0;
|
||||
@@ -39,6 +40,7 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
gameState,
|
||||
player->player_id(),
|
||||
criticalTileCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
attackerUnitIdsRequiringWaterCrossing.insert(
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const LandRegions& landRegions,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "AIMinimumDistanceAndTarget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
@@ -36,6 +37,15 @@ auto CanReachWithDistances(
|
||||
return distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE;
|
||||
}
|
||||
|
||||
auto DefinitelyReachesAllWithoutWater(
|
||||
const LandRegions &landRegions,
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations) -> bool {
|
||||
return std::ranges::all_of(destinations, [&](const Coords &destination) {
|
||||
return landRegions.DefinitelyConnectedWithoutWater(origin, destination);
|
||||
});
|
||||
}
|
||||
|
||||
auto CanCreateWaterCrossing(
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> bool {
|
||||
@@ -55,8 +65,41 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId pid,
|
||||
const CoordsSet &destinations,
|
||||
const LandRegions &landRegions,
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
|
||||
std::vector<const net::eagle0::shardok::storage::fb::Unit *> unitsToCheck{};
|
||||
std::array<int8_t, 9> startingPositionIndices{};
|
||||
startingPositionIndices.fill(-1);
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
if (unit->player_id() != pid) continue;
|
||||
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
if (!DefinitelyReachesAllWithoutWater(landRegions, unit->location(), destinations)) {
|
||||
unitsToCheck.push_back(unit);
|
||||
}
|
||||
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
|
||||
if (startingPositionIndices[unit->starting_position_index()] == -1) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 0;
|
||||
const auto *startingPositions = gameState->hex_map()
|
||||
->attacker_starting_positions()
|
||||
->Get(unit->starting_position_index())
|
||||
->positions();
|
||||
for (const Coords *position : *startingPositions) {
|
||||
if (!DefinitelyReachesAllWithoutWater(landRegions, *position, destinations)) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startingPositionIndices[unit->starting_position_index()] == 1) {
|
||||
unitsToCheck.push_back(unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unitsToCheck.empty()) return {};
|
||||
|
||||
const HexMap *map = gameState->hex_map();
|
||||
MapId mapId = ActionPointDistancesCache::GetMapId(map);
|
||||
|
||||
@@ -90,11 +133,10 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
|
||||
vector<UnitId> unitIdsToReturn{};
|
||||
|
||||
// Set this to -1 for unchecked, 0 for does not need crossing, 1 for does need crossing
|
||||
std::vector<int> startingPositionIndices(9, -1);
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
if (unit->player_id() != pid) continue;
|
||||
|
||||
// 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.
|
||||
startingPositionIndices.fill(-1);
|
||||
for (const auto *unit : unitsToCheck) {
|
||||
const auto &battType = battalionTypeGetter(unit->battalion().type());
|
||||
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
@@ -319,6 +361,7 @@ auto WaterCrossingScore(
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom,
|
||||
const LandRegions &landRegions,
|
||||
const APDCache &apdCache) -> double {
|
||||
uint32_t castleClaimCount = 0;
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
@@ -348,6 +391,7 @@ auto WaterCrossingScore(
|
||||
gameState,
|
||||
playerId,
|
||||
castleCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return kNoRequiredCrossingScore;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
@@ -35,6 +36,7 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const CoordsSet& destinations,
|
||||
const LandRegions& landRegions,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
|
||||
|
||||
@@ -92,6 +94,7 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& castleCoords,
|
||||
const CoordsSet& startCrossingFrom,
|
||||
const LandRegions& landRegions,
|
||||
const APDCache& apdCache) -> double;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -44,6 +44,7 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
gameState,
|
||||
playerId,
|
||||
castleCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return startCrossingFrom;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
@@ -23,11 +24,16 @@ class AIWaterCrossingCommandChooser {
|
||||
private:
|
||||
const PlayerId playerId;
|
||||
const APDCache apdCache;
|
||||
const LandRegions &landRegions;
|
||||
|
||||
public:
|
||||
AIWaterCrossingCommandChooser(const PlayerId pid, APDCache apdCache)
|
||||
AIWaterCrossingCommandChooser(
|
||||
const PlayerId pid,
|
||||
APDCache apdCache,
|
||||
const LandRegions &landRegions)
|
||||
: playerId(pid),
|
||||
apdCache(std::move(apdCache)) {}
|
||||
apdCache(std::move(apdCache)),
|
||||
landRegions(landRegions) {}
|
||||
|
||||
[[nodiscard]] auto StartCrossingFrom(
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
|
||||
@@ -73,6 +73,7 @@ cc_library(
|
||||
],
|
||||
deps = [
|
||||
":ai_score_utilities",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:terrain",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
@@ -317,6 +318,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:hex_map_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:hex_map_cc_fbs",
|
||||
],
|
||||
@@ -334,6 +336,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ ShardokAIClient::ShardokAIClient(
|
||||
aiAlgorithmType(aiAlgorithmType),
|
||||
scoringCalculatorType(scoringCalculatorType),
|
||||
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
|
||||
waterCrossingCommandChooser(playerId, apdCache),
|
||||
waterCrossingCommandChooser(playerId, apdCache, alCache->GetLandRegions()),
|
||||
mctsConfig(mctsConfig) {
|
||||
// Pre-generate the most common cache entries for better performance
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
|
||||
@@ -302,6 +302,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
castleCoords,
|
||||
maxRounds,
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
battalionTypeGetter)
|
||||
: AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
playerId,
|
||||
|
||||
@@ -309,6 +309,7 @@ auto AbstractAIScoreCalculator::CalculateAttackerVictoryConditionScore(
|
||||
gameState,
|
||||
castleCoords,
|
||||
attackerStrategy.targetLocations,
|
||||
GetAlCache()->GetLandRegions(),
|
||||
GetApdCache());
|
||||
break;
|
||||
|
||||
|
||||
@@ -59,6 +59,20 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "land_regions",
|
||||
srcs = ["LandRegions.cpp"],
|
||||
hdrs = ["LandRegions.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":coordinates",
|
||||
":terrain",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:hex_map_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:terrain_cc_fbs",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "terrain",
|
||||
srcs = ["Terrain.cpp"],
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Copyright 2026 Dan Crosby
|
||||
//
|
||||
|
||||
#include "LandRegions.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Terrain.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
using TerrainType = net::eagle0::shardok::storage::fb::Terrain_::Type;
|
||||
|
||||
constexpr std::array<std::array<int, 2>, 6> kEvenRowNeighborOffsets = {
|
||||
std::array{-1, 0},
|
||||
std::array{0, 1},
|
||||
std::array{1, 0},
|
||||
std::array{1, -1},
|
||||
std::array{0, -1},
|
||||
std::array{-1, -1}};
|
||||
constexpr std::array<std::array<int, 2>, 6> kOddRowNeighborOffsets = {
|
||||
std::array{-1, 1},
|
||||
std::array{0, 1},
|
||||
std::array{1, 1},
|
||||
std::array{1, 0},
|
||||
std::array{0, -1},
|
||||
std::array{-1, 0}};
|
||||
|
||||
auto IsGuaranteedLand(const TerrainType type) -> bool {
|
||||
return !IsWater(type) && type != TerrainType::Type_MOUNTAIN;
|
||||
}
|
||||
|
||||
auto ToIndex(const int row, const int column, const int columnCount) -> int {
|
||||
return row * columnCount + column;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto ComputeLandRegions(
|
||||
const int rowCount,
|
||||
const int columnCount,
|
||||
const std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
|
||||
-> std::vector<LandRegionId> {
|
||||
const int tileCount = rowCount * columnCount;
|
||||
if (tileCount < 0 || terrain.size() != static_cast<size_t>(tileCount)) {
|
||||
throw std::invalid_argument("Land region terrain size does not match map dimensions");
|
||||
}
|
||||
|
||||
std::vector<LandRegionId> regions(static_cast<size_t>(tileCount), kNoLandRegion);
|
||||
std::vector<int> pending;
|
||||
pending.reserve(static_cast<size_t>(tileCount));
|
||||
|
||||
int nextRegion = 0;
|
||||
for (int startIndex = 0; startIndex < tileCount; ++startIndex) {
|
||||
if (regions[startIndex] != kNoLandRegion || !IsGuaranteedLand(terrain[startIndex].type())) {
|
||||
continue;
|
||||
}
|
||||
if (nextRegion > std::numeric_limits<LandRegionId>::max()) {
|
||||
throw std::overflow_error("Map has too many distinct land regions");
|
||||
}
|
||||
|
||||
regions[startIndex] = static_cast<LandRegionId>(nextRegion);
|
||||
pending.push_back(startIndex);
|
||||
while (!pending.empty()) {
|
||||
const int index = pending.back();
|
||||
pending.pop_back();
|
||||
|
||||
const int row = index / columnCount;
|
||||
const int column = index % columnCount;
|
||||
const auto& offsets = row % 2 == 0 ? kEvenRowNeighborOffsets : kOddRowNeighborOffsets;
|
||||
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);
|
||||
if (regions[neighborIndex] != kNoLandRegion ||
|
||||
!IsGuaranteedLand(terrain[neighborIndex].type())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
regions[neighborIndex] = static_cast<LandRegionId>(nextRegion);
|
||||
pending.push_back(neighborIndex);
|
||||
}
|
||||
}
|
||||
|
||||
++nextRegion;
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
LandRegions::LandRegions(const net::eagle0::shardok::storage::fb::HexMap* map)
|
||||
: rowCount(map->row_count()),
|
||||
columnCount(map->column_count()) {
|
||||
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);
|
||||
}
|
||||
|
||||
LandRegions::LandRegions(
|
||||
const int rowCount,
|
||||
const int columnCount,
|
||||
const std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
|
||||
: rowCount(rowCount),
|
||||
columnCount(columnCount),
|
||||
regions(ComputeLandRegions(rowCount, columnCount, terrain)) {}
|
||||
|
||||
auto LandRegions::RegionAt(const Coords& coords) const -> LandRegionId {
|
||||
if (coords.row() < 0 || coords.column() < 0 || coords.row() >= rowCount ||
|
||||
coords.column() >= columnCount) {
|
||||
return kNoLandRegion;
|
||||
}
|
||||
return regions[ToIndex(coords.row(), coords.column(), columnCount)];
|
||||
}
|
||||
|
||||
auto LandRegions::DefinitelyConnectedWithoutWater(const Coords& origin, const Coords& destination)
|
||||
const -> bool {
|
||||
const auto originRegion = RegionAt(origin);
|
||||
const auto destinationRegion = RegionAt(destination);
|
||||
return originRegion != kNoLandRegion && originRegion == destinationRegion;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// Copyright 2026 Dan Crosby
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
|
||||
#define EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/terrain.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using LandRegionId = int8_t;
|
||||
|
||||
constexpr LandRegionId kNoLandRegion = -1;
|
||||
|
||||
[[nodiscard]] auto ComputeLandRegions(
|
||||
int rowCount,
|
||||
int columnCount,
|
||||
std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
|
||||
-> std::vector<LandRegionId>;
|
||||
|
||||
class LandRegions {
|
||||
private:
|
||||
int rowCount;
|
||||
int columnCount;
|
||||
std::vector<LandRegionId> regions;
|
||||
|
||||
public:
|
||||
explicit LandRegions(const net::eagle0::shardok::storage::fb::HexMap* map);
|
||||
|
||||
LandRegions(
|
||||
int rowCount,
|
||||
int columnCount,
|
||||
std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain);
|
||||
|
||||
// A true result is conclusive: the two locations have a static path that uses neither water nor
|
||||
// mountains. False is inconclusive because dynamic bridges, ice, or unit capabilities may
|
||||
// connect different static regions.
|
||||
[[nodiscard]] auto DefinitelyConnectedWithoutWater(
|
||||
const Coords& origin,
|
||||
const Coords& destination) const -> bool;
|
||||
|
||||
[[nodiscard]] auto RegionAt(const Coords& coords) const -> LandRegionId;
|
||||
[[nodiscard]] auto size() const -> size_t { return regions.size(); }
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
|
||||
@@ -59,7 +59,10 @@ class AIAttackerStrategySelectorTest : public ::testing::Test {
|
||||
|
||||
apdCache = std::make_shared<ActionPointDistancesCache>();
|
||||
alCache = std::make_unique<AttackLocationsCache>(hexMap, GetGameSettingsGetter());
|
||||
waterCrossingCommandChooser = std::make_unique<AIWaterCrossingCommandChooser>(0, apdCache);
|
||||
waterCrossingCommandChooser = std::make_unique<AIWaterCrossingCommandChooser>(
|
||||
0,
|
||||
apdCache,
|
||||
alCache->GetLandRegions());
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -92,6 +92,7 @@ TEST_F(AIStrategySelectorTest, NotEnoughAttackerUnits_returnsScatterStrategy) {
|
||||
criticalTileCoords,
|
||||
GetGameSettings()->GetGetter().Backing().max_rounds(),
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
})
|
||||
@@ -117,6 +118,7 @@ TEST_F(AIStrategySelectorTest, EnoughAttackerUnitsOnlyIncludingUndead_returnsSca
|
||||
criticalTileCoords,
|
||||
GetGameSettings()->GetGetter().Backing().max_rounds(),
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
})
|
||||
@@ -142,6 +144,7 @@ TEST_F(AIStrategySelectorTest, EnoughAttackerUnits_returnsHoldCastlesStrategy) {
|
||||
criticalTileCoords,
|
||||
GetGameSettings()->GetGetter().Backing().max_rounds(),
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
})
|
||||
@@ -171,6 +174,7 @@ TEST_F(AIStrategySelectorTest,
|
||||
criticalTileCoords,
|
||||
GetGameSettings()->GetGetter().Backing().max_rounds(),
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
})
|
||||
@@ -199,6 +203,7 @@ TEST_F(AIStrategySelectorTest,
|
||||
criticalTileCoords,
|
||||
GetGameSettings()->GetGetter().Backing().max_rounds(),
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
})
|
||||
@@ -228,6 +233,7 @@ TEST_F(AIStrategySelectorTest,
|
||||
criticalTileCoords,
|
||||
GetGameSettings()->GetGetter().Backing().max_rounds(),
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
})
|
||||
@@ -257,6 +263,7 @@ TEST_F(AIStrategySelectorTest,
|
||||
criticalTileCoords,
|
||||
GetGameSettings()->GetGetter().Backing().max_rounds(),
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
})
|
||||
@@ -287,6 +294,7 @@ TEST_F(AIStrategySelectorTest,
|
||||
criticalTileCoords,
|
||||
GetGameSettings()->GetGetter().Backing().max_rounds(),
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
})
|
||||
|
||||
@@ -33,7 +33,21 @@ class AIWaterCrossingCalculatorTest : public ::testing::Test {
|
||||
|
||||
const auto unitsOffset = fbb.CreateVectorOfSortedStructs(&unitsVec);
|
||||
|
||||
const auto hexMapOffset = AddHexMap(fbb, BASIC_MAP_FB);
|
||||
auto terrainTypes = TERRAIN_VALUES_FB;
|
||||
terrainTypes[0 * 6 + 3] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
|
||||
terrainTypes[1 * 6 + 3] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
|
||||
terrainTypes[2 * 6 + 3] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
|
||||
terrainTypes[2 * 6 + 4] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
|
||||
terrainTypes[2 * 6 + 5] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
|
||||
terrainTypes[3 * 6 + 5] = net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER;
|
||||
const auto hexMapOffset = AddHexMap(
|
||||
fbb,
|
||||
6,
|
||||
6,
|
||||
terrainTypes,
|
||||
EMPTY_STARTING_POSITIONS,
|
||||
EMPTY_POSITIONS_LIST,
|
||||
SUNNY_MONTHLY_WEATHERS_FB);
|
||||
|
||||
net::eagle0::shardok::storage::fb::GameStateBuilder gsb(fbb);
|
||||
gsb.add_units(unitsOffset);
|
||||
@@ -55,19 +69,6 @@ class AIWaterCrossingCalculatorTest : public ::testing::Test {
|
||||
// P P P P P P
|
||||
// * P P P P P
|
||||
|
||||
MutableTerrain(hexMap, Coords(0, 3))
|
||||
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
|
||||
MutableTerrain(hexMap, Coords(1, 3))
|
||||
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
|
||||
MutableTerrain(hexMap, Coords(2, 3))
|
||||
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
|
||||
MutableTerrain(hexMap, Coords(2, 4))
|
||||
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
|
||||
MutableTerrain(hexMap, Coords(2, 5))
|
||||
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
|
||||
MutableTerrain(hexMap, Coords(3, 5))
|
||||
->mutate_type(net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER);
|
||||
|
||||
mapId = ActionPointDistancesCache::GetMapId(hexMap);
|
||||
|
||||
alCache = std::make_unique<AttackLocationsCache>(hexMap, GetGameSettingsGetter());
|
||||
@@ -103,10 +104,12 @@ TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_allSameSide_re
|
||||
gameState,
|
||||
0,
|
||||
destinations,
|
||||
alCache->GetLandRegions(),
|
||||
apdCache,
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
}));
|
||||
EXPECT_EQ(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
|
||||
}
|
||||
|
||||
TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_allOtherSide_returnsTwo) {
|
||||
@@ -123,10 +126,12 @@ TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_allOtherSide_r
|
||||
gameState,
|
||||
0,
|
||||
destinations,
|
||||
alCache->GetLandRegions(),
|
||||
apdCache,
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
}));
|
||||
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
|
||||
}
|
||||
|
||||
TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_oneOtherSide_returnsOne) {
|
||||
@@ -143,6 +148,7 @@ TEST_F(AIWaterCrossingCalculatorTest, UnitsRequiringWaterCrossing_oneOtherSide_r
|
||||
gameState,
|
||||
0,
|
||||
destinations,
|
||||
alCache->GetLandRegions(),
|
||||
apdCache,
|
||||
[](BattalionTypeId typeId) {
|
||||
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
|
||||
@@ -161,6 +167,7 @@ TEST_F(AIWaterCrossingCalculatorTest,
|
||||
gameState,
|
||||
0,
|
||||
destinations,
|
||||
alCache->GetLandRegions(),
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
|
||||
@@ -174,6 +181,7 @@ TEST_F(AIWaterCrossingCalculatorTest,
|
||||
gameState,
|
||||
0,
|
||||
destinations,
|
||||
alCache->GetLandRegions(),
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
EXPECT_EQ(beforeFire, afterFire);
|
||||
@@ -181,6 +189,52 @@ TEST_F(AIWaterCrossingCalculatorTest,
|
||||
EXPECT_TRUE(fireTerrain->modifier().fire().present());
|
||||
}
|
||||
|
||||
TEST_F(AIWaterCrossingCalculatorTest,
|
||||
UnitsRequiringWaterCrossing_sameRegionWithFire_skipsDistances) {
|
||||
unit0->mutable_location() = Coords(0, 4);
|
||||
unit1->mutable_location() = Coords(0, 5);
|
||||
MutableTerrain(hexMap, Coords(0, 4))->mutable_modifier().mutable_fire().mutate_present(true);
|
||||
|
||||
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(0, ActionPointDistancesCache::GetThreadLocalCacheSize());
|
||||
}
|
||||
|
||||
TEST_F(AIWaterCrossingCalculatorTest,
|
||||
UnitsRequiringWaterCrossing_bridgeBetweenRegions_fallsBackToDistances) {
|
||||
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);
|
||||
|
||||
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, CanReach_sameSide_returnsTrue) {
|
||||
EXPECT_TRUE(CanReach(origin, sameSideDestination, hexMap, mapId, apdCache, heavyCavalry));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,18 @@ cc_test(
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "land_regions_test",
|
||||
srcs = ["LandRegions_test.cpp"],
|
||||
copts = TEST_COPTS,
|
||||
linkstatic = True,
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
"@googletest//:gtest",
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "tile_modifier_test",
|
||||
srcs = ["TileModifier_test.cpp"],
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// Copyright 2026 Dan Crosby
|
||||
//
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
using Terrain = net::eagle0::shardok::storage::fb::Terrain;
|
||||
using TerrainType = net::eagle0::shardok::storage::fb::Terrain_::Type;
|
||||
using TileModifier = net::eagle0::shardok::storage::fb::TileModifier;
|
||||
|
||||
auto TerrainGrid(const int size, const TerrainType type = TerrainType::Type_PLAINS)
|
||||
-> std::vector<Terrain> {
|
||||
return std::vector<Terrain>(static_cast<size_t>(size), Terrain(type, TileModifier()));
|
||||
}
|
||||
|
||||
TEST(LandRegionsTest, ConnectedLandSharesOneRegion) {
|
||||
const auto terrain = TerrainGrid(9);
|
||||
|
||||
const LandRegions regions(3, 3, terrain);
|
||||
|
||||
ASSERT_EQ(9, regions.size());
|
||||
EXPECT_NE(kNoLandRegion, regions.RegionAt(Coords(0, 0)));
|
||||
EXPECT_TRUE(regions.DefinitelyConnectedWithoutWater(Coords(0, 0), Coords(2, 2)));
|
||||
}
|
||||
|
||||
TEST(LandRegionsTest, WaterSeparatesRegionsAndHasNoRegion) {
|
||||
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 regions(3, 3, terrain);
|
||||
|
||||
EXPECT_EQ(kNoLandRegion, regions.RegionAt(Coords(0, 1)));
|
||||
EXPECT_EQ(kNoLandRegion, regions.RegionAt(Coords(1, 1)));
|
||||
EXPECT_EQ(kNoLandRegion, regions.RegionAt(Coords(2, 1)));
|
||||
EXPECT_NE(kNoLandRegion, regions.RegionAt(Coords(1, 0)));
|
||||
EXPECT_NE(kNoLandRegion, regions.RegionAt(Coords(1, 2)));
|
||||
EXPECT_FALSE(regions.DefinitelyConnectedWithoutWater(Coords(1, 0), Coords(1, 2)));
|
||||
}
|
||||
|
||||
TEST(LandRegionsTest, MountainsAlsoSeparateGuaranteedLandRegions) {
|
||||
auto terrain = TerrainGrid(9);
|
||||
terrain[1].mutate_type(TerrainType::Type_MOUNTAIN);
|
||||
terrain[4].mutate_type(TerrainType::Type_MOUNTAIN);
|
||||
terrain[7].mutate_type(TerrainType::Type_MOUNTAIN);
|
||||
|
||||
const LandRegions regions(3, 3, terrain);
|
||||
|
||||
EXPECT_EQ(kNoLandRegion, regions.RegionAt(Coords(1, 1)));
|
||||
EXPECT_FALSE(regions.DefinitelyConnectedWithoutWater(Coords(1, 0), Coords(1, 2)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace shardok
|
||||
Reference in New Issue
Block a user