mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 02:15:43 +00:00
Compare commits
38
Commits
tp2
...
occupancy-lookup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d4470717 | ||
|
|
a6d0387235 | ||
|
|
1b65ee0b24 | ||
|
|
f36b869044 | ||
|
|
461a0794c3 | ||
|
|
89d0984342 | ||
|
|
a6258e64f2 | ||
|
|
21297a51ca | ||
|
|
c219637b9f | ||
|
|
b8915f460e | ||
|
|
d038247a41 | ||
|
|
3e1b8447f4 | ||
|
|
13c3ae1c2f | ||
|
|
3fb1ce1232 | ||
|
|
be61700b2c | ||
|
|
e9ed1c1ee5 | ||
|
|
75bb7774ef | ||
|
|
98956d1986 | ||
|
|
de9ef4ed59 | ||
|
|
40c5c0ecde | ||
|
|
b0b6bdda06 | ||
|
|
bfb78c2b85 | ||
|
|
bc3c14bde7 | ||
|
|
353fb08592 | ||
|
|
74c8ca80bc | ||
|
|
f668328983 | ||
|
|
9fa948d63f | ||
|
|
86a0212062 | ||
|
|
f910661c32 | ||
|
|
cd28e2dfcf | ||
|
|
9bccccc3fb | ||
|
|
5603d57e76 | ||
|
|
359eceff97 | ||
|
|
acf1af5fcc | ||
|
|
f4e35bf4f0 | ||
|
|
a3383f8871 | ||
|
|
366d4790cd | ||
|
|
0dc8b75906 |
@@ -2,9 +2,9 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This document details the implementation of an embedded occupants vector in the GameState flatbuffer to replace O(n)
|
||||
unit iteration with O(1) position lookups. It also catalogs all Occupant() and KnownEnemyOccupant() calls that could not
|
||||
be converted to use the new optimized methods.
|
||||
This document details the implementation of an embedded occupants vector in the GameState flatbuffer and OccupancyLookup
|
||||
class to replace O(n) unit iteration with O(1) position lookups. It catalogs all Occupant() and KnownEnemyOccupant()
|
||||
calls and their conversion status.
|
||||
|
||||
## Completed Conversions
|
||||
|
||||
@@ -65,74 +65,143 @@ be converted to use the new optimized methods.
|
||||
|
||||
### KnownEnemyOccupant() Conversions
|
||||
|
||||
**Result: 0 conversions possible**
|
||||
**Result: 8 conversions completed, all CommandFactory helper methods converted**
|
||||
|
||||
All KnownEnemyOccupant() calls are in command factory methods that receive decomposed game state parameters (Units*,
|
||||
vector<PlayerId>, etc.) rather than complete GameStateW objects.
|
||||
#### Successfully Converted (8 conversions)
|
||||
|
||||
1. **MeleeCommandFactory.cpp**:
|
||||
- Line 58: `KnownEnemyOccupant(unit->player_id(), units, allyPids, adjCoords)` → `occupancyLookup.GetKnownEnemyOccupant(unit->player_id(), allyPids, adjCoords)`
|
||||
|
||||
2. **RepairCommandFactory.cpp**:
|
||||
- Line 44: `KnownEnemyOccupant(playerId, units, allyPids, coords)` → `occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, coords)`
|
||||
|
||||
3. **FearCommandFactory.cpp**:
|
||||
- Line 35: `KnownEnemyOccupant(playerId, units, allyPids, coords)` → `occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, coords)`
|
||||
|
||||
4. **LightningBoltCommandFactory.cpp**:
|
||||
- Line 54: `KnownEnemyOccupant(playerId, units, allyPids, coords)` → `occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, coords)`
|
||||
|
||||
5. **ReduceCommandFactory.cpp**:
|
||||
- Line 48: `KnownEnemyOccupant(playerId, units, allyPids, coords)` → `occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, coords)`
|
||||
|
||||
6. **ChallengeDuelCommandFactory.cpp**:
|
||||
- Line 35: `KnownEnemyOccupant(playerId, units, allyPids, coords)` → `occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, coords)`
|
||||
|
||||
7. **HideCommandFactory.cpp**:
|
||||
- Line 45: `KnownEnemyOccupant(playerId, units, allyPids, coords)` → `occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, coords)`
|
||||
|
||||
8. **ArcheryCommandFactory.cpp**:
|
||||
- Line 89: `KnownEnemyOccupant(playerId, units, allyPids, coords)` → `occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, coords)`
|
||||
|
||||
#### Architectural Improvements
|
||||
|
||||
All CommandFactory helper methods have been successfully converted to use the `OccupancyLookup` class. The CommandFactory base class includes `OccupancyLookup` in the `CommandParams` struct, and all command factories now use O(1) lookups through their helper methods.
|
||||
|
||||
## Remaining Unconverted Calls
|
||||
|
||||
### Occupant() Calls That Cannot Be Converted
|
||||
### ✅ MAJOR LEGACY FUNCTIONS ELIMINATED
|
||||
|
||||
#### 1. PerformUndeadCommandsAction.cpp (2 calls - No GameStateW access)
|
||||
All major legacy functions using O(n) lookups have been successfully removed:
|
||||
|
||||
- **Line 69**: `Occupant(units, FromCoordsProto(possibleAttackCommandProto.target()))`
|
||||
- **Line 99**: `Occupant(units, adjCoords)`
|
||||
- **Reason**: These calls are in the `ChooseUndeadCommand()` function which only receives `const Units* units`
|
||||
parameter, not a full GameStateW.
|
||||
- **Location**: `src/main/cpp/net/eagle0/shardok/library/actions/PerformUndeadCommandsAction.cpp`
|
||||
1. **✅ REMOVED**: `MakeIceAdjustmentAction()` legacy version - eliminated function with `Occupant(units, coords)` call
|
||||
2. **✅ REMOVED**: `MakeIceAndSnowAdjustmentActions()` legacy version - now delegates to GameStateW overload
|
||||
3. **✅ REMOVED**: `IsInEnemyZoc()` legacy version - eliminated function with `KnownEnemyOccupant()` call
|
||||
4. **✅ REMOVED**: `AttackOrientationForAttack()` legacy version - eliminated function with `Occupant(units, destination)` call, all tests updated
|
||||
5. **✅ CREATED**: `MaybeOccupantInDirection()` OccupancyLookup overload - supports AttackOrientationForAttack optimization
|
||||
|
||||
#### 2. AICommandFilter.cpp (1 call - Raw pointer access)
|
||||
### Remaining Legacy Calls (All Low-Impact)
|
||||
|
||||
- **Line 399**: `KnownEnemyOccupant(pid, units, allyPids, fireLocation)` (in EXTINGUISH_FIRE_COMMAND case)
|
||||
- **Reason**: Method receives `const GameState* gameState` parameter, not GameStateW. Has TODO comment noting this
|
||||
limitation.
|
||||
- **Location**: `src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.cpp`
|
||||
#### 1. Test Files (2 calls - Test infrastructure only)
|
||||
|
||||
#### 3. UpdateGameStatusAction.cpp - Member Variable Usage
|
||||
- **HexMapUtils_test.cpp:76**: `KnownEnemyOccupant(actingPlayerId, gameState->units(), alliedPids, ownUnit->location())`
|
||||
- **HexMapUtils_test.cpp:82, 92, 102**: Additional test calls to `KnownEnemyOccupant()`
|
||||
- **Reason**: Test files exercising legacy utility function APIs. Not performance-critical.
|
||||
- **Location**: `src/test/cpp/net/eagle0/shardok/library/util/HexMapUtils_test.cpp`
|
||||
|
||||
- **Various calls**: Uses `gameState` member variable of type `const GameState*`
|
||||
- **Reason**: Class was designed to take raw GameState pointer in constructor, though InternalExecute method has
|
||||
GameStateW access.
|
||||
- **Location**: `src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.cpp`
|
||||
#### 2. HexMapUtils Internal Implementation (6 calls - Supporting legacy API)
|
||||
|
||||
#### 4. IceAndSnowAdjustmentActionFactory.cpp (1 call - Factory pattern)
|
||||
- **HexMapUtils.cpp:82**: `KnownEnemyOccupant(player, units, allyPids, adjCoords)` in `HasForestAccess()` legacy function
|
||||
- **HexMapUtils.cpp:536**: `Occupant(units, maybeTile.value())` in `MaybeOccupantInDirection()` legacy function
|
||||
- **HexMapUtils.cpp:686**: `Occupant(units, coords)` in legacy utility function
|
||||
- **HexMapUtils.cpp:707**: `KnownEnemyOccupant(playerId, units, allyPids, adjCoords)` in `KnownAdjacentEnemies()` legacy function
|
||||
- **HexMapUtils.cpp:737**: `Occupant(units, coords)` in `KnownEnemyOccupant()` legacy function
|
||||
- **Reason**: Implementation of legacy utility functions that maintain backward compatibility. OccupancyLookup overloads exist for performance-critical paths.
|
||||
|
||||
- **Line 42**: `Occupant(units, coords)`
|
||||
- **Reason**: Factory method receives individual parameters, not GameStateW.
|
||||
- **Location**: `src/main/cpp/net/eagle0/shardok/library/action_factories/IceAndSnowAdjustmentActionFactory.cpp`
|
||||
#### 3. MoveCommandFactory Internal Implementation (1 call - Complex call chain)
|
||||
|
||||
- **MoveCommandFactory.cpp:133**: `KnownOccupant(playerId, units, allyPids, adjacentTile)` in `UnoccupiedAdjacentCoords()` legacy function
|
||||
- **Reason**: Deep in MoveCommandFactory call chain where legacy API is used. The main `AddAvailableMoveCommands()` has been updated to support OccupancyLookup, but internal helper functions still use legacy patterns.
|
||||
- **Impact**: Low - MoveCommand execution itself uses GameStateW.GetOccupant() for O(1) lookups during actual execution.
|
||||
|
||||
#### 4. Function Definitions in Headers (Multiple - Template definitions)
|
||||
|
||||
- **HexMapUtils.hpp**: Multiple template function definitions for different container types
|
||||
- Lines 111, 122, 165, 178, 188, 197, 207: Various `Occupant()` template specializations
|
||||
- Lines 215, 231, 238: `FriendlyOccupant()` template specializations
|
||||
- Lines 254, 261, 278, 295: `KnownEnemyOccupant()` and `KnownOccupant()` template specializations
|
||||
- **Reason**: Template function definitions that provide generic occupancy checking for different container types. These support the utility function ecosystem and maintain API compatibility.
|
||||
|
||||
### Performance Impact Assessment
|
||||
|
||||
**✅ NEGLIGIBLE IMPACT**: All remaining legacy calls are in:
|
||||
- Test infrastructure (not runtime performance)
|
||||
- Legacy utility function implementations (OccupancyLookup overloads exist for hot paths)
|
||||
- Template definitions (provide generic API support)
|
||||
- Deep internal helper functions (main execution paths optimized)
|
||||
|
||||
**✅ HOT PATHS FULLY OPTIMIZED**: All performance-critical paths now use O(1) lookups:
|
||||
- CommandFactory classes use OccupancyLookup via CommandParams
|
||||
- Command execution uses GameStateW.GetOccupant()
|
||||
- Action execution uses GameStateW.GetOccupant()
|
||||
- AI evaluation uses optimized utility function overloads
|
||||
|
||||
### KnownEnemyOccupant() Calls That Cannot Be Converted
|
||||
|
||||
#### Command Factory Methods (8 calls - No GameStateW access)
|
||||
#### ~~Command Factory Helper Methods (7 calls - CONVERTED)~~
|
||||
|
||||
1. **RepairCommandFactory.cpp** - Line 44
|
||||
2. **FearCommandFactory.cpp** - Line 35
|
||||
3. **LightningBoltCommandFactory.cpp** - Line 54
|
||||
4. **ReduceCommandFactory.cpp** - Line 48
|
||||
5. **ChallengeDuelCommandFactory.cpp** - Line 35
|
||||
6. **HideCommandFactory.cpp** - Line 45
|
||||
7. **MeleeCommandFactory.cpp** - Line 58
|
||||
8. **ArcheryCommandFactory.cpp** - Line 89
|
||||
**✅ All 7 CommandFactory helper methods have been successfully converted to use OccupancyLookup:**
|
||||
|
||||
**Common Reason**: All command factory methods follow a pattern where they receive individual game state components (
|
||||
`Units* units`, `vector<PlayerId> allyPids`, etc.) rather than a complete GameStateW object.
|
||||
1. **RepairCommandFactory.cpp** - ✅ CONVERTED: Helper method now accepts `OccupancyLookup` parameter
|
||||
2. **FearCommandFactory.cpp** - ✅ CONVERTED: Helper method now accepts `OccupancyLookup` parameter
|
||||
3. **LightningBoltCommandFactory.cpp** - ✅ CONVERTED: Helper method now accepts `OccupancyLookup` parameter
|
||||
4. **ReduceCommandFactory.cpp** - ✅ CONVERTED: Helper method now accepts `OccupancyLookup` parameter
|
||||
5. **ChallengeDuelCommandFactory.cpp** - ✅ CONVERTED: Helper method now accepts `OccupancyLookup` parameter
|
||||
6. **HideCommandFactory.cpp** - ✅ CONVERTED: Helper method now accepts `OccupancyLookup` parameter
|
||||
7. **ArcheryCommandFactory.cpp** - ✅ CONVERTED: Helper method now accepts `OccupancyLookup` parameter
|
||||
|
||||
#### Utility Functions (3 calls - Utility function parameters)
|
||||
**Status**: All helper methods have been refactored to accept `const OccupancyLookup& occupancyLookup` parameter and use `occupancyLookup.GetKnownEnemyOccupant()` instead of the old decomposed parameter approach.
|
||||
|
||||
1. **HexMapUtils.cpp** - Lines 81, 670
|
||||
2. **ZoneOfControlCalculator.cpp** - Line 143
|
||||
#### ~~Utility Functions (3 calls - CONVERTED WITH OVERLOADS)~~
|
||||
|
||||
**Reason**: These are utility functions that take decomposed parameters for reusability across different contexts.
|
||||
**✅ All 3 utility functions have been given OccupancyLookup overloads:**
|
||||
|
||||
1. **HexMapUtils.cpp** - ✅ CONVERTED: `HasForestAccess` overload created accepting `OccupancyLookup`
|
||||
2. **HexMapUtils.cpp** - ✅ CONVERTED: `KnownAdjacentEnemies` overload created accepting `OccupancyLookup`
|
||||
3. **ZoneOfControlCalculator.cpp** - ✅ CONVERTED: `IsInEnemyZoc` overload created accepting `OccupancyLookup`
|
||||
4. **ZoneOfControlCalculator.cpp** - ✅ CONVERTED: Legacy `AttackOrientationForAttack` function removed, tests updated to use OccupancyLookup overload
|
||||
|
||||
**Status**: Overloaded versions of all utility functions have been created. Call sites using CommandFactory classes or Commands with access to GameStateW/OccupancyLookup have been updated to use the O(1) overloads. The original versions remain for legacy usage.
|
||||
|
||||
**Updated Call Sites**:
|
||||
- ✅ **BuildBridgeCommandFactory**: Updated to use `HasForestAccess` overload
|
||||
- ✅ **ReduceCommand**: Updated to use `HasForestAccess` overload
|
||||
- ✅ **ChargeCommandFactory**: Updated to use `IsInEnemyZoc` overload
|
||||
- ✅ **HideCommandFactory**: Updated to use `IsInEnemyZoc` overload
|
||||
- ✅ **MoveCommand**: Updated to use `KnownAdjacentEnemies` overload (2 call sites converted)
|
||||
|
||||
**All eligible call sites have been converted to use O(1) OccupancyLookup overloads.**
|
||||
|
||||
## Performance Impact
|
||||
|
||||
### Achieved Improvements
|
||||
|
||||
- **16 Occupant() calls** converted from O(n) iteration to O(1) lookup
|
||||
- **16 Occupant() calls** converted from O(n) iteration to O(1) lookup using `GameStateW.GetOccupant()`
|
||||
- **8 KnownEnemyOccupant() calls** converted to O(1) lookup using `OccupancyLookup.GetKnownEnemyOccupant()`
|
||||
- **All CommandFactory helper methods** successfully converted to use OccupancyLookup pattern
|
||||
- **Architectural foundation** established with `OccupancyLookup` class and `CommandParams` integration
|
||||
- Eliminated cache invalidation issues with thread-local approach
|
||||
- Automatic copying of occupants vector with GameState copies
|
||||
- **Estimated Performance Gain**: 2-5% reduction in AI search time for typical game states
|
||||
- **Measured Performance Gain**: 4.6% improvement in AI search depth (182 vs 174 commands evaluated at depth 3)
|
||||
|
||||
### Trade-offs
|
||||
|
||||
@@ -157,12 +226,23 @@ vector<PlayerId>, etc.) rather than complete GameStateW objects.
|
||||
|
||||
## Recommendations for Future Work
|
||||
|
||||
### Potential Additional Conversions
|
||||
### High-Impact Potential Conversions
|
||||
|
||||
1. **Refactor command factories** to accept GameStateW instead of decomposed parameters
|
||||
2. **Update ShardokEngine** to use GameStateW internally where possible
|
||||
3. **Create GameStateW constructors** from raw GameState* to enable more conversions
|
||||
4. **Modernize legacy classes** to use GameStateW member variables
|
||||
1. **~~Command Factory Helper Methods~~ ✅ COMPLETED**: All 7 CommandFactory helper methods have been successfully converted:
|
||||
- ✅ Modified helper method signatures to include `const OccupancyLookup& occupancyLookup`
|
||||
- ✅ Updated main `AddAvailableCommands` methods to pass `params.occupancyLookup`
|
||||
- ✅ Converted all `KnownEnemyOccupant(playerId, units, allyPids, coords)` to `occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, coords)`
|
||||
|
||||
2. **~~Utility Function Overloads~~ ✅ COMPLETED**: Created OccupancyLookup-aware versions of key utility functions:
|
||||
- ✅ `HexMapUtils::HasForestAccess()` overload accepting `OccupancyLookup`
|
||||
- ✅ `HexMapUtils::KnownAdjacentEnemies()` overload accepting `OccupancyLookup`
|
||||
- ✅ `ZoneOfControlCalculator::IsInEnemyZoc()` overload accepting `OccupancyLookup`
|
||||
|
||||
### Lower Priority Conversions
|
||||
|
||||
1. **Update ShardokEngine** to use GameStateW internally where possible
|
||||
2. **Create GameStateW constructors** from raw GameState* to enable more conversions
|
||||
3. **Modernize legacy classes** to use GameStateW member variables
|
||||
|
||||
### Copy-on-Write Consideration
|
||||
|
||||
@@ -174,33 +254,57 @@ search. This could provide additional performance benefits by eliminating unnece
|
||||
### Core Changes Made
|
||||
|
||||
1. **game_state.fbs**: Added `occupants:[int16];` field
|
||||
2. **GameStateW.cpp**: Implemented GetOccupant() and UpdateOccupant() methods
|
||||
3. **GameStateCopier.cpp**: Populates occupants vector during GameState creation
|
||||
4. **ActionResultApplier.cpp**: Maintains occupants vector during unit movement
|
||||
2. **GameStateW.cpp**: Implemented `GetOccupant()`, `GetKnownEnemyOccupant()`, and `UpdateOccupant()` methods
|
||||
3. **GameStateW.hpp**: Added `OccupancyLookup` class for efficient occupancy checking via command parameters
|
||||
4. **CommandFactory.hpp**: Added `OccupancyLookup& occupancyLookup` to `CommandParams` struct
|
||||
5. **GameStateCopier.cpp**: Populates occupants vector during GameState creation
|
||||
6. **ActionResultApplier.cpp**: Maintains occupants vector during unit movement
|
||||
|
||||
### Key Method Signatures
|
||||
|
||||
```cpp
|
||||
// O(1) occupant lookup
|
||||
// GameStateW O(1) occupant lookup
|
||||
auto GameStateW::GetOccupant(const Coords& coords) const -> const Unit*;
|
||||
|
||||
// O(1) enemy occupant lookup
|
||||
// GameStateW O(1) enemy occupant lookup
|
||||
auto GameStateW::GetKnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> const Unit*;
|
||||
|
||||
// OccupancyLookup class for command parameters
|
||||
class OccupancyLookup {
|
||||
auto GetOccupant(const Coords& coords) const -> const Unit*;
|
||||
auto GetKnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> const Unit*;
|
||||
};
|
||||
|
||||
// Incremental occupants vector maintenance
|
||||
void GameStateW::UpdateOccupant(
|
||||
UnitId unitId,
|
||||
const Coords& oldCoords,
|
||||
const Coords& newCoords);
|
||||
|
||||
// Command factory integration
|
||||
struct CommandParams {
|
||||
// ... other fields ...
|
||||
const OccupancyLookup& occupancyLookup;
|
||||
};
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
The occupants vector optimization successfully converted 12 high-frequency Occupant() calls to O(1) lookups while
|
||||
maintaining correctness through automatic copying and incremental updates. The remaining 15+ unconverted calls are
|
||||
primarily in architectural layers (command factories, utilities) that would require broader refactoring to convert. The
|
||||
performance improvement achieved represents a solid foundation that could be extended with future architectural
|
||||
modernization.
|
||||
The occupants vector optimization successfully converted **16 Occupant() calls** and **8 KnownEnemyOccupant() calls**
|
||||
to O(1) lookups, achieving a **4.6% improvement in AI search performance**. The implementation includes:
|
||||
|
||||
- **Immediate benefits**: 24 high-frequency lookups converted from O(n) to O(1)
|
||||
- **Architectural foundation**: `OccupancyLookup` class and `CommandParams` integration enable future conversions
|
||||
- **Complete CommandFactory conversion**: All 7 CommandFactory helper methods successfully converted
|
||||
- **Measured performance**: 182 vs 174 commands evaluated at depth 3 in AI performance tests
|
||||
|
||||
**All utility function calls have been successfully converted to use O(1) OccupancyLookup overloads where GameStateW access is available.**
|
||||
|
||||
This optimization provides both immediate performance benefits and establishes the infrastructure for additional
|
||||
conversions through targeted refactoring of helper methods and utility functions.
|
||||
@@ -8,6 +8,8 @@ namespace shardok {
|
||||
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
|
||||
constexpr double kDefaultMorale = 50.0;
|
||||
|
||||
auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) -> Battalion {
|
||||
Battalion shardokBattalion{};
|
||||
|
||||
@@ -15,7 +17,7 @@ auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) ->
|
||||
shardokBattalion.mutate_size(battalion.size());
|
||||
shardokBattalion.mutate_type(
|
||||
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(battalion.type()));
|
||||
shardokBattalion.mutate_morale(battalion.morale());
|
||||
shardokBattalion.mutate_morale(kDefaultMorale);
|
||||
shardokBattalion.mutate_armament(battalion.armament());
|
||||
shardokBattalion.mutate_training(battalion.training());
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIFleeDecisionCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
@@ -11,11 +12,30 @@ namespace shardok {
|
||||
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
constexpr double MAXIMUM_RATIO_FOR_ATTACKER_TO_FLEE = 0.50;
|
||||
// Combat success threshold below which we should consider fleeing
|
||||
// This replaces the simple troop ratio check with sophisticated probability estimation
|
||||
constexpr double FLEE_CONSIDERATION_THRESHOLD = 0.25;
|
||||
|
||||
// Create a 2D grid of unit positions for spatial analysis
|
||||
static inline auto Occupants(
|
||||
const flatbuffers::Vector<const Unit*>& units,
|
||||
const int rowCount,
|
||||
const int columnCount) -> vector<const Unit*> {
|
||||
vector<const Unit*> positions(rowCount * columnCount);
|
||||
|
||||
for (const auto& unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
const int index = unit->location().row() * columnCount + unit->location().column();
|
||||
positions[index] = unit;
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
const PlayerId attackerPid,
|
||||
const net::eagle0::shardok::storage::fb::GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
@@ -24,8 +44,6 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
const vector<CommandProto>& availableCommands) -> AIStrategy {
|
||||
uint32_t attackerUnitCount = 0;
|
||||
int defenderOccupiedCriticalTileCount = 0;
|
||||
int attackerTroops = 0;
|
||||
int defenderTroops = 0;
|
||||
bool canFlee = false;
|
||||
|
||||
vector<const Unit*> attackerUnits{};
|
||||
@@ -40,8 +58,6 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
if (pi != nullptr) {
|
||||
if (pi->is_defender()) {
|
||||
if (unit->location().row() >= 0) {
|
||||
defenderTroops += unit->battalion().size();
|
||||
|
||||
if (criticalTileCoords.Contains(unit->location())) {
|
||||
++defenderOccupiedCriticalTileCount;
|
||||
}
|
||||
@@ -50,7 +66,6 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
}
|
||||
} else if (unit->player_id() == attackerPid) {
|
||||
++attackerUnitCount;
|
||||
attackerTroops += unit->battalion().size();
|
||||
if (unit->can_flee()) canFlee = true;
|
||||
attackerUnits.push_back(unit);
|
||||
} else {
|
||||
@@ -60,7 +75,13 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
}
|
||||
|
||||
AIStrategy chosenStrategy;
|
||||
if (canFlee && attackerTroops < MAXIMUM_RATIO_FOR_ATTACKER_TO_FLEE * defenderTroops) {
|
||||
|
||||
// Use sophisticated combat success estimation instead of simple troop ratio
|
||||
if (canFlee && AIFleeDecisionCalculator::ShouldConsiderFleeing(
|
||||
attackerPid,
|
||||
gameState,
|
||||
settings,
|
||||
FLEE_CONSIDERATION_THRESHOLD)) {
|
||||
chosenStrategy = FleeStrategy;
|
||||
} else if (const CoordsSet startCrossingLocations =
|
||||
waterCrossingCommandChooser
|
||||
|
||||
@@ -8,18 +8,16 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCommandChooser.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
|
||||
class AIAttackerStrategySelector {
|
||||
public:
|
||||
static auto BestAttackerStrategy(
|
||||
PlayerId attackerPid,
|
||||
const GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
|
||||
@@ -14,7 +14,7 @@ constexpr double MAXIMUM_RATIO_FOR_DEFENDER_TO_FLEE = 0.15;
|
||||
constexpr double MINIMUM_RATIO_FOR_DEFENDER_TO_HOLD = 0.60;
|
||||
|
||||
auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
const GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
const APDCache& apdCache,
|
||||
const SettingsGetter& settings) -> AIStrategy {
|
||||
|
||||
@@ -7,16 +7,15 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
class AIDefenderStrategySelector {
|
||||
using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
|
||||
public:
|
||||
static auto BestDefenderStrategy(
|
||||
const GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
const APDCache& apdCache,
|
||||
const SettingsGetter& settings) -> AIStrategy;
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// AIFleeDecisionCalculator.cpp
|
||||
// eagle0
|
||||
//
|
||||
// Handles AI flee decision logic including combat success estimation
|
||||
// and flee vs fight evaluation for final round scenarios
|
||||
//
|
||||
|
||||
#include "AIFleeDecisionCalculator.hpp"
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
auto AIFleeDecisionCalculator::GetFleeCommandIndex(
|
||||
const vector<CommandProto>::const_iterator& fleeCommand,
|
||||
const vector<CommandProto>& availableCommands) -> size_t {
|
||||
return static_cast<size_t>(std::distance(availableCommands.begin(), fleeCommand));
|
||||
}
|
||||
|
||||
auto AIFleeDecisionCalculator::EstimateCombatSuccess(
|
||||
PlayerId attackerPlayerId,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings) -> double {
|
||||
if (gameState->status() == nullptr ||
|
||||
gameState->status()->state() !=
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING) {
|
||||
return 1.0; // we're still in set_up so we can't really evaluate
|
||||
}
|
||||
|
||||
// Combat success estimation based on unit power, heroes, and capture dynamics
|
||||
double attackerPower = 0.0;
|
||||
double defenderPower = 0.0;
|
||||
int attackerTroops = 0; // Still track raw troops for special cases
|
||||
int defenderTroops = 0;
|
||||
int attackerUnits = 0;
|
||||
int defenderUnits = 0;
|
||||
int attackerHeroes = 0;
|
||||
int defenderHeroes = 0;
|
||||
bool defenderHasVips = false;
|
||||
|
||||
// Calculate total power and count units/heroes for each side
|
||||
for (const auto* unit : *gameState->units()) {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
|
||||
|
||||
const auto* pi = PlayerInfoForPid(gameState, unit->player_id());
|
||||
if (pi == nullptr) continue;
|
||||
|
||||
const int unitTroops = unit->battalion().size();
|
||||
const bool hasHero = unit->has_attached_hero();
|
||||
const double unitPower = ContextFreeUnitValue(unit);
|
||||
|
||||
if (pi->is_defender()) {
|
||||
defenderPower += unitPower;
|
||||
defenderTroops += unitTroops;
|
||||
defenderUnits++;
|
||||
if (hasHero) {
|
||||
defenderHeroes++;
|
||||
if (unit->attached_hero().is_vip()) { defenderHasVips = true; }
|
||||
}
|
||||
} else if (unit->player_id() == attackerPlayerId) {
|
||||
attackerPower += unitPower;
|
||||
attackerTroops += unitTroops;
|
||||
attackerUnits++;
|
||||
if (hasHero) { attackerHeroes++; }
|
||||
}
|
||||
}
|
||||
|
||||
const int roundsRemaining = settings.Backing().max_rounds() - gameState->current_round();
|
||||
|
||||
// Special case: Attacker has no heroes - automatic loss
|
||||
if (attackerHeroes == 0) {
|
||||
return 0.0; // Cannot win without heroes
|
||||
}
|
||||
|
||||
// Special case: Defender has no heroes - automatic win for attacker
|
||||
if (defenderHeroes == 0) {
|
||||
return 1.0; // Guaranteed win
|
||||
}
|
||||
|
||||
// Special case: Attacker has no troops (but has heroes)
|
||||
if (attackerTroops == 0) {
|
||||
// Very difficult to win with heroes alone
|
||||
return 0.05; // Extremely low chance
|
||||
}
|
||||
|
||||
// Special case: Defender has no troops but has heroes
|
||||
if (defenderTroops == 0) {
|
||||
// Defenders with only heroes are vulnerable to capture
|
||||
// Only truly difficult if time is extremely limited
|
||||
if (roundsRemaining <= 1) {
|
||||
// Last round - very hard to capture all heroes
|
||||
return 0.3; // Low but not impossible
|
||||
} else if (roundsRemaining <= 2) {
|
||||
return 0.6; // Still achievable
|
||||
} else {
|
||||
// With 3+ rounds, capturing defenseless heroes is quite feasible
|
||||
return 0.85; // High probability of success
|
||||
}
|
||||
}
|
||||
|
||||
// Normal case: Both sides have troops
|
||||
// Base probability from power ratio (accounts for unit quality, not just quantity)
|
||||
const double powerRatio = attackerPower / std::max(1.0, defenderPower);
|
||||
double baseProbability = std::min(0.95, std::max(0.05, powerRatio * 0.5));
|
||||
|
||||
// Adjust for time pressure - attackers need to win before time runs out
|
||||
if (roundsRemaining <= 1) {
|
||||
baseProbability *= 0.6; // Severe penalty for last round
|
||||
} else if (roundsRemaining <= 3) {
|
||||
baseProbability *= 0.8; // Moderate penalty
|
||||
}
|
||||
|
||||
// Adjust for unit count (more units = better tactical flexibility)
|
||||
const double unitRatio =
|
||||
static_cast<double>(attackerUnits) / std::max(1.0, static_cast<double>(defenderUnits));
|
||||
if (unitRatio < 0.5) {
|
||||
baseProbability *= 0.8;
|
||||
} else if (unitRatio > 1.5) {
|
||||
baseProbability *= 1.15;
|
||||
}
|
||||
|
||||
// Adjust for hero presence
|
||||
if (defenderHeroes > attackerHeroes && defenderHasVips) {
|
||||
// Defender has more heroes including VIPs - harder to capture
|
||||
baseProbability *= 0.85;
|
||||
}
|
||||
|
||||
return std::min(0.95, std::max(0.05, baseProbability));
|
||||
}
|
||||
|
||||
auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
PlayerId playerId,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const GameStateW& guessedState,
|
||||
const vector<CommandProto>& availableCommands,
|
||||
const vector<CommandProto>::const_iterator& fleeCommand,
|
||||
bool enableDebugLogging) -> FleeDecision {
|
||||
// Get flee success odds
|
||||
const int fleeSuccessChance = fleeCommand->odds().success_chance();
|
||||
|
||||
// Get thresholds from settings
|
||||
const int minimumFleeOddsThreshold = settingsGetter.Backing().ai_minimum_flee_odds_threshold();
|
||||
const int desperateFleeThreshold = settingsGetter.Backing().ai_desperate_flee_threshold();
|
||||
|
||||
if (enableDebugLogging) {
|
||||
printf("AI FinalRound: Evaluating flee (odds=%d%%)...\n", fleeSuccessChance);
|
||||
}
|
||||
|
||||
// Check if flee odds are good enough to attempt
|
||||
if (fleeSuccessChance >= minimumFleeOddsThreshold) {
|
||||
if (enableDebugLogging) {
|
||||
printf("AI FinalRound: Good flee odds (%d%% >= %d%%), choosing flee\n",
|
||||
fleeSuccessChance,
|
||||
minimumFleeOddsThreshold);
|
||||
}
|
||||
return FleeDecision{
|
||||
true,
|
||||
GetFleeCommandIndex(fleeCommand, availableCommands),
|
||||
"Good flee odds"};
|
||||
}
|
||||
|
||||
// Low flee odds - evaluate if fighting might be better
|
||||
const double combatWinChance = EstimateCombatSuccess(playerId, guessedState, settingsGetter);
|
||||
|
||||
// If combat situation is hopeless, even bad flee odds are better than certain death
|
||||
if (combatWinChance <= 0.05 && fleeSuccessChance >= desperateFleeThreshold) {
|
||||
if (enableDebugLogging) {
|
||||
printf("AI FinalRound: Combat hopeless (%.1f%%), desperate flee attempt (%d%%)\n",
|
||||
combatWinChance * 100,
|
||||
fleeSuccessChance);
|
||||
}
|
||||
return FleeDecision{
|
||||
true,
|
||||
GetFleeCommandIndex(fleeCommand, availableCommands),
|
||||
"Combat hopeless, desperate flee"};
|
||||
}
|
||||
|
||||
// Detailed flee vs fight comparison
|
||||
const double fleeChance = static_cast<double>(fleeSuccessChance) / 100.0;
|
||||
|
||||
// Compare expected outcomes:
|
||||
// - Flee: fleeChance of survival (not victory, but avoiding loss)
|
||||
// - Fight: combatWinChance of victory (better than survival)
|
||||
|
||||
constexpr double FLEE_VS_COMBAT_MARGIN =
|
||||
0.8; // Require 80% of combat chance to prefer fighting
|
||||
const double adjustedCombatThreshold = combatWinChance * FLEE_VS_COMBAT_MARGIN;
|
||||
|
||||
if (enableDebugLogging) {
|
||||
printf("AI FinalRound: Flee=%d%%, Combat=%.1f%%, Threshold=%.1f%% -> ",
|
||||
fleeSuccessChance,
|
||||
combatWinChance * 100,
|
||||
adjustedCombatThreshold * 100);
|
||||
}
|
||||
|
||||
if (fleeChance > adjustedCombatThreshold) {
|
||||
if (enableDebugLogging) { printf("FLEE (better odds)\n"); }
|
||||
return FleeDecision{
|
||||
true,
|
||||
GetFleeCommandIndex(fleeCommand, availableCommands),
|
||||
"Flee has better expected outcome"};
|
||||
} else {
|
||||
if (enableDebugLogging) { printf("FIGHT (better expected outcome)\n"); }
|
||||
// Return 0 to indicate we should use standard command selection
|
||||
return FleeDecision{
|
||||
false,
|
||||
0, // Will be replaced by StandardChooseCommandIndex
|
||||
"Fighting has better expected outcome"};
|
||||
}
|
||||
}
|
||||
|
||||
auto AIFleeDecisionCalculator::ShouldConsiderFleeing(
|
||||
PlayerId attackerPlayerId,
|
||||
const GameStateW& guessedState,
|
||||
const SettingsGetter& settings,
|
||||
double fleeConsiderationThreshold) -> bool {
|
||||
// Get combat success probability
|
||||
const double combatSuccessChance =
|
||||
EstimateCombatSuccess(attackerPlayerId, guessedState, settings);
|
||||
|
||||
// Consider fleeing if combat success chance is below threshold
|
||||
return combatSuccessChance < fleeConsiderationThreshold;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// AIFleeDecisionCalculator.hpp
|
||||
// eagle0
|
||||
//
|
||||
// Handles AI flee decision logic including combat success estimation
|
||||
// and flee vs fight evaluation for final round scenarios
|
||||
//
|
||||
|
||||
#ifndef AIFleeDecisionCalculator_hpp
|
||||
#define AIFleeDecisionCalculator_hpp
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
|
||||
class AIFleeDecisionCalculator {
|
||||
public:
|
||||
// Configuration for flee decision thresholds
|
||||
struct FleeThresholds {
|
||||
int minimumFleeOddsThreshold; // Minimum flee success odds to consider fleeing
|
||||
int desperateFleeThreshold; // Flee threshold when combat is hopeless
|
||||
};
|
||||
|
||||
// Result of flee vs fight evaluation
|
||||
struct FleeDecision {
|
||||
bool shouldFlee;
|
||||
size_t commandIndex; // Index of command to execute (flee or fight)
|
||||
const char* reasoning; // Debug explanation of decision
|
||||
};
|
||||
|
||||
// Evaluate whether to flee or fight in the final round
|
||||
[[nodiscard]] static auto EvaluateFleeVsFight(
|
||||
PlayerId playerId,
|
||||
const SettingsGetter& settings,
|
||||
const GameStateW& guessedState,
|
||||
const vector<CommandProto>& availableCommands,
|
||||
const vector<CommandProto>::const_iterator& fleeCommand,
|
||||
bool enableDebugLogging = false) -> FleeDecision;
|
||||
|
||||
// Estimate probability of combat success for the attacker
|
||||
[[nodiscard]] static auto EstimateCombatSuccess(
|
||||
PlayerId attackerPlayerId,
|
||||
const GameStateW& guessedState,
|
||||
const SettingsGetter& settings) -> double;
|
||||
|
||||
// Determine if the attacker should consider fleeing based on combat odds
|
||||
// Returns true if fleeing should be considered as an option
|
||||
[[nodiscard]] static auto ShouldConsiderFleeing(
|
||||
PlayerId attackerPlayerId,
|
||||
const GameStateW& guessedState,
|
||||
const SettingsGetter& settings,
|
||||
double fleeConsiderationThreshold = 0.5) -> bool;
|
||||
|
||||
private:
|
||||
// Helper to get flee command index
|
||||
[[nodiscard]] static auto GetFleeCommandIndex(
|
||||
const vector<CommandProto>::const_iterator& fleeCommand,
|
||||
const vector<CommandProto>& availableCommands) -> size_t;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif /* AIFleeDecisionCalculator_hpp */
|
||||
@@ -180,7 +180,7 @@ static auto RecursiveAttackerMultiplierForTargetDistance(
|
||||
const Unit *attackingUnit,
|
||||
vector<TargetAndAttackLocations>::const_iterator &priorityListNext,
|
||||
const vector<TargetAndAttackLocations>::const_iterator &priorityListEnd,
|
||||
const vector<const Unit *> &occupants,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *map,
|
||||
const BattalionTypeSPtr &battType,
|
||||
const ActionPointDistances *notBravingApd,
|
||||
@@ -191,7 +191,7 @@ static auto RecursiveAttackerMultiplierForTargetDistance(
|
||||
const Unit *attackingUnit,
|
||||
vector<TargetAndAttackLocations>::const_iterator &priorityListNext,
|
||||
const vector<TargetAndAttackLocations>::const_iterator &priorityListEnd,
|
||||
const vector<const Unit *> &occupants,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *map,
|
||||
const BattalionTypeSPtr &battType,
|
||||
const ActionPointDistances *notBravingApd,
|
||||
@@ -204,14 +204,13 @@ static auto RecursiveAttackerMultiplierForTargetDistance(
|
||||
|
||||
// If the target is unoccupied or is occupied by this player, give the maximum multiplier, but
|
||||
// also add the bonus for the next up in the priority list
|
||||
if (const Unit *occupant = occupants
|
||||
[topPriorityTarget.row() * map->column_count() + topPriorityTarget.column()];
|
||||
if (const Unit *occupant = occupancyLookup.GetOccupant(topPriorityTarget);
|
||||
!occupant || occupant->player_id() == attackingUnit->player_id()) {
|
||||
return kMaxProximityBuf + RecursiveAttackerMultiplierForTargetDistance(
|
||||
attackingUnit,
|
||||
++priorityListNext,
|
||||
priorityListEnd,
|
||||
occupants,
|
||||
occupancyLookup,
|
||||
map,
|
||||
battType,
|
||||
notBravingApd,
|
||||
@@ -231,7 +230,7 @@ static auto RecursiveAttackerMultiplierForTargetDistance(
|
||||
auto AttackerMultiplierForTargetDistance(
|
||||
const Unit *attackingUnit,
|
||||
const vector<TargetAndAttackLocations> &priorityList,
|
||||
const vector<const Unit *> &occupants,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *map,
|
||||
const BattalionTypeSPtr &battType,
|
||||
const ActionPointDistances *notBravingApd,
|
||||
@@ -242,7 +241,7 @@ auto AttackerMultiplierForTargetDistance(
|
||||
attackingUnit,
|
||||
iter,
|
||||
end(priorityList),
|
||||
occupants,
|
||||
occupancyLookup,
|
||||
map,
|
||||
battType,
|
||||
notBravingApd,
|
||||
@@ -261,12 +260,10 @@ auto AttackerUnitsScore(
|
||||
const APDCache &apdCache,
|
||||
const MapId &mapId) -> ScoreValue {
|
||||
// Cache frequently accessed FlatBuffer fields to avoid repeated offset calculations
|
||||
const auto *cachedGameState = gameState.Get();
|
||||
const auto *cachedUnits = cachedGameState->units();
|
||||
const auto *cachedHexMap = cachedGameState->hex_map();
|
||||
const int16_t cachedRowCount = cachedHexMap->row_count();
|
||||
const int16_t cachedColumnCount = cachedHexMap->column_count();
|
||||
const int cachedCurrentRound = cachedGameState->current_round();
|
||||
const auto *gameStateRawPtr = gameState.Get();
|
||||
const auto *cachedUnits = gameStateRawPtr->units();
|
||||
const auto *cachedHexMap = gameStateRawPtr->hex_map();
|
||||
const int cachedCurrentRound = gameStateRawPtr->current_round();
|
||||
|
||||
bool isLateGame = cachedCurrentRound > 18; // Inline IsLateGame for efficiency
|
||||
|
||||
@@ -286,10 +283,10 @@ auto AttackerUnitsScore(
|
||||
double attackerUnitsValue = 0;
|
||||
double defenderUnitsValue = 0;
|
||||
|
||||
auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);
|
||||
auto occupancyLookup = gameState.CreateOccupancyLookup();
|
||||
|
||||
for (const Unit *unit : *cachedUnits) {
|
||||
const auto *pi = PlayerInfoForPid(cachedGameState, unit->player_id());
|
||||
const auto *pi = PlayerInfoForPid(gameState, unit->player_id());
|
||||
if (pi == nullptr) continue;
|
||||
|
||||
switch (unit->status()) {
|
||||
@@ -350,7 +347,7 @@ auto AttackerUnitsScore(
|
||||
: AttackerMultiplierForTargetDistance(
|
||||
unit,
|
||||
priorityList->priorityOrder,
|
||||
occupants,
|
||||
occupancyLookup,
|
||||
cachedHexMap,
|
||||
settings.GetBattalionType(
|
||||
static_cast<BattalionTypeId>(battTypeId)),
|
||||
|
||||
@@ -16,7 +16,7 @@ auto HasAttachedHeroWithProfession(
|
||||
unit->attached_hero().profession_info().profession() == profession;
|
||||
}
|
||||
|
||||
auto CastleClaimCapableAttackerUnitCount(const GameState *gameState) -> int {
|
||||
auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int {
|
||||
int count = 0;
|
||||
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
@@ -32,7 +32,7 @@ auto CastleClaimCapableAttackerUnitCount(const GameState *gameState) -> int {
|
||||
return count;
|
||||
}
|
||||
|
||||
auto PlayerInfoForPid(const GameState *gs, const PlayerId pid) -> const PlayerInfo * {
|
||||
auto PlayerInfoForPid(const GameStateW &gs, const PlayerId pid) -> const PlayerInfo * {
|
||||
if (gs->player_infos()) {
|
||||
for (const auto &pi : *gs->player_infos()) {
|
||||
if (pi->player_id() == pid) return pi;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
@@ -25,8 +26,8 @@ auto HasAttachedHeroWithProfession(
|
||||
const Unit *unit,
|
||||
net::eagle0::shardok::storage::fb::Profession profession) -> bool;
|
||||
|
||||
auto CastleClaimCapableAttackerUnitCount(const GameState *gameState) -> int;
|
||||
auto PlayerInfoForPid(const GameState *gs, PlayerId pid) -> const PlayerInfo *;
|
||||
auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int;
|
||||
auto PlayerInfoForPid(const GameStateW &, PlayerId pid) -> const PlayerInfo *;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -18,6 +18,23 @@ using std::shared_ptr;
|
||||
constexpr double kProfessionValue = 200;
|
||||
constexpr double kCastleMultiplierBonus = 1.0;
|
||||
constexpr double kOnFireMultiplier = 0.25;
|
||||
|
||||
// Create a 2D grid of unit positions for spatial analysis
|
||||
static inline auto Occupants(
|
||||
const vector<const Unit *> &units,
|
||||
const int rowCount,
|
||||
const int columnCount) -> vector<const Unit *> {
|
||||
vector<const Unit *> positions(rowCount * columnCount);
|
||||
|
||||
for (const auto &unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
const int index = unit->location().row() * columnCount + unit->location().column();
|
||||
positions[index] = unit;
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
constexpr double kAdjacentFireMultiplier = 0.99;
|
||||
constexpr double kOnIceMultiplier = 0.25;
|
||||
constexpr double kMeteorStartInRangeValue = 50;
|
||||
|
||||
@@ -122,7 +122,7 @@ auto AttackerDebufForDefenderOccupiedCriticalTile(
|
||||
}
|
||||
|
||||
auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
const net::eagle0::shardok::storage::fb::GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileLocations,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
@@ -130,13 +130,10 @@ auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
const SettingsGetter& settings) -> ScoreValue {
|
||||
ScoreValue total = 0.0;
|
||||
|
||||
const auto rc = gameState->hex_map()->row_count();
|
||||
const auto cc = gameState->hex_map()->column_count();
|
||||
const auto occupants = Occupants(*gameState->units(), rc, cc);
|
||||
auto occupancyLookup = gameState.CreateOccupancyLookup();
|
||||
|
||||
for (const Coords& criticalTileLocation : criticalTileLocations) {
|
||||
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
|
||||
const auto& occupant = occupants[index];
|
||||
const auto* occupant = occupancyLookup.GetOccupant(criticalTileLocation);
|
||||
|
||||
if (occupant && occupant->battalion().type() !=
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
|
||||
@@ -152,7 +149,7 @@ auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
}
|
||||
|
||||
auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
const net::eagle0::shardok::storage::fb::GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileLocations,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
@@ -178,14 +175,11 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
|
||||
ScoreValue total = 0.0;
|
||||
|
||||
const auto rc = gameState->hex_map()->row_count();
|
||||
const auto cc = gameState->hex_map()->column_count();
|
||||
const auto occupants = Occupants(*gameState->units(), rc, cc);
|
||||
auto occupancyLookup = gameState.CreateOccupancyLookup();
|
||||
|
||||
for (const Coords& criticalTileLocation : criticalTileLocations) {
|
||||
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
|
||||
const auto terrain = GetTerrain(gameState->hex_map(), criticalTileLocation);
|
||||
const auto& occupant = occupants[index];
|
||||
const auto* occupant = occupancyLookup.GetOccupant(criticalTileLocation);
|
||||
|
||||
if (occupant) {
|
||||
if (occupant->player_id() == player->player_id()) {
|
||||
@@ -246,7 +240,7 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
}
|
||||
|
||||
auto LastPlayerStandingVictoryScore(
|
||||
const GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#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/settings/GameSettings.hpp"
|
||||
@@ -23,7 +24,7 @@ using std::vector;
|
||||
using ScoreValue = double;
|
||||
|
||||
auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
const net::eagle0::shardok::storage::fb::GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileLocations,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
@@ -31,7 +32,7 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
const SettingsGetter& settings) -> ScoreValue;
|
||||
|
||||
auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
const net::eagle0::shardok::storage::fb::GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileLocations,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
@@ -39,7 +40,7 @@ auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
const SettingsGetter& settings) -> ScoreValue;
|
||||
|
||||
auto LastPlayerStandingVictoryScore(
|
||||
const GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
namespace shardok {
|
||||
|
||||
auto UnitIdsRequiringWaterCrossing(
|
||||
const GameState *gameState,
|
||||
const GameStateW &gameState,
|
||||
const PlayerId pid,
|
||||
const CoordsSet &destinations,
|
||||
const APDCache &apdCache,
|
||||
@@ -74,7 +74,7 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
}
|
||||
|
||||
auto UnitIdsToCreateWaterCrossing(
|
||||
const GameState *gameState,
|
||||
const GameStateW &gameState,
|
||||
const PlayerId pid,
|
||||
const APDCache &apdCache,
|
||||
const SettingsGetter &settings) -> vector<UnitId> {
|
||||
@@ -196,7 +196,7 @@ auto WaterCrossingTiles(
|
||||
|
||||
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
|
||||
auto IntendedCrossingStarts(
|
||||
const GameState *gameState,
|
||||
const GameStateW &gameState,
|
||||
const vector<UnitId> &unitIdsCreatingCrossing,
|
||||
const CoordsSet &tilesToStartCrossingFrom,
|
||||
const MapId &mapId,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef EAGLE0_AIWATERCROSSINGCALCULATOR_HPP
|
||||
#define EAGLE0_AIWATERCROSSINGCALCULATOR_HPP
|
||||
|
||||
#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/settings/GameSettings.hpp"
|
||||
@@ -29,7 +30,7 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
|
||||
|
||||
// Units that need a water crossing to reach at least one of the destinations
|
||||
auto UnitIdsRequiringWaterCrossing(
|
||||
const GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const CoordsSet& destinations,
|
||||
const APDCache& apdCache,
|
||||
@@ -37,7 +38,7 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
|
||||
// Units belonging to the player that are capable of creating water crossings
|
||||
auto UnitIdsToCreateWaterCrossing(
|
||||
const GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const APDCache& apdCache,
|
||||
const SettingsGetter& settings) -> vector<UnitId>;
|
||||
@@ -67,7 +68,7 @@ auto WaterCrossingTiles(
|
||||
|
||||
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
|
||||
auto IntendedCrossingStarts(
|
||||
const GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
const vector<UnitId>& unitIdsCreatingCrossing,
|
||||
const CoordsSet& tilesToStartCrossingFrom,
|
||||
const MapId& mapId,
|
||||
|
||||
@@ -17,7 +17,7 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
|
||||
|
||||
[[nodiscard]] auto AIWaterCrossingCommandChooser::WaterCrossingScore(
|
||||
const SettingsGetter &settingsGetter,
|
||||
const GameState *gameState,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom) const -> ScoreValue {
|
||||
int castleClaimCount = 0;
|
||||
@@ -119,7 +119,7 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
|
||||
|
||||
auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
const SettingsGetter &settingsGetter,
|
||||
const GameState *gameState,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords) const -> CoordsSet {
|
||||
CoordsSet startCrossingFrom(gameState->hex_map());
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
@@ -34,12 +35,12 @@ public:
|
||||
|
||||
auto StartCrossingFrom(
|
||||
const SettingsGetter &settingsGetter,
|
||||
const GameState *gameState,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords) const -> CoordsSet;
|
||||
|
||||
[[nodiscard]] auto WaterCrossingScore(
|
||||
const SettingsGetter &settingsGetter,
|
||||
const GameState *gameState,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom) const -> ScoreValue;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ cc_library(
|
||||
],
|
||||
deps = [
|
||||
":ai_attack_locations",
|
||||
":ai_flee_decision_calculator",
|
||||
":ai_score_utilities",
|
||||
":ai_strategy",
|
||||
":ai_water_crossing_command_chooser",
|
||||
@@ -70,6 +71,7 @@ cc_library(
|
||||
":ai_score_utilities",
|
||||
":ai_strategy",
|
||||
":ai_water_crossing_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coords_set",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
@@ -120,12 +122,32 @@ cc_library(
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_flee_decision_calculator",
|
||||
srcs = ["AIFleeDecisionCalculator.cpp"],
|
||||
hdrs = ["AIFleeDecisionCalculator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":ai_score_utilities",
|
||||
":ai_unit_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_command_filter",
|
||||
srcs = ["AICommandFilter.cpp"],
|
||||
@@ -210,6 +232,7 @@ cc_library(
|
||||
":ai_attack_locations",
|
||||
":ai_distance_debuf",
|
||||
":ai_score_utilities",
|
||||
"//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/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
@@ -227,6 +250,7 @@ cc_library(
|
||||
],
|
||||
deps = [
|
||||
":ai_minimum_distance_and_target",
|
||||
"//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/fb_helpers:hex_map_helpers",
|
||||
@@ -244,6 +268,7 @@ cc_library(
|
||||
deps = [
|
||||
":ai_minimum_distance_and_target",
|
||||
":ai_water_crossing_calculator",
|
||||
"//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/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
@@ -299,6 +324,7 @@ cc_library(
|
||||
deps = [
|
||||
":ai_attacker_strategy_selector",
|
||||
":ai_defender_strategy_selector",
|
||||
":ai_flee_decision_calculator",
|
||||
":ai_iterative_deepening",
|
||||
":ai_score_calculator",
|
||||
":ai_time_budget",
|
||||
|
||||
@@ -8,10 +8,14 @@
|
||||
|
||||
#include "ShardokAIClient.hpp"
|
||||
|
||||
#define DEBUG_FLEE_DECISIONS
|
||||
|
||||
#include <google/protobuf/util/message_differencer.h>
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
#include "AIFleeDecisionCalculator.hpp"
|
||||
#include "AIScoreUtilities.hpp"
|
||||
#include "AITimeBudget.hpp"
|
||||
#include "IterativeDeepeningAI.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/TimeUtils.hpp"
|
||||
@@ -175,23 +179,41 @@ auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &guessedState,
|
||||
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
|
||||
if (const auto fleeCommand = std::ranges::find_if(
|
||||
realAvailableCommands,
|
||||
[](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
|
||||
return cmd.type() == net::eagle0::shardok::common::FLEE_COMMAND;
|
||||
});
|
||||
fleeCommand == realAvailableCommands.end()) {
|
||||
const auto fleeCommand = std::ranges::find_if(
|
||||
realAvailableCommands,
|
||||
[](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
|
||||
return cmd.type() == net::eagle0::shardok::common::FLEE_COMMAND;
|
||||
});
|
||||
|
||||
if (fleeCommand == realAvailableCommands.end()) {
|
||||
return LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
} else {
|
||||
}
|
||||
|
||||
// Use the flee decision calculator
|
||||
const auto fleeDecision = AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
playerId,
|
||||
settings->GetGetter(),
|
||||
guessedState,
|
||||
realAvailableCommands,
|
||||
fleeCommand,
|
||||
#ifdef DEBUG_FLEE_DECISIONS
|
||||
true // Enable debug logging
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
);
|
||||
|
||||
if (fleeDecision.shouldFlee) {
|
||||
CommandChoiceResults results{};
|
||||
results.chosenIndex =
|
||||
static_cast<size_t>(std::distance(realAvailableCommands.begin(), fleeCommand));
|
||||
results.chosenIndex = fleeDecision.commandIndex;
|
||||
results.availableCommandCount = realAvailableCommands.size();
|
||||
results.depthAchieved = 1; // Simple heuristic choice
|
||||
results.depthAchieved = 1; // Heuristic choice
|
||||
results.commandCountEvaluated = 1; // Only evaluated one command type
|
||||
results.completionReason =
|
||||
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS; // Heuristic choice
|
||||
results.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
return results;
|
||||
} else {
|
||||
// Fight instead of flee
|
||||
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ private:
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& guessedState,
|
||||
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
[[nodiscard]] auto ChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const net::eagle0::shardok::api::GameStateView& gsv,
|
||||
|
||||
@@ -110,6 +110,7 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
|
||||
// New style factory commands
|
||||
CommandList oneUnitCommands{};
|
||||
auto occupancyLookup = gameState.CreateOccupancyLookup();
|
||||
const CommandFactory::CommandParams commandParams{
|
||||
.unit = unit,
|
||||
.remainingActionPoints = remainingActionPoints,
|
||||
@@ -121,7 +122,8 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
.units = gameState->units(),
|
||||
.allyPids = allyPids,
|
||||
.isAttacker = isAttacker,
|
||||
.unitMovedIntoZoc = unitMovedIntoZoc};
|
||||
.unitMovedIntoZoc = unitMovedIntoZoc,
|
||||
.occupancyLookup = occupancyLookup};
|
||||
|
||||
for (const auto &commandFactory : commandFactories) {
|
||||
if (!onlyFollowUps || commandFactory->IncludeInFollowUps()) {
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "occupancy_lookup",
|
||||
srcs = ["OccupancyLookup.cpp"],
|
||||
hdrs = ["OccupancyLookup.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":shardok_c_types",
|
||||
":shardok_exception",
|
||||
"//src/main/cpp/net/eagle0/common:container_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coords_set",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "game_state_w",
|
||||
srcs = ["GameStateW.cpp"],
|
||||
@@ -7,6 +24,7 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":occupancy_lookup",
|
||||
":shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/common:container_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
|
||||
|
||||
@@ -122,4 +122,6 @@ auto GameStateW::GetOccupiedTilesBitfield() const -> const flatbuffers::Vector<u
|
||||
return state->occupied_tiles();
|
||||
}
|
||||
|
||||
auto GameStateW::CreateOccupancyLookup() const -> OccupancyLookup { return OccupancyLookup(Get()); }
|
||||
|
||||
} // namespace shardok
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef EAGLE0_GAMESTATEW_HPP
|
||||
#define EAGLE0_GAMESTATEW_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/OccupancyLookup.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
@@ -102,6 +103,15 @@ public:
|
||||
* GameStateW lookups in performance-critical code like MoveCommand.
|
||||
*/
|
||||
[[nodiscard]] auto GetOccupiedTilesBitfield() const -> const flatbuffers::Vector<uint8_t>*;
|
||||
|
||||
/**
|
||||
* @brief Create an OccupancyLookup for efficient occupancy checking.
|
||||
* @return An OccupancyLookup instance that can be passed to commands.
|
||||
*
|
||||
* This provides a clean way to access occupancy functionality without
|
||||
* directly exposing GameStateW internals to command constructors.
|
||||
*/
|
||||
[[nodiscard]] auto CreateOccupancyLookup() const -> OccupancyLookup;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// Created by Dan Crosby on 2025-01-24.
|
||||
//
|
||||
|
||||
#include "OccupancyLookup.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// OccupancyLookup implementation
|
||||
OccupancyLookup::OccupancyLookup(const net::eagle0::shardok::storage::fb::GameState* gameState)
|
||||
: gameState_(gameState),
|
||||
hexMap_(gameState ? gameState->hex_map() : nullptr),
|
||||
occupiedTilesBitfield_(nullptr),
|
||||
rowCount_(0),
|
||||
columnCount_(0) {
|
||||
if (hexMap_) {
|
||||
rowCount_ = hexMap_->row_count();
|
||||
columnCount_ = hexMap_->column_count();
|
||||
|
||||
// Get the occupied tiles bitfield if available and valid
|
||||
if (gameState_->occupied_tiles() && !gameState_->occupied_tiles()->empty()) {
|
||||
if (const size_t expectedBitfieldSize = (rowCount_ * columnCount_ + 7) / 8;
|
||||
gameState_->occupied_tiles()->size() == expectedBitfieldSize) {
|
||||
occupiedTilesBitfield_ = gameState_->occupied_tiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto OccupancyLookup::GetOccupant(const Coords& coords) const -> const Unit* {
|
||||
if (!gameState_ || !hexMap_) { return nullptr; }
|
||||
|
||||
// Check bounds
|
||||
if (coords.row() < 0 || coords.row() >= rowCount_ || coords.column() < 0 ||
|
||||
coords.column() >= columnCount_) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Fast path: use bitfield cache if available
|
||||
if (occupiedTilesBitfield_) {
|
||||
const size_t tileIndex = coords.row() * columnCount_ + coords.column();
|
||||
const size_t byteIndex = tileIndex / 8;
|
||||
const size_t bitOffset = tileIndex % 8;
|
||||
const uint8_t byte = occupiedTilesBitfield_->Get(byteIndex);
|
||||
|
||||
if (const bool isOccupied = (byte & (1 << bitOffset)) != 0; !isOccupied) {
|
||||
return nullptr; // Fast path: definitely no unit here (90% of cases)
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: O(n) search through units
|
||||
// Used when bitfield not available OR when bitfield indicates occupation
|
||||
if (!gameState_->units()) { return nullptr; }
|
||||
|
||||
for (int i = 0; i < gameState_->units()->size(); ++i) {
|
||||
if (const auto* unit = gameState_->units()->Get(i);
|
||||
unit && unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location().row() == coords.row() &&
|
||||
unit->location().column() == coords.column()) {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto OccupancyLookup::GetKnownEnemyOccupant(
|
||||
const PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> const Unit* {
|
||||
if (const auto* occupant = GetOccupant(coords)) {
|
||||
if (!occupant->hidden() && occupant->player_id() != playerId &&
|
||||
!common::Contains(allyPids, occupant->player_id())) {
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto OccupancyLookup::HasForestAccess(
|
||||
const Coords& coords,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const PlayerId playerId) const -> bool {
|
||||
if (!hexMap_) return false;
|
||||
|
||||
if (GetTerrain(hexMap_, coords)->type() ==
|
||||
net::eagle0::shardok::storage::fb::Terrain_::Type_FOREST)
|
||||
return true;
|
||||
|
||||
const CoordsSet& adjacentCoords = HexMapUtils::GetAdjacentCoords(hexMap_, coords);
|
||||
|
||||
return std::any_of(
|
||||
std::begin(adjacentCoords),
|
||||
std::end(adjacentCoords),
|
||||
[&](const Coords& adjCoords) {
|
||||
if (GetKnownEnemyOccupant(playerId, allyPids, adjCoords)) return false;
|
||||
if (GetTerrain(hexMap_, adjCoords)->type() ==
|
||||
net::eagle0::shardok::storage::fb::Terrain_::Type_FOREST)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
auto OccupancyLookup::GetFriendlyOccupant(
|
||||
const PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> const Unit* {
|
||||
if (const auto* occupant = GetOccupant(coords)) {
|
||||
if (occupant->player_id() == playerId ||
|
||||
common::Contains(allyPids, occupant->player_id())) {
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto OccupancyLookup::GetKnownOccupant(
|
||||
const PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> const Unit* {
|
||||
if (const auto* occupant = GetOccupant(coords)) {
|
||||
if (occupant->player_id() == playerId ||
|
||||
common::Contains(allyPids, occupant->player_id()) || !occupant->hidden()) {
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto OccupancyLookup::GetKnownAdjacentEnemies(
|
||||
const PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> std::vector<const Unit*> {
|
||||
std::vector<const Unit*> knownAdjacentEnemies{};
|
||||
if (!hexMap_) return knownAdjacentEnemies;
|
||||
|
||||
for (const CoordsSet& adjacentCoords = HexMapUtils::GetAdjacentCoords(hexMap_, coords);
|
||||
const auto& adjCoords : adjacentCoords) {
|
||||
if (const auto* possibleEnemy = GetKnownEnemyOccupant(playerId, allyPids, adjCoords)) {
|
||||
knownAdjacentEnemies.push_back(possibleEnemy);
|
||||
}
|
||||
}
|
||||
|
||||
return knownAdjacentEnemies;
|
||||
}
|
||||
|
||||
auto OccupancyLookup::GetOccupantInDirection(const Coords& coords, const HexMapDirection direction)
|
||||
const -> const Unit* {
|
||||
if (!hexMap_) return nullptr;
|
||||
|
||||
if (const auto& maybeTile = MaybeTileInDirection(hexMap_, coords, direction);
|
||||
maybeTile.has_value()) {
|
||||
return GetOccupant(maybeTile.value());
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto OccupancyLookup::IsBelievedEmpty(PlayerId playerId, const Coords& coords) const -> bool {
|
||||
if (const Unit* occupantOptional = GetOccupant(coords)) {
|
||||
return (occupantOptional->hidden() && occupantOptional->player_id() != playerId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// Created by Dan Crosby on 2025-01-24.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_OCCUPANCYLOOKUP_HPP
|
||||
#define EAGLE0_OCCUPANCYLOOKUP_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapDirection.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
/**
|
||||
* @class OccupancyLookup
|
||||
* @brief Encapsulates efficient occupancy checking using the occupied tiles bitfield.
|
||||
*
|
||||
* This class provides a clean interface for occupancy checks and can be passed
|
||||
* efficiently through command parameters. It uses the optimized bitfield approach
|
||||
* for O(1) empty tile checks and falls back to O(n) unit search only when needed.
|
||||
*/
|
||||
class OccupancyLookup {
|
||||
public:
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
|
||||
/**
|
||||
* @brief Construct an OccupancyLookup from a GameState.
|
||||
* @param gameState The game state to create lookup for.
|
||||
*/
|
||||
explicit OccupancyLookup(const net::eagle0::shardok::storage::fb::GameState* gameState);
|
||||
|
||||
/**
|
||||
* @brief Get the unit occupying the specified coordinates.
|
||||
* @param coords The coordinates to check.
|
||||
* @return Pointer to the unit at the coordinates, or nullptr if none.
|
||||
*/
|
||||
[[nodiscard]] auto GetOccupant(const Coords& coords) const -> const Unit*;
|
||||
|
||||
/**
|
||||
* @brief Get the known enemy unit occupying the specified coordinates.
|
||||
* @param playerId The player ID to check enemies for.
|
||||
* @param allyPids Vector of allied player IDs.
|
||||
* @param coords The coordinates to check.
|
||||
* @return Pointer to the enemy unit at the coordinates, or nullptr if none.
|
||||
*/
|
||||
[[nodiscard]] auto GetKnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> const Unit*;
|
||||
|
||||
/**
|
||||
* @brief Check if a player has forest access at the specified coordinates.
|
||||
* @param coords The coordinates to check.
|
||||
* @param allyPids Vector of allied player IDs.
|
||||
* @param playerId The player ID to check for.
|
||||
* @return True if the player has forest access, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] auto HasForestAccess(
|
||||
const Coords& coords,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
PlayerId playerId) const -> bool;
|
||||
|
||||
/**
|
||||
* @brief Get a friendly unit at the specified coordinates.
|
||||
* @param playerId The player ID to check for.
|
||||
* @param allyPids Vector of allied player IDs.
|
||||
* @param coords The coordinates to check.
|
||||
* @return Pointer to the friendly unit, or nullptr if none.
|
||||
*/
|
||||
[[nodiscard]] auto GetFriendlyOccupant(
|
||||
PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> const Unit*;
|
||||
|
||||
/**
|
||||
* @brief Get a known (visible) unit at the specified coordinates.
|
||||
* @param playerId The player ID doing the checking.
|
||||
* @param allyPids Vector of allied player IDs.
|
||||
* @param coords The coordinates to check.
|
||||
* @return Pointer to the known unit, or nullptr if none or hidden.
|
||||
*/
|
||||
[[nodiscard]] auto GetKnownOccupant(
|
||||
PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> const Unit*;
|
||||
|
||||
/**
|
||||
* @brief Get all known adjacent enemy units.
|
||||
* @param playerId The player ID to check enemies for.
|
||||
* @param allyPids Vector of allied player IDs.
|
||||
* @param coords The center coordinates.
|
||||
* @return Vector of pointers to adjacent enemy units.
|
||||
*/
|
||||
[[nodiscard]] auto GetKnownAdjacentEnemies(
|
||||
PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> std::vector<const Unit*>;
|
||||
|
||||
/**
|
||||
* @brief Get occupant in a specific direction from coordinates.
|
||||
* @param coords The starting coordinates.
|
||||
* @param direction The direction to check.
|
||||
* @return Pointer to the unit in that direction, or nullptr if none.
|
||||
*/
|
||||
[[nodiscard]] auto GetOccupantInDirection(const Coords& coords, HexMapDirection direction) const
|
||||
-> const Unit*;
|
||||
|
||||
/**
|
||||
* @brief Check if a tile is believed to be empty by a player.
|
||||
* @param playerId The player ID doing the checking.
|
||||
* @param coords The coordinates to check.
|
||||
* @return True if believed empty (actually empty or occupied by hidden enemy).
|
||||
*/
|
||||
[[nodiscard]] auto IsBelievedEmpty(PlayerId playerId, const Coords& coords) const -> bool;
|
||||
|
||||
private:
|
||||
const net::eagle0::shardok::storage::fb::GameState* gameState_;
|
||||
const net::eagle0::shardok::storage::fb::HexMap* hexMap_;
|
||||
const flatbuffers::Vector<uint8_t>* occupiedTilesBitfield_;
|
||||
int8_t rowCount_;
|
||||
int8_t columnCount_;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_OCCUPANCYLOOKUP_HPP
|
||||
+37
-34
@@ -29,17 +29,34 @@ IceAndSnowAdjustmentActionFactory::IceAndSnowAdjustmentActionFactory(
|
||||
const shardok::SettingsGetter& getter)
|
||||
: settings(getter) {}
|
||||
|
||||
auto IceAndSnowAdjustmentActionFactory::MakeIceAdjustmentAction(
|
||||
auto IceAndSnowAdjustmentActionFactory::MakeSnowAdjustmentAction(
|
||||
const HexMap* hexMap,
|
||||
const Coords& coords,
|
||||
const Units& units,
|
||||
const Terrain* terrain,
|
||||
const int month,
|
||||
const WeatherFb* weather) const -> shardok::ActionSPtr {
|
||||
return std::make_shared<SnowAdjustmentAction>(
|
||||
coords,
|
||||
terrain,
|
||||
hexMap->monthly_weather()->Get(month - 1)->average_temperature(),
|
||||
*weather,
|
||||
settings);
|
||||
}
|
||||
|
||||
auto IsSnowing(const WeatherFb* weather) -> bool {
|
||||
return weather->conditions() == net::eagle0::shardok::storage::fb::WeatherConditions_SNOW ||
|
||||
weather->conditions() == net::eagle0::shardok::storage::fb::WeatherConditions_BLIZZARD;
|
||||
}
|
||||
|
||||
// GameStateW overload for MakeIceAdjustmentAction
|
||||
auto IceAndSnowAdjustmentActionFactory::MakeIceAdjustmentAction(
|
||||
const Coords& coords,
|
||||
const GameStateW& gameState,
|
||||
const Terrain* terrain) const -> ActionSPtr {
|
||||
std::shared_ptr<ShardokAction> fallAction = nullptr;
|
||||
std::shared_ptr<ShardokAction> frozenAction = nullptr;
|
||||
|
||||
const auto* const occupant = Occupant(units, coords);
|
||||
const auto* const occupant = gameState.GetOccupant(coords);
|
||||
if (occupant) {
|
||||
const auto& battType = settings.GetBattalionType(occupant->battalion().type());
|
||||
|
||||
@@ -63,48 +80,34 @@ auto IceAndSnowAdjustmentActionFactory::MakeIceAdjustmentAction(
|
||||
coords,
|
||||
terrain,
|
||||
IceIntegrityAdjustment(
|
||||
hexMap->monthly_weather()->Get(month - 1)->average_temperature(),
|
||||
weather,
|
||||
gameState->hex_map()
|
||||
->monthly_weather()
|
||||
->Get(gameState->month() - 1)
|
||||
->average_temperature(),
|
||||
gameState->weather(),
|
||||
terrain,
|
||||
settings),
|
||||
fallAction,
|
||||
frozenAction);
|
||||
}
|
||||
|
||||
auto IceAndSnowAdjustmentActionFactory::MakeSnowAdjustmentAction(
|
||||
const HexMap* hexMap,
|
||||
const Coords& coords,
|
||||
const Terrain* terrain,
|
||||
const int month,
|
||||
const WeatherFb* weather) const -> shardok::ActionSPtr {
|
||||
return std::make_shared<SnowAdjustmentAction>(
|
||||
coords,
|
||||
terrain,
|
||||
hexMap->monthly_weather()->Get(month - 1)->average_temperature(),
|
||||
*weather,
|
||||
settings);
|
||||
}
|
||||
|
||||
auto IsSnowing(const WeatherFb* weather) -> bool {
|
||||
return weather->conditions() == net::eagle0::shardok::storage::fb::WeatherConditions_SNOW ||
|
||||
weather->conditions() == net::eagle0::shardok::storage::fb::WeatherConditions_BLIZZARD;
|
||||
}
|
||||
|
||||
// GameStateW overload for MakeIceAndSnowAdjustmentActions
|
||||
auto IceAndSnowAdjustmentActionFactory::MakeIceAndSnowAdjustmentActions(
|
||||
const HexMap* hexMap,
|
||||
const Units& units,
|
||||
const int month,
|
||||
const WeatherFb* weather) const -> shardok::ActionList {
|
||||
const GameStateW& gameState) const -> ActionList {
|
||||
ActionList actions;
|
||||
|
||||
for (const Coords& coords : GetAllCoords(hexMap)) {
|
||||
const auto* terrain = GetTerrain(hexMap, coords);
|
||||
for (const Coords& coords : GetAllCoords(gameState->hex_map())) {
|
||||
const auto* terrain = GetTerrain(gameState->hex_map(), coords);
|
||||
|
||||
if (IsWater(terrain->type())) {
|
||||
actions.emplace_back(
|
||||
MakeIceAdjustmentAction(hexMap, coords, units, terrain, month, weather));
|
||||
} else if (IsSnowing(weather) || (terrain->modifier().snow().present())) {
|
||||
actions.emplace_back(MakeSnowAdjustmentAction(hexMap, coords, terrain, month, weather));
|
||||
actions.emplace_back(MakeIceAdjustmentAction(coords, gameState, terrain));
|
||||
} else if (IsSnowing(gameState->weather()) || (terrain->modifier().snow().present())) {
|
||||
actions.emplace_back(MakeSnowAdjustmentAction(
|
||||
gameState->hex_map(),
|
||||
coords,
|
||||
terrain,
|
||||
gameState->month(),
|
||||
gameState->weather()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-10
@@ -26,13 +26,11 @@ public:
|
||||
explicit IceAndSnowAdjustmentActionFactory(const SettingsGetter &getter);
|
||||
virtual ~IceAndSnowAdjustmentActionFactory() = default;
|
||||
|
||||
// GameStateW overload for MakeIceAdjustmentAction
|
||||
[[nodiscard]] virtual auto MakeIceAdjustmentAction(
|
||||
const HexMap *hexMap,
|
||||
const Coords &coords,
|
||||
const Units &units,
|
||||
const Terrain *terrain,
|
||||
int month,
|
||||
const WeatherFb *weather) const -> ActionSPtr;
|
||||
const GameStateW &gameState,
|
||||
const Terrain *terrain) const -> ActionSPtr;
|
||||
|
||||
[[nodiscard]] virtual auto MakeSnowAdjustmentAction(
|
||||
const HexMap *hexMap,
|
||||
@@ -41,11 +39,9 @@ public:
|
||||
int month,
|
||||
const WeatherFb *weather) const -> ActionSPtr;
|
||||
|
||||
[[nodiscard]] virtual auto MakeIceAndSnowAdjustmentActions(
|
||||
const HexMap *hexMap,
|
||||
const Units &units,
|
||||
int month,
|
||||
const WeatherFb *weather) const -> ActionList;
|
||||
// GameStateW overload for MakeIceAndSnowAdjustmentActions
|
||||
[[nodiscard]] virtual auto MakeIceAndSnowAdjustmentActions(const GameStateW &gameState) const
|
||||
-> ActionList;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
+4
-1
@@ -184,10 +184,13 @@ auto ActionPointDistancesCache::GetRaw(
|
||||
// Declaring here to keep the copied map in scope
|
||||
const HexMap* mapToUse = map;
|
||||
|
||||
// ReSharper disable once CppTooWideScope
|
||||
// ReSharper disable once CppJoinDeclarationAndAssignment
|
||||
fb::HexMapW iceClearedMap;
|
||||
if (hasIce) {
|
||||
// Create ice-cleared map for pathfinding
|
||||
// This prevents AI from considering ice as a valid path toward enemies
|
||||
fb::HexMapW iceClearedMap = CreateIceClearedMap(map);
|
||||
iceClearedMap = CreateIceClearedMap(map);
|
||||
mapToUse = iceClearedMap.Get();
|
||||
}
|
||||
|
||||
|
||||
@@ -71,14 +71,14 @@ auto ToVector(const Units &units) -> vector<const Unit *> {
|
||||
|
||||
auto FallIntoWaterAction::AdjacentsWithTerrain(
|
||||
const Coords &location,
|
||||
const GameState *gameState,
|
||||
const GameStateW &gameState,
|
||||
const PlayerId playerId) -> vector<AdjacentWithTerrain> {
|
||||
const CoordsSet adjacentCoords = HexMapUtils::GetAdjacentCoords(gameState->hex_map(), location);
|
||||
|
||||
vector<AdjacentWithTerrain> vec{};
|
||||
|
||||
for (const Coords &adjCoords : adjacentCoords) {
|
||||
const auto *possibleOccupant = Occupant(gameState->units(), adjCoords);
|
||||
const auto *possibleOccupant = gameState.GetOccupant(adjCoords);
|
||||
if (possibleOccupant &&
|
||||
(possibleOccupant->player_id() == playerId || !possibleOccupant->hidden())) {
|
||||
continue;
|
||||
|
||||
@@ -35,7 +35,7 @@ private:
|
||||
|
||||
static auto AdjacentsWithTerrain(
|
||||
const Coords& location,
|
||||
const GameState* gameState,
|
||||
const GameStateW& gameState,
|
||||
PlayerId playerId) -> vector<AdjacentWithTerrain>;
|
||||
|
||||
[[nodiscard]] auto InternalExecute(
|
||||
|
||||
@@ -164,11 +164,8 @@ auto NewRoundAction::InternalExecute(
|
||||
}
|
||||
|
||||
// Check for ice adjustments
|
||||
ActionList iceAdjustmentAction = iceAdjustmentActionFactory->MakeIceAndSnowAdjustmentActions(
|
||||
runningGameState->hex_map(),
|
||||
*runningGameState->units(),
|
||||
runningGameState->month(),
|
||||
runningGameState->weather());
|
||||
ActionList iceAdjustmentAction =
|
||||
iceAdjustmentActionFactory->MakeIceAndSnowAdjustmentActions(runningGameState);
|
||||
for (const ActionSPtr &action : iceAdjustmentAction) {
|
||||
auto iceAdjustmentResults = action->Execute(currentState, generator);
|
||||
for (const auto &result : iceAdjustmentResults) {
|
||||
|
||||
@@ -16,8 +16,7 @@ using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
|
||||
auto ChooseUndeadCommand(
|
||||
const CommandListSPtr &commands,
|
||||
const Units *units,
|
||||
const HexMap *hexMap,
|
||||
const GameStateW &gameState,
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator) -> CommandSPtr;
|
||||
|
||||
auto PerformUndeadCommandsAction::InternalExecute(
|
||||
@@ -33,11 +32,7 @@ auto PerformUndeadCommandsAction::InternalExecute(
|
||||
/* includeFollowUps=*/false);
|
||||
|
||||
if (commands) {
|
||||
const auto chosenCommand = ChooseUndeadCommand(
|
||||
commands,
|
||||
runningGameState->units(),
|
||||
runningGameState->hex_map(),
|
||||
generator);
|
||||
const auto chosenCommand = ChooseUndeadCommand(commands, runningGameState, generator);
|
||||
|
||||
auto results = chosenCommand->Execute(runningGameState, generator);
|
||||
for (const auto &oneResult : results) {
|
||||
@@ -52,21 +47,21 @@ auto PerformUndeadCommandsAction::InternalExecute(
|
||||
|
||||
auto ChooseUndeadCommand(
|
||||
const CommandListSPtr &commands,
|
||||
const Units *units,
|
||||
const HexMap *hexMap,
|
||||
const GameStateW &gameState,
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator) -> CommandSPtr {
|
||||
CommandList attackCommands;
|
||||
|
||||
for (const auto &command : *commands) {
|
||||
const CommandProto &possibleAttackCommandProto = command->GetCommandProto();
|
||||
if (!possibleAttackCommandProto.has_actor()) continue;
|
||||
const auto *const actor = units->Get(possibleAttackCommandProto.actor().value());
|
||||
const auto *const actor =
|
||||
gameState->units()->Get(possibleAttackCommandProto.actor().value());
|
||||
if (actor->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
(possibleAttackCommandProto.type() == net::eagle0::shardok::common::MELEE_COMMAND ||
|
||||
possibleAttackCommandProto.type() == net::eagle0::shardok::common::CHARGE_COMMAND)) {
|
||||
// If this command is to attack the formerly commanding necromancer, take it.
|
||||
const auto *const occupant =
|
||||
Occupant(units, FromCoordsProto(possibleAttackCommandProto.target()));
|
||||
gameState.GetOccupant(FromCoordsProto(possibleAttackCommandProto.target()));
|
||||
if (actor->targeted_unit() != -1 && occupant &&
|
||||
occupant->unit_id() == actor->targeted_unit()) {
|
||||
return command;
|
||||
@@ -89,14 +84,15 @@ auto ChooseUndeadCommand(
|
||||
continue;
|
||||
|
||||
// See if this move would allow us to move adjacent the enemy
|
||||
const auto *const actor = units->Get(possibleMoveOrEndTurnCommand.actor().value());
|
||||
const auto *const actor =
|
||||
gameState->units()->Get(possibleMoveOrEndTurnCommand.actor().value());
|
||||
if (actor->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
actor->targeted_unit() != -1) {
|
||||
const UnitId targetedUnitId = actor->targeted_unit();
|
||||
for (const Coords &adjCoords : HexMapUtils::GetAdjacentCoords(
|
||||
hexMap,
|
||||
gameState->hex_map(),
|
||||
FromCoordsProto(possibleMoveOrEndTurnCommand.target()))) {
|
||||
const auto *const occupant = Occupant(units, adjCoords);
|
||||
const auto *const occupant = gameState.GetOccupant(adjCoords);
|
||||
if (occupant && occupant->unit_id() == targetedUnitId) {
|
||||
if (bestMoveCommand == nullptr ||
|
||||
bestMoveCommand->GetCommandProto().action_points().value() >
|
||||
|
||||
+6
-4
@@ -88,7 +88,8 @@ void BraveWaterCommandFactory::AddAvailableBraveWaterCommands(
|
||||
const Coords &position,
|
||||
const WeatherFb *weather,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const {
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup) const {
|
||||
if (!unit->has_attached_hero()) return;
|
||||
if (unit->attached_hero().vigor() < settings.Backing().minimum_vigor_to_act()) return;
|
||||
|
||||
@@ -108,7 +109,7 @@ void BraveWaterCommandFactory::AddAvailableBraveWaterCommands(
|
||||
for (const Coords &possibleTarget : tilesAcrossWater) {
|
||||
const auto terrain = GetTerrain(hexMap, possibleTarget);
|
||||
|
||||
if (IsBelievedEmpty(unit->player_id(), possibleTarget, units) &&
|
||||
if (occupancyLookup.IsBelievedEmpty(unit->player_id(), possibleTarget) &&
|
||||
!IsWater(terrain->type()) &&
|
||||
battalionType->GetCostToEnterTerrain(terrain).type !=
|
||||
ActionCost::ActionCostType::impossible) {
|
||||
@@ -118,7 +119,7 @@ void BraveWaterCommandFactory::AddAvailableBraveWaterCommands(
|
||||
|
||||
for (const auto &target : braveWaterTargets) {
|
||||
std::optional<UnitId> ambusher = std::optional<UnitId>();
|
||||
const auto &targetOccupant = Occupant(units, target);
|
||||
const auto &targetOccupant = occupancyLookup.GetOccupant(target);
|
||||
if (targetOccupant) { ambusher = targetOccupant->unit_id(); }
|
||||
commands.push_back(BraveWaterCommandFactory(settings)
|
||||
.MakeBraveWaterCommand(unit, ambusher, target, weather));
|
||||
@@ -136,7 +137,8 @@ void BraveWaterCommandFactory::AddAvailableCommands(
|
||||
params.position,
|
||||
params.weatherFb,
|
||||
params.hexMap,
|
||||
params.units);
|
||||
params.units,
|
||||
params.occupancyLookup);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
+2
-1
@@ -34,7 +34,8 @@ public:
|
||||
const Coords &position,
|
||||
const WeatherFb *weather,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const;
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup) const;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
};
|
||||
|
||||
+3
-1
@@ -23,6 +23,7 @@ void BuildBridgeCommandFactory::AddAvailableBuildBridgeCommands(
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const {
|
||||
const PlayerId playerId = unit->player_id();
|
||||
if (!unit->has_attached_hero()) return;
|
||||
@@ -36,7 +37,7 @@ void BuildBridgeCommandFactory::AddAvailableBuildBridgeCommands(
|
||||
settings.ActionCostFor(settings.Backing().build_bridge_action_point_cost());
|
||||
|
||||
if (cost.IsPossible(remainingActionPoints)) {
|
||||
const bool nearbyForest = HasForestAccess(position, units, hexMap, allyPids, playerId);
|
||||
const bool nearbyForest = occupancyLookup.HasForestAccess(position, allyPids, playerId);
|
||||
|
||||
for (const Coords &adjCoords : HexMapUtils::GetAdjacentCoords(hexMap, position)) {
|
||||
if (IsWater(GetTerrain(hexMap, adjCoords)->type()) &&
|
||||
@@ -67,6 +68,7 @@ void BuildBridgeCommandFactory::AddAvailableCommands(
|
||||
params.position,
|
||||
params.hexMap,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.allyPids);
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ public:
|
||||
const Coords& position,
|
||||
const HexMap* hexMap,
|
||||
const Units* units,
|
||||
const OccupancyLookup& occupancyLookup,
|
||||
const vector<PlayerId>& allyPids) const;
|
||||
|
||||
void AddAvailableCommands(CommandList& commands, const CommandParams& params) const override;
|
||||
|
||||
+6
-4
@@ -16,7 +16,7 @@ void ChallengeDuelCommandFactory::AddAvailableChallengeDuelCommands(
|
||||
const Coords &position,
|
||||
const ActionPoints remainingActionPoints,
|
||||
const HexMap *map,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const {
|
||||
if (actingUnit->has_attached_hero() &&
|
||||
actingUnit->attached_hero().profession_info().profession() ==
|
||||
@@ -31,8 +31,10 @@ void ChallengeDuelCommandFactory::AddAvailableChallengeDuelCommands(
|
||||
|
||||
if (destinationTerrain->modifier().castle().present()) { continue; }
|
||||
|
||||
const auto *enemyOccupant =
|
||||
KnownEnemyOccupant(actingUnit->player_id(), units, allyPids, adjCoords);
|
||||
const auto *enemyOccupant = occupancyLookup.GetKnownEnemyOccupant(
|
||||
actingUnit->player_id(),
|
||||
allyPids,
|
||||
adjCoords);
|
||||
|
||||
if (enemyOccupant) {
|
||||
const auto *targetUnit = enemyOccupant;
|
||||
@@ -65,7 +67,7 @@ void ChallengeDuelCommandFactory::AddAvailableCommands(
|
||||
params.position,
|
||||
params.remainingActionPoints,
|
||||
params.hexMap,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.allyPids);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ public:
|
||||
const Coords &position,
|
||||
ActionPoints remainingActionPoints,
|
||||
const HexMap *map,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
|
||||
@@ -48,6 +48,7 @@ void ChargeCommandFactory::AddAvailableChargeCommands(
|
||||
const PossibleChargees &possibleChargees,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const {
|
||||
if (!isEligibleCharger) return;
|
||||
bool foundOne = false;
|
||||
@@ -64,7 +65,7 @@ void ChargeCommandFactory::AddAvailableChargeCommands(
|
||||
const AttackOrientation orientation = AttackOrientationForAttack(
|
||||
settings,
|
||||
unit->player_id(),
|
||||
units,
|
||||
occupancyLookup,
|
||||
hexMap,
|
||||
unitLocation,
|
||||
targetCoords);
|
||||
@@ -74,7 +75,13 @@ void ChargeCommandFactory::AddAvailableChargeCommands(
|
||||
}
|
||||
}
|
||||
if (foundOne) {
|
||||
if (IsInEnemyZoc(settings, unit->player_id(), units, hexMap, allyPids, position)) {
|
||||
if (IsInEnemyZoc(
|
||||
settings,
|
||||
unit->player_id(),
|
||||
occupancyLookup,
|
||||
hexMap,
|
||||
allyPids,
|
||||
position)) {
|
||||
commands.push_back(std::make_shared<UnitRestCommand>(
|
||||
unit->player_id(),
|
||||
unit->unit_id(),
|
||||
@@ -97,6 +104,7 @@ void ChargeCommandFactory::AddAvailableCommands(CommandList &commands, const Com
|
||||
params.possibleChargees,
|
||||
params.hexMap,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.allyPids);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ public:
|
||||
const PossibleChargees &possibleChargees,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef EAGLE0_COMMANDFACTORY_HPP
|
||||
#define EAGLE0_COMMANDFACTORY_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
@@ -36,6 +37,7 @@ public:
|
||||
const vector<PlayerId>& allyPids;
|
||||
const bool isAttacker;
|
||||
const bool unitMovedIntoZoc;
|
||||
const OccupancyLookup& occupancyLookup;
|
||||
};
|
||||
|
||||
virtual void AddAvailableCommands(CommandList& commands, const CommandParams& params) const = 0;
|
||||
|
||||
@@ -17,7 +17,7 @@ void FearCommandFactory::AddAvailableFearCommands(
|
||||
const ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const {
|
||||
const auto &hero = unit->attached_hero();
|
||||
|
||||
@@ -31,8 +31,10 @@ void FearCommandFactory::AddAvailableFearCommands(
|
||||
const auto coordsSet = TilesWithExactDistance(hexMap, position, distance);
|
||||
|
||||
for (const Coords &fearCoords : coordsSet) {
|
||||
const auto *enemyOccupantOptional =
|
||||
KnownEnemyOccupant(unit->player_id(), units, allyPids, fearCoords);
|
||||
const auto *enemyOccupantOptional = occupancyLookup.GetKnownEnemyOccupant(
|
||||
unit->player_id(),
|
||||
allyPids,
|
||||
fearCoords);
|
||||
if (!enemyOccupantOptional) continue;
|
||||
const Unit *potentialTarget = enemyOccupantOptional;
|
||||
if (!settings.GetBattalionType(potentialTarget->battalion().type())
|
||||
@@ -78,7 +80,7 @@ void FearCommandFactory::AddAvailableCommands(
|
||||
params.remainingActionPoints,
|
||||
params.position,
|
||||
params.hexMap,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.allyPids);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public:
|
||||
ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
|
||||
@@ -20,6 +20,7 @@ void FleeCommandFactory::AddAvailableFleeCommands(
|
||||
CommandList &commands,
|
||||
const Unit *unit,
|
||||
const Units *allUnits,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *map,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const ActionPoints remainingActionPoints) const {
|
||||
@@ -39,7 +40,7 @@ void FleeCommandFactory::AddAvailableFleeCommands(
|
||||
auto perImpassableTile = settings.Backing().flee_success_per_adjacent_impassable_tile();
|
||||
|
||||
for (const auto &adjTile : HexMapUtils::GetAdjacentTiles(map, unit->location())) {
|
||||
const auto *occupant = Occupant(allUnits, adjTile.coords);
|
||||
const auto *occupant = occupancyLookup.GetOccupant(adjTile.coords);
|
||||
if (occupant && !occupant->hidden()) {
|
||||
if (common::Contains(allyPids, occupant->player_id())) {
|
||||
adjacentFriendliesMod += perAdjacentFriendly;
|
||||
@@ -53,7 +54,7 @@ void FleeCommandFactory::AddAvailableFleeCommands(
|
||||
|
||||
auto adjustmentForTwoAway = settings.Backing().flee_success_adjustment_for_two_away();
|
||||
for (const auto &twoAwayCoords : TilesWithExactDistance(map, unit->location(), 2)) {
|
||||
const auto *occupant = Occupant(allUnits, twoAwayCoords);
|
||||
const auto *occupant = occupancyLookup.GetOccupant(twoAwayCoords);
|
||||
if (occupant && !occupant->hidden()) {
|
||||
if (common::Contains(allyPids, occupant->player_id())) {
|
||||
adjacentFriendliesMod += int32_t(perAdjacentFriendly * adjustmentForTwoAway);
|
||||
@@ -95,6 +96,7 @@ void FleeCommandFactory::AddAvailableCommands(
|
||||
commands,
|
||||
params.unit,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.hexMap,
|
||||
params.allyPids,
|
||||
params.remainingActionPoints);
|
||||
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
CommandList &commands,
|
||||
const Unit *unit,
|
||||
const Units *allUnits,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *map,
|
||||
const vector<PlayerId> &allyPids,
|
||||
ActionPoints remainingActionPoints) const;
|
||||
|
||||
+5
-3
@@ -26,7 +26,8 @@ void FreezeWaterCommandFactory::AddAvailableFreezeWaterCommands(
|
||||
const Coords &position,
|
||||
const WeatherFb *weather,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const {
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup) const {
|
||||
if (!unit->has_attached_hero()) return;
|
||||
const auto &hero = unit->attached_hero();
|
||||
|
||||
@@ -41,7 +42,7 @@ void FreezeWaterCommandFactory::AddAvailableFreezeWaterCommands(
|
||||
const auto &targetTerrain = GetTerrain(hexMap, coords);
|
||||
if (IsWater(targetTerrain->type()) && !WaterIsFullyFrozen(targetTerrain)) {
|
||||
PercentileRollOdds odds = GetFreezeWaterOdds(settings, weather, hero.wisdom());
|
||||
const auto *underwaterOccupant = Occupant(units, coords);
|
||||
const auto *underwaterOccupant = occupancyLookup.GetOccupant(coords);
|
||||
|
||||
if (underwaterOccupant == nullptr ||
|
||||
settings.GetBattalionType(underwaterOccupant->battalion().type())
|
||||
@@ -85,7 +86,8 @@ void FreezeWaterCommandFactory::AddAvailableCommands(
|
||||
params.position,
|
||||
params.weatherFb,
|
||||
params.hexMap,
|
||||
params.units);
|
||||
params.units,
|
||||
params.occupancyLookup);
|
||||
}
|
||||
|
||||
auto GetFreezeWaterOdds(
|
||||
|
||||
+2
-1
@@ -29,7 +29,8 @@ public:
|
||||
const Coords &position,
|
||||
const WeatherFb *weather,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const;
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup) const;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
};
|
||||
|
||||
@@ -29,11 +29,11 @@ auto HideCommandFactory::PositionIsHideable(
|
||||
CommandList &existingCommands,
|
||||
const HexMap *hexMap,
|
||||
const Coords &target,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const std::vector<shardok::PlayerId> &allyPids) const -> bool {
|
||||
if (!AllowsHiding(GetTerrain(hexMap, target))) return false;
|
||||
|
||||
const auto occupant = Occupant(units, target);
|
||||
const auto occupant = occupancyLookup.GetOccupant(target);
|
||||
if (occupant) {
|
||||
if (occupant->player_id() == actor->player_id()) return false;
|
||||
if (common::Contains(allyPids, occupant->player_id())) return false;
|
||||
@@ -42,7 +42,7 @@ auto HideCommandFactory::PositionIsHideable(
|
||||
|
||||
for (const Coords &adjCoords : HexMapUtils::GetAdjacentCoords(hexMap, target)) {
|
||||
const auto oneOverOccupant =
|
||||
KnownEnemyOccupant(actor->player_id(), units, allyPids, adjCoords);
|
||||
occupancyLookup.GetKnownEnemyOccupant(actor->player_id(), allyPids, adjCoords);
|
||||
if (!oneOverOccupant) continue;
|
||||
if (!oneOverOccupant->has_attached_hero()) continue;
|
||||
|
||||
@@ -64,19 +64,21 @@ auto HideCommandFactory::AddAvailableHideCommands(
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const std::vector<shardok::PlayerId> &allyPids) const -> void {
|
||||
if (!CanHide(settings, actor)) return;
|
||||
if (actor->hidden()) return;
|
||||
if (remainingActionPoints < settings.Backing().hide_action_point_cost()) return;
|
||||
if (IsInEnemyZoc(settings, actor->player_id(), units, hexMap, allyPids, position)) return;
|
||||
if (IsInEnemyZoc(settings, actor->player_id(), occupancyLookup, hexMap, allyPids, position))
|
||||
return;
|
||||
|
||||
CoordsSet hideablePositions(hexMap);
|
||||
if (PositionIsHideable(actor, existingCommands, hexMap, position, units, allyPids)) {
|
||||
if (PositionIsHideable(actor, existingCommands, hexMap, position, occupancyLookup, allyPids)) {
|
||||
hideablePositions.Add(position);
|
||||
}
|
||||
|
||||
for (const Coords &adj : HexMapUtils::GetAdjacentCoords(hexMap, position)) {
|
||||
if (PositionIsHideable(actor, existingCommands, hexMap, adj, units, allyPids)) {
|
||||
if (PositionIsHideable(actor, existingCommands, hexMap, adj, occupancyLookup, allyPids)) {
|
||||
hideablePositions.Add(adj);
|
||||
}
|
||||
}
|
||||
@@ -98,6 +100,7 @@ void HideCommandFactory::AddAvailableCommands(
|
||||
params.position,
|
||||
params.hexMap,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.allyPids);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ private:
|
||||
CommandList &existingCommands,
|
||||
const HexMap *hexMap,
|
||||
const Coords &target,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const std::vector<shardok::PlayerId> &allyPids) const -> bool;
|
||||
|
||||
public:
|
||||
@@ -37,6 +37,7 @@ public:
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const -> void;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
|
||||
+6
-4
@@ -38,7 +38,7 @@ auto LightningBoltCommandFactory::AddAvailableLightningBoltCommands(
|
||||
const ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const -> void {
|
||||
if (!CanLightningBolt(settings, unit)) { return; }
|
||||
|
||||
@@ -50,8 +50,10 @@ auto LightningBoltCommandFactory::AddAvailableLightningBoltCommands(
|
||||
CoordsSet coords = TilesWithExactDistance(hexMap, position, distance);
|
||||
|
||||
for (const Coords &lightningCoords : coords) {
|
||||
const auto &occupantOptional =
|
||||
KnownEnemyOccupant(unit->player_id(), units, allyPids, lightningCoords);
|
||||
const auto &occupantOptional = occupancyLookup.GetKnownEnemyOccupant(
|
||||
unit->player_id(),
|
||||
allyPids,
|
||||
lightningCoords);
|
||||
if (occupantOptional) {
|
||||
existingCommands.emplace_back(MakeLightningBoltCommand(unit, occupantOptional));
|
||||
}
|
||||
@@ -68,7 +70,7 @@ void LightningBoltCommandFactory::AddAvailableCommands(
|
||||
params.remainingActionPoints,
|
||||
params.position,
|
||||
params.hexMap,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.allyPids);
|
||||
}
|
||||
} // namespace shardok
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ public:
|
||||
ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const -> void;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
|
||||
@@ -51,12 +51,14 @@ auto AdjacentMoveDestinations(
|
||||
ActionPoints remainingActionPoints,
|
||||
bool inEnemyZoc,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *hexMap,
|
||||
const SettingsGetter &settings,
|
||||
const vector<PlayerId> &allyPids) -> vector<AccumulatedMoveInfo>;
|
||||
|
||||
auto ConstructMoveDestinations(
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Unit *movingUnit,
|
||||
ActionPoints startingActionPoints,
|
||||
@@ -69,7 +71,8 @@ void MoveCommandFactory::AddAvailableMoveCommands(
|
||||
const Unit *movingUnit,
|
||||
const ActionPoints remainingActionPoints,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const {
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup) const {
|
||||
if (movingUnit->stun_rounds_remaining() > 0) return;
|
||||
if (movingUnit->has_attached_hero() &&
|
||||
movingUnit->attached_hero().vigor() < settings.Backing().minimum_vigor_to_act()) {
|
||||
@@ -80,6 +83,7 @@ void MoveCommandFactory::AddAvailableMoveCommands(
|
||||
|
||||
const auto destinations = ConstructMoveDestinations(
|
||||
units,
|
||||
occupancyLookup,
|
||||
allyPids,
|
||||
movingUnit,
|
||||
remainingActionPoints,
|
||||
@@ -116,19 +120,21 @@ void MoveCommandFactory::AddAvailableCommands(
|
||||
params.unit,
|
||||
params.remainingActionPoints,
|
||||
params.hexMap,
|
||||
params.units);
|
||||
params.units,
|
||||
params.occupancyLookup);
|
||||
}
|
||||
}
|
||||
|
||||
// OccupancyLookup version of UnoccupiedAdjacentCoords
|
||||
auto UnoccupiedAdjacentCoords(
|
||||
const PlayerId playerId,
|
||||
const HexMap *hexMap,
|
||||
const Coords &from,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) -> CoordsSet {
|
||||
CoordsSet toReturn(hexMap);
|
||||
for (const auto &adjacentTile : HexMapUtils::GetAdjacentCoords(hexMap, from)) {
|
||||
const auto *occupant = KnownOccupant(playerId, units, allyPids, adjacentTile);
|
||||
const auto *occupant = occupancyLookup.GetKnownOccupant(playerId, allyPids, adjacentTile);
|
||||
if (!occupant) { toReturn.Add(adjacentTile); }
|
||||
}
|
||||
return toReturn;
|
||||
@@ -140,6 +146,7 @@ auto AdjacentMoveDestinations(
|
||||
const ActionPoints remainingActionPoints,
|
||||
const bool inEnemyZoc,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *hexMap,
|
||||
const SettingsGetter &settings,
|
||||
const vector<PlayerId> &allyPids) -> vector<AccumulatedMoveInfo> {
|
||||
@@ -149,7 +156,7 @@ auto AdjacentMoveDestinations(
|
||||
movingUnit->player_id(),
|
||||
hexMap,
|
||||
baseInfo.endLocation,
|
||||
units,
|
||||
occupancyLookup,
|
||||
allyPids);
|
||||
|
||||
validDestinations.reserve(nextCandidates.size());
|
||||
@@ -179,6 +186,7 @@ auto AdjacentMoveDestinations(
|
||||
|
||||
auto ConstructMoveDestinations(
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Unit *movingUnit,
|
||||
const ActionPoints startingActionPoints,
|
||||
@@ -204,6 +212,7 @@ auto ConstructMoveDestinations(
|
||||
startingActionPoints,
|
||||
inEnemyZoc,
|
||||
units,
|
||||
occupancyLookup,
|
||||
hexMap,
|
||||
settings,
|
||||
allyPids);
|
||||
@@ -238,6 +247,7 @@ auto ConstructMoveDestinations(
|
||||
remainingActionPoints,
|
||||
newInEnemyZoc,
|
||||
units,
|
||||
occupancyLookup,
|
||||
hexMap,
|
||||
settings,
|
||||
allyPids);
|
||||
|
||||
@@ -31,7 +31,8 @@ public:
|
||||
const Unit *movingUnit,
|
||||
ActionPoints remainingActionPoints,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const;
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup) const;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
|
||||
|
||||
+4
-3
@@ -21,7 +21,7 @@ void RaiseDeadCommandFactory::AddAvailableRaiseDeadCommands(
|
||||
const ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const {
|
||||
const OccupancyLookup &occupancyLookup) const {
|
||||
// requires a hero
|
||||
if (!unit->has_attached_hero()) { return; }
|
||||
|
||||
@@ -45,7 +45,8 @@ void RaiseDeadCommandFactory::AddAvailableRaiseDeadCommands(
|
||||
const CoordsSet coordsSet = TilesWithExactDistance(hexMap, position, distance);
|
||||
|
||||
for (const Coords &raiseDeadCoords : coordsSet) {
|
||||
if (!IsBelievedEmpty(unit->player_id(), raiseDeadCoords, units)) continue;
|
||||
if (!occupancyLookup.IsBelievedEmpty(unit->player_id(), raiseDeadCoords))
|
||||
continue;
|
||||
if (undeadType->GetCostToEnterTerrain(GetTerrain(hexMap, raiseDeadCoords))
|
||||
.type == ActionCost::impossible)
|
||||
continue;
|
||||
@@ -78,7 +79,7 @@ void RaiseDeadCommandFactory::AddAvailableCommands(
|
||||
params.remainingActionPoints,
|
||||
params.position,
|
||||
params.hexMap,
|
||||
params.units);
|
||||
params.occupancyLookup);
|
||||
}
|
||||
|
||||
auto GetRaiseDeadOdds(const SettingsGetter &settings, double charisma, double intelligence)
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ public:
|
||||
ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const;
|
||||
const OccupancyLookup &occupancyLookup) const;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ auto ReduceCommandFactory::AddAvailableReduceCommands(
|
||||
const Coords &position,
|
||||
const HexMap *map,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Units *units) const -> void {
|
||||
const OccupancyLookup &occupancyLookup) const -> void {
|
||||
const PlayerId playerId = unit->player_id();
|
||||
if (!unit->has_attached_hero()) return;
|
||||
|
||||
@@ -41,11 +41,11 @@ auto ReduceCommandFactory::AddAvailableReduceCommands(
|
||||
// Don't allow reducing own position
|
||||
if (reduceCoords == position) continue;
|
||||
|
||||
const auto *const occupant = Occupant(units, reduceCoords);
|
||||
const auto *const occupant = occupancyLookup.GetOccupant(reduceCoords);
|
||||
if (occupant && occupant->player_id() == playerId) continue;
|
||||
|
||||
const auto *const enemyOccupant =
|
||||
KnownEnemyOccupant(playerId, units, allyPids, reduceCoords);
|
||||
occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, reduceCoords);
|
||||
|
||||
const auto *terrain = GetTerrain(map, reduceCoords);
|
||||
|
||||
@@ -82,7 +82,7 @@ void ReduceCommandFactory::AddAvailableCommands(CommandList &commands, const Com
|
||||
params.position,
|
||||
params.hexMap,
|
||||
params.allyPids,
|
||||
params.units);
|
||||
params.occupancyLookup);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -27,7 +27,7 @@ public:
|
||||
const Coords &position,
|
||||
const HexMap *map,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Units *units) const -> void;
|
||||
const OccupancyLookup &occupancyLookup) const -> void;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
};
|
||||
|
||||
+5
-3
@@ -31,7 +31,8 @@ auto ReinforceCommandFactory::AddAvailableReinforceCommands(
|
||||
const Unit *reinforcingUnit,
|
||||
const ActionPoints remainingActionPoints,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const -> void {
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup) const -> void {
|
||||
const ActionCost cost =
|
||||
settings.ActionCostFor(settings.Backing().reinforce_action_point_cost());
|
||||
if (!cost.IsPossible(remainingActionPoints)) return;
|
||||
@@ -60,7 +61,7 @@ auto ReinforceCommandFactory::AddAvailableReinforceCommands(
|
||||
for (const auto &position : *startingPositions) {
|
||||
const int positionIndex = position->row() * hexMap->column_count() + position->column();
|
||||
// Position must not be occupied and must not be on fire
|
||||
if (!Occupant(units, *position) &&
|
||||
if (!occupancyLookup.GetOccupant(*position) &&
|
||||
!hexMap->terrain()->Get(positionIndex)->modifier().fire().present()) {
|
||||
existingCommands.push_back(MakeReinforceCommand(
|
||||
reinforcingUnit->player_id(),
|
||||
@@ -79,7 +80,8 @@ void ReinforceCommandFactory::AddAvailableCommands(
|
||||
params.unit,
|
||||
params.remainingActionPoints,
|
||||
params.hexMap,
|
||||
params.units);
|
||||
params.units,
|
||||
params.occupancyLookup);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@ public:
|
||||
const Unit *reinforcingUnit,
|
||||
ActionPoints remainingActionPoints,
|
||||
const HexMap *hexMap,
|
||||
const Units *units) const -> void;
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup) const -> void;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ void RepairCommandFactory::AddAvailableRepairCommands(
|
||||
const ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const {
|
||||
const PlayerId playerId = unit->player_id();
|
||||
if (!unit->has_attached_hero()) return;
|
||||
@@ -41,7 +41,7 @@ void RepairCommandFactory::AddAvailableRepairCommands(
|
||||
coordsToCheck.Add(position);
|
||||
|
||||
for (const Coords &coords : coordsToCheck) {
|
||||
if (KnownEnemyOccupant(playerId, units, allyPids, coords)) continue;
|
||||
if (occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, coords)) continue;
|
||||
const auto *terrain = GetTerrain(hexMap, coords);
|
||||
if ((terrain->modifier().bridge().present() &&
|
||||
terrain->modifier().bridge().integrity() < 100.0) ||
|
||||
@@ -69,7 +69,7 @@ void RepairCommandFactory::AddAvailableCommands(CommandList &commands, const Com
|
||||
params.remainingActionPoints,
|
||||
params.position,
|
||||
params.hexMap,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.allyPids);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public:
|
||||
ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
|
||||
+5
-3
@@ -29,7 +29,8 @@ auto StartFireCommandFactory::AddAvailableStartFireCommands(
|
||||
const HexMap *hexMap,
|
||||
const ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const WeatherFb *weather) const -> void {
|
||||
const WeatherFb *weather,
|
||||
const OccupancyLookup &occupancyLookup) const -> void {
|
||||
const PlayerId playerId = unit->player_id();
|
||||
if (!unit->has_attached_hero()) return;
|
||||
const auto &hero = unit->attached_hero();
|
||||
@@ -43,7 +44,7 @@ auto StartFireCommandFactory::AddAvailableStartFireCommands(
|
||||
if (GetTerrain(hexMap, adjCoords)->modifier().fire().present()) { continue; }
|
||||
|
||||
const auto &occupantOptional =
|
||||
FriendlyOccupant(playerId, units, allyPids, adjCoords);
|
||||
occupancyLookup.GetFriendlyOccupant(playerId, allyPids, adjCoords);
|
||||
if (!occupantOptional) {
|
||||
const auto *const targetTerrain = GetTerrain(hexMap, adjCoords);
|
||||
PercentileRollOdds odds = GetStartFireOdds(
|
||||
@@ -74,6 +75,7 @@ void StartFireCommandFactory::AddAvailableCommands(
|
||||
params.hexMap,
|
||||
params.remainingActionPoints,
|
||||
params.position,
|
||||
params.weatherFb);
|
||||
params.weatherFb,
|
||||
params.occupancyLookup);
|
||||
}
|
||||
} // namespace shardok
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@ public:
|
||||
const HexMap *hexMap,
|
||||
ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const WeatherFb *weather) const -> void;
|
||||
const WeatherFb *weather,
|
||||
const OccupancyLookup &occupancyLookup) const -> void;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
};
|
||||
|
||||
+6
-4
@@ -66,7 +66,7 @@ void ArcheryCommandFactory::AddAvailableArcheryCommands(
|
||||
const Coords &position,
|
||||
const WeatherFb *weather,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const {
|
||||
if (unit->volleys_remaining() < 1) return;
|
||||
if (!unit->can_archery()) return;
|
||||
@@ -85,8 +85,10 @@ void ArcheryCommandFactory::AddAvailableArcheryCommands(
|
||||
}
|
||||
|
||||
for (const Coords &archeryCoords : targetCoords) {
|
||||
const auto *const enemyOccupant =
|
||||
KnownEnemyOccupant(unit->player_id(), units, allyPids, archeryCoords);
|
||||
const auto *const enemyOccupant = occupancyLookup.GetKnownEnemyOccupant(
|
||||
unit->player_id(),
|
||||
allyPids,
|
||||
archeryCoords);
|
||||
if (enemyOccupant) {
|
||||
commands.push_back(MakeArcheryCommand(
|
||||
unit,
|
||||
@@ -105,7 +107,7 @@ void ArcheryCommandFactory::AddAvailableCommands(CommandList &commands, const Co
|
||||
params.position,
|
||||
params.weatherFb,
|
||||
params.hexMap,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.allyPids);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ public:
|
||||
const Coords &position,
|
||||
const WeatherFb *weather,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
+3
-3
@@ -51,11 +51,11 @@ void MeleeCommandFactory::AddAvailableMeleeCommands(
|
||||
const ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const {
|
||||
for (const Coords &adjCoords : HexMapUtils::GetAdjacentCoords(hexMap, position)) {
|
||||
const auto *enemyOccupant =
|
||||
KnownEnemyOccupant(unit->player_id(), units, allyPids, adjCoords);
|
||||
occupancyLookup.GetKnownEnemyOccupant(unit->player_id(), allyPids, adjCoords);
|
||||
|
||||
if (enemyOccupant) {
|
||||
ActionCost costForMelee =
|
||||
@@ -75,7 +75,7 @@ void MeleeCommandFactory::AddAvailableCommands(
|
||||
params.remainingActionPoints,
|
||||
params.position,
|
||||
params.hexMap,
|
||||
params.units,
|
||||
params.occupancyLookup,
|
||||
params.allyPids);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ public:
|
||||
ActionPoints remainingActionPoints,
|
||||
const Coords &position,
|
||||
const HexMap *hexMap,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const vector<PlayerId> &allyPids) const;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
@@ -59,10 +59,11 @@ auto MeleeCommand::InternalExecuteWithRoll(
|
||||
bool isAmbush = attacker->hidden() && PercentileRollSucceeds(ambushOdds, ambushRoll) &&
|
||||
CombatUtils::DefenderTerrainAllowsAmbush(*defenderTerrain);
|
||||
|
||||
OccupancyLookup occupancyLookup(currentState.operator->());
|
||||
AttackOrientation attackOrientation = AttackOrientationForAttack(
|
||||
settings,
|
||||
attacker->player_id(),
|
||||
currentState->units(),
|
||||
occupancyLookup,
|
||||
currentState->hex_map(),
|
||||
attacker->location(),
|
||||
defender->location());
|
||||
|
||||
@@ -52,8 +52,9 @@ auto shardok::MoveCommand::InternalExecute(
|
||||
for (const Coords& destination : interimTargets) {
|
||||
const auto* destinationTerrain = GetTerrain(map, destination);
|
||||
bool startedInEnemyZoc = enemyZocCoords.Contains(origin);
|
||||
auto occupancyLookup = currentState.CreateOccupancyLookup();
|
||||
vector<const Unit*> knownAdjacentEnemies =
|
||||
KnownAdjacentEnemies(player, allUnits, map, allyPids, origin);
|
||||
occupancyLookup.GetKnownAdjacentEnemies(player, allyPids, origin);
|
||||
|
||||
MutatingSpendActionPoints(
|
||||
&mover,
|
||||
@@ -128,7 +129,7 @@ auto shardok::MoveCommand::InternalExecute(
|
||||
|
||||
if (!startedInEnemyZoc) {
|
||||
vector<const Unit*> newAdjacentEnemies =
|
||||
KnownAdjacentEnemies(player, allUnits, map, allyPids, destination);
|
||||
occupancyLookup.GetKnownAdjacentEnemies(player, allyPids, destination);
|
||||
|
||||
for (const Unit* possibleChargee : newAdjacentEnemies) {
|
||||
if (std::find_if(
|
||||
|
||||
@@ -42,10 +42,9 @@ auto ReduceCommand::InternalExecute(
|
||||
|
||||
const auto& actingHero = actor->attached_hero();
|
||||
|
||||
bool hasForestAccess = HasForestAccess(
|
||||
auto occupancyLookup = currentState.CreateOccupancyLookup();
|
||||
bool hasForestAccess = occupancyLookup.HasForestAccess(
|
||||
actor->location(),
|
||||
currentState->units(),
|
||||
currentState->hex_map(),
|
||||
AlliedPids(currentState, GetPlayerId()),
|
||||
GetPlayerId());
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ cc_library(
|
||||
],
|
||||
deps = [
|
||||
":hex_map_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/unit",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:unit_view_cc_proto",
|
||||
|
||||
@@ -65,27 +65,6 @@ auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain * {
|
||||
return map->terrain()->GetMutableObject(coords.row() * map->column_count() + coords.column());
|
||||
}
|
||||
|
||||
auto HasForestAccess(
|
||||
const Coords &coords,
|
||||
const Units *units,
|
||||
const HexMap *hexMap,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const PlayerId player) -> bool {
|
||||
if (GetTerrain(hexMap, coords)->type() ==
|
||||
net::eagle0::shardok::storage::fb::Terrain_::Type_FOREST)
|
||||
return true;
|
||||
|
||||
const CoordsSet adjacentCoords = HexMapUtils::GetAdjacentCoords(hexMap, coords);
|
||||
|
||||
return any_of(begin(adjacentCoords), end(adjacentCoords), [&](const Coords &adjCoords) {
|
||||
if (KnownEnemyOccupant(player, units, allyPids, adjCoords)) return false;
|
||||
if (GetTerrain(hexMap, adjCoords)->type() ==
|
||||
net::eagle0::shardok::storage::fb::Terrain_::Type_FOREST)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const std::vector<CoordsSet> HexMapUtils::standardCoords =
|
||||
HexMapUtils::ComputeAllStandardAdjacentCoords();
|
||||
|
||||
@@ -503,19 +482,6 @@ auto MaybeTileInDirection(
|
||||
return {};
|
||||
}
|
||||
|
||||
auto MaybeOccupantInDirection(
|
||||
const HexMap *map,
|
||||
const Units *units,
|
||||
const Coords &coords,
|
||||
const HexMapDirection direction) -> const Unit * {
|
||||
const auto &maybeTile = MaybeTileInDirection(map, coords, direction);
|
||||
if (maybeTile.has_value()) {
|
||||
return Occupant(units, maybeTile.value());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
auto GetAllCoords(const HexMap *hexMap) -> CoordsSet {
|
||||
CoordsSet cs(hexMap);
|
||||
for (int row = 0; row < hexMap->row_count(); row++) {
|
||||
@@ -645,49 +611,8 @@ auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet {
|
||||
return criticalTileLocations;
|
||||
}
|
||||
|
||||
auto IsBelievedEmpty(const PlayerId playerId, const Coords &coords, const Units *units) -> bool {
|
||||
const Unit *occupantOptional = Occupant(units, coords);
|
||||
if (occupantOptional) {
|
||||
return (occupantOptional->hidden() && occupantOptional->player_id() != playerId);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
auto DistanceBetween(const Coords &c1, const Coords &c2) -> int {
|
||||
return CubeDistance(OffsetToCube(c1), OffsetToCube(c2));
|
||||
}
|
||||
|
||||
auto KnownAdjacentEnemies(
|
||||
const PlayerId playerId,
|
||||
const Units *units,
|
||||
const HexMap *map,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> vector<const Unit *> {
|
||||
vector<const Unit *> knownAdjacentEnemies{};
|
||||
|
||||
for (const Coords &adjCoords : HexMapUtils::GetAdjacentCoords(map, coords)) {
|
||||
const auto *possibleEnemy = KnownEnemyOccupant(playerId, units, allyPids, adjCoords);
|
||||
if (possibleEnemy) { knownAdjacentEnemies.push_back(possibleEnemy); }
|
||||
}
|
||||
|
||||
return knownAdjacentEnemies;
|
||||
}
|
||||
|
||||
auto KnownEnemyOccupant(
|
||||
const PlayerId playerId,
|
||||
const Units *units,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> const Unit * {
|
||||
const auto *occupant = Occupant(units, coords);
|
||||
if (occupant) {
|
||||
if (!occupant->hidden() && occupant->player_id() != playerId &&
|
||||
!common::Contains(allyPids, occupant->player_id())) {
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class GameStateW;
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
|
||||
using HexMapT = net::eagle0::shardok::storage::fb::HexMapT;
|
||||
@@ -73,13 +76,6 @@ public:
|
||||
|
||||
[[nodiscard]] auto AllCastleCoords(const HexMap *hexMap) -> CoordsSet;
|
||||
|
||||
auto HasForestAccess(
|
||||
const Coords &coords,
|
||||
const Units *units,
|
||||
const HexMap *hexMap,
|
||||
const vector<PlayerId> &allyPids,
|
||||
PlayerId player) -> bool;
|
||||
|
||||
auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
|
||||
auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
|
||||
|
||||
@@ -97,189 +93,6 @@ auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool
|
||||
[[nodiscard]] auto TwoAwayTilesUnblockedByMountains(const HexMap *map, const Coords &coords)
|
||||
-> CoordsSet;
|
||||
|
||||
static inline auto Occupant(const Units *units, const Coords &coords) -> const Unit * {
|
||||
for (const auto &unit : *units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location() == coords) {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static inline auto Occupant(const Units &units, const Coords &coords) -> const Unit * {
|
||||
for (const auto &unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location() == coords) {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static inline auto Occupants(
|
||||
const vector<const Unit *> &units,
|
||||
const int rowCount,
|
||||
const int columnCount) -> vector<const Unit *> {
|
||||
vector<const Unit *> positions(rowCount * columnCount);
|
||||
|
||||
for (const auto &unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
const int index = unit->location().row() * columnCount + unit->location().column();
|
||||
positions[index] = unit;
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
static inline auto Occupants(
|
||||
const flatbuffers::Vector<const Unit *> &units,
|
||||
const int rowCount,
|
||||
const int columnCount) -> vector<const Unit *> {
|
||||
vector<const Unit *> positions(rowCount * columnCount);
|
||||
|
||||
for (const auto &unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
const int index = unit->location().row() * columnCount + unit->location().column();
|
||||
positions[index] = unit;
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
static inline auto Occupant(const vector<const Unit *> &units, const Coords &coords)
|
||||
-> const Unit * {
|
||||
for (const auto &unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location() == coords) {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template<class UnitContainer>
|
||||
auto Occupant(const UnitContainer &units, const Coords &coords)
|
||||
-> std::optional<typename UnitContainer::value_type> {
|
||||
for (const auto &unit : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
}
|
||||
|
||||
return std::optional<typename UnitContainer::value_type>();
|
||||
}
|
||||
|
||||
template<class U>
|
||||
auto Occupant(const std::map<UnitId, U> &units, const Coords &coords) -> std::optional<U> {
|
||||
for (const auto &kv : units) {
|
||||
if (kv.second.location() == coords) { return kv.second; }
|
||||
}
|
||||
|
||||
return std::optional<U>();
|
||||
}
|
||||
|
||||
template<class U>
|
||||
auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
|
||||
-> std::optional<U> {
|
||||
for (const auto &kv : units) {
|
||||
if (kv.second.location() == coords) { return kv.second; }
|
||||
}
|
||||
|
||||
return std::optional<U>();
|
||||
}
|
||||
|
||||
template<class U, class UnitContainer>
|
||||
auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
|
||||
for (const auto &unit : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
}
|
||||
|
||||
return std::optional<U>();
|
||||
}
|
||||
|
||||
static inline auto FriendlyOccupant(
|
||||
const PlayerId pid,
|
||||
const Units *units,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> const Unit * {
|
||||
const auto *occupant = Occupant(units, coords);
|
||||
if (occupant) {
|
||||
if (occupant->player_id() == pid || common::Contains(allyPids, occupant->player_id())) {
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template<class U, class UnitContainer>
|
||||
auto FriendlyOccupant(
|
||||
PlayerId pid,
|
||||
const UnitContainer &units,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> std::optional<U> {
|
||||
auto occupantOptional = Occupant(units, coords);
|
||||
if (occupantOptional.has_value()) {
|
||||
const auto &occupant = *occupantOptional;
|
||||
if (occupant.player_id() == pid || common::Contains(allyPids, occupant.player_id())) {
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
auto KnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const Units *units,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> const Unit *;
|
||||
|
||||
template<class UnitContainer>
|
||||
auto KnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const UnitContainer &units,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> std::optional<typename UnitContainer::value_type> {
|
||||
auto occupantOptional = Occupant(units, coords);
|
||||
if (occupantOptional.has_value()) {
|
||||
const auto &occupant = *occupantOptional;
|
||||
if (!occupant.hidden() && occupant.player_id() != playerId &&
|
||||
!common::Contains(allyPids, occupant.player_id())) {
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
|
||||
return std::optional<typename UnitContainer::value_type>();
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline auto KnownOccupant(
|
||||
const PlayerId playerId,
|
||||
const Units *units,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> const Unit * {
|
||||
const auto *occupant = Occupant(units, coords);
|
||||
if (occupant) {
|
||||
if (occupant->player_id() == playerId ||
|
||||
common::Contains(allyPids, occupant->player_id()) || !occupant->hidden()) {
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto KnownAdjacentEnemies(
|
||||
PlayerId playerId,
|
||||
const Units *units,
|
||||
const HexMap *map,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> vector<const Unit *>;
|
||||
|
||||
auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
|
||||
-> HexMapDirectionsTo;
|
||||
|
||||
@@ -291,12 +104,6 @@ static inline auto DirectionsTo(const HexMap *map, const Coords &origin, const C
|
||||
auto MaybeTileInDirection(const HexMap *map, const Coords &inCoords, HexMapDirection orientation)
|
||||
-> std::optional<Coords>;
|
||||
|
||||
auto MaybeOccupantInDirection(
|
||||
const HexMap *map,
|
||||
const Units *units,
|
||||
const Coords &coords,
|
||||
HexMapDirection direction) -> const Unit *;
|
||||
|
||||
auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;
|
||||
|
||||
void MutatingSetTileModifier(
|
||||
@@ -316,8 +123,6 @@ void MutatingSetTileModifier(
|
||||
|
||||
auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;
|
||||
|
||||
auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units) -> bool;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif /* HexMapUtils_hpp */
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "ZoneOfControlCalculator.hpp"
|
||||
|
||||
#include "HexMapUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
|
||||
|
||||
namespace shardok {
|
||||
@@ -75,44 +76,6 @@ auto ExertsZoneOfControl(const SettingsGetter &settings, const UnitView &unit) -
|
||||
return true;
|
||||
}
|
||||
|
||||
auto AttackOrientationForAttack(
|
||||
const SettingsGetter &settings,
|
||||
const PlayerId attackingPlayer,
|
||||
const Units *units,
|
||||
const HexMap *map,
|
||||
const Coords &origin,
|
||||
const Coords &destination) -> AttackOrientation {
|
||||
const HexMapDirection direction = DirectionsTo(map, origin, destination).main;
|
||||
|
||||
const auto &targetOccupant = Occupant(units, destination);
|
||||
|
||||
const Unit *rearOccupant = MaybeOccupantInDirection(map, units, destination, direction);
|
||||
if (rearOccupant) {
|
||||
if (rearOccupant->player_id() == attackingPlayer &&
|
||||
ExertsZoneOfControl(settings, rearOccupant, targetOccupant->player_id())) {
|
||||
return AttackOrientation::rear;
|
||||
}
|
||||
}
|
||||
|
||||
const Unit *flankOccupant = MaybeOccupantInDirection(map, units, destination, direction - 1);
|
||||
if (flankOccupant) {
|
||||
if (flankOccupant->player_id() == attackingPlayer &&
|
||||
ExertsZoneOfControl(settings, flankOccupant, targetOccupant->player_id())) {
|
||||
return AttackOrientation::flank;
|
||||
}
|
||||
}
|
||||
|
||||
flankOccupant = MaybeOccupantInDirection(map, units, destination, direction + 1);
|
||||
if (flankOccupant) {
|
||||
if (flankOccupant->player_id() == attackingPlayer &&
|
||||
ExertsZoneOfControl(settings, flankOccupant, targetOccupant->player_id())) {
|
||||
return AttackOrientation::flank;
|
||||
}
|
||||
}
|
||||
|
||||
return AttackOrientation::front;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto TilesInEnemyZoc(
|
||||
const SettingsGetter &settings,
|
||||
const PlayerId playerId,
|
||||
@@ -131,18 +94,59 @@ auto AttackOrientationForAttack(
|
||||
return cs;
|
||||
}
|
||||
|
||||
// OccupancyLookup overload for IsInEnemyZoc
|
||||
[[nodiscard]] auto IsInEnemyZoc(
|
||||
const SettingsGetter &settings,
|
||||
const PlayerId playerId,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *hexMap,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> bool {
|
||||
CoordsSet adjCoords = HexMapUtils::GetAdjacentCoords(hexMap, coords);
|
||||
return std::any_of(std::begin(adjCoords), std::end(adjCoords), [&](const Coords &adjCoords) {
|
||||
const Unit *possibleZocEnemy = KnownEnemyOccupant(playerId, units, allyPids, adjCoords);
|
||||
const Unit *possibleZocEnemy =
|
||||
occupancyLookup.GetKnownEnemyOccupant(playerId, allyPids, adjCoords);
|
||||
return possibleZocEnemy && ExertsZoneOfControl(settings, possibleZocEnemy, playerId);
|
||||
});
|
||||
}
|
||||
|
||||
// OccupancyLookup overload for AttackOrientationForAttack
|
||||
auto AttackOrientationForAttack(
|
||||
const SettingsGetter &settings,
|
||||
const PlayerId attackingPlayer,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *map,
|
||||
const Coords &origin,
|
||||
const Coords &destination) -> AttackOrientation {
|
||||
const HexMapDirection direction = DirectionsTo(map, origin, destination).main;
|
||||
|
||||
const auto *targetOccupant = occupancyLookup.GetOccupant(destination);
|
||||
|
||||
const Unit *rearOccupant = occupancyLookup.GetOccupantInDirection(destination, direction);
|
||||
if (rearOccupant) {
|
||||
if (rearOccupant->player_id() == attackingPlayer &&
|
||||
ExertsZoneOfControl(settings, rearOccupant, targetOccupant->player_id())) {
|
||||
return AttackOrientation::rear;
|
||||
}
|
||||
}
|
||||
|
||||
const Unit *flankOccupant = occupancyLookup.GetOccupantInDirection(destination, direction - 1);
|
||||
if (flankOccupant) {
|
||||
if (flankOccupant->player_id() == attackingPlayer &&
|
||||
ExertsZoneOfControl(settings, flankOccupant, targetOccupant->player_id())) {
|
||||
return AttackOrientation::flank;
|
||||
}
|
||||
}
|
||||
|
||||
flankOccupant = occupancyLookup.GetOccupantInDirection(destination, direction + 1);
|
||||
if (flankOccupant) {
|
||||
if (flankOccupant->player_id() == attackingPlayer &&
|
||||
ExertsZoneOfControl(settings, flankOccupant, targetOccupant->player_id())) {
|
||||
return AttackOrientation::flank;
|
||||
}
|
||||
}
|
||||
|
||||
return AttackOrientation::front;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
#define ZoneOfControlCalculator_hpp
|
||||
|
||||
#include "HexMapUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/OccupancyLookup.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using net::eagle0::shardok::api::UnitView;
|
||||
|
||||
[[nodiscard]] auto
|
||||
@@ -29,18 +31,20 @@ ExertsZoneOfControl(const SettingsGetter &settings, const Unit *unit, PlayerId t
|
||||
const HexMap *hexMap,
|
||||
const vector<PlayerId> &allyPids) -> CoordsSet;
|
||||
|
||||
// OccupancyLookup overload for IsInEnemyZoc
|
||||
[[nodiscard]] auto IsInEnemyZoc(
|
||||
const SettingsGetter &settings,
|
||||
PlayerId playerId,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *hexMap,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> bool;
|
||||
|
||||
// OccupancyLookup overload for AttackOrientationForAttack
|
||||
[[nodiscard]] auto AttackOrientationForAttack(
|
||||
const SettingsGetter &settings,
|
||||
PlayerId attackingPlayer,
|
||||
const Units *units,
|
||||
const OccupancyLookup &occupancyLookup,
|
||||
const HexMap *map,
|
||||
const Coords &origin,
|
||||
const Coords &destination) -> AttackOrientation;
|
||||
|
||||
@@ -97,6 +97,7 @@ cc_library(
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
|
||||
|
||||
@@ -28,23 +28,41 @@ using UnitViewProto = net::eagle0::shardok::api::UnitView;
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
constexpr int8_t kGuessedStat = 50;
|
||||
constexpr int8_t kGuessedStat = 25;
|
||||
|
||||
auto GuessedUnit(
|
||||
const SettingsGetter &settings,
|
||||
const UnitViewProto &uv,
|
||||
PlayerId toPid,
|
||||
Coords location,
|
||||
net::eagle0::shardok::storage::fb::UnitStatus status) -> Unit;
|
||||
net::eagle0::shardok::storage::fb::UnitStatus status,
|
||||
const std::unordered_map<PlayerId, AverageStats> &playerAverages) -> Unit;
|
||||
|
||||
auto GuessedHero(const HeroViewProto &hv) -> net::eagle0::shardok::storage::fb::Hero;
|
||||
|
||||
auto GuessedBattalion(const BattalionViewProto &bv) -> net::eagle0::shardok::storage::fb::Battalion;
|
||||
auto GuessedBattalion(
|
||||
const BattalionViewProto &bv,
|
||||
PlayerId playerId,
|
||||
const std::unordered_map<PlayerId, AverageStats> &playerAverages)
|
||||
-> net::eagle0::shardok::storage::fb::Battalion;
|
||||
|
||||
// Find a unit at a specific location
|
||||
static auto Occupant(
|
||||
const google::protobuf::RepeatedPtrField<UnitViewProto> &units,
|
||||
const Coords &coords) -> std::optional<UnitViewProto> {
|
||||
for (const auto &unit : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto GameStateGuesser::GuessedState(
|
||||
PlayerId pid,
|
||||
const SettingsGetter &settings,
|
||||
const GameStateView &gameStateView) -> GameStateW {
|
||||
// Get known unit stats up front to use for guessing
|
||||
auto playerAverages = KnownUnitStats(gameStateView);
|
||||
|
||||
bool isDefender = false;
|
||||
bool defenderExists = false;
|
||||
for (const auto &pip : gameStateView.player_infos()) {
|
||||
@@ -136,7 +154,8 @@ auto GameStateGuesser::GuessedState(
|
||||
uv,
|
||||
pid,
|
||||
FromCoordsProto(uv.location()),
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT));
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT,
|
||||
playerAverages));
|
||||
if (uv.player_id() == 1) foundDefender = true;
|
||||
if (uv.unit_id() > maxId) maxId = uv.unit_id();
|
||||
foundUnits.push_back(uv.unit_id());
|
||||
@@ -154,14 +173,14 @@ auto GameStateGuesser::GuessedState(
|
||||
for (const auto &startPosProto :
|
||||
gameStateView.hex_map().defender_starting_positions().positions()) {
|
||||
Coords startPos = FromCoordsProto(startPosProto);
|
||||
if (!Occupant<UnitViewProto>(gameStateView.units(), startPos).has_value()) {
|
||||
if (!Occupant(gameStateView.units(), startPos).has_value()) {
|
||||
location = startPos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unitsVec.push_back(GuessedUnit(settings, uv, pid, location, status));
|
||||
unitsVec.push_back(GuessedUnit(settings, uv, pid, location, status, playerAverages));
|
||||
|
||||
if (uv.unit_id() > maxId) maxId = uv.unit_id();
|
||||
foundUnits.push_back(uv.unit_id());
|
||||
@@ -197,7 +216,7 @@ auto GameStateGuesser::GuessedState(
|
||||
for (const auto &startPosProto :
|
||||
gameStateView.hex_map().defender_starting_positions().positions()) {
|
||||
Coords startPos = FromCoordsProto(startPosProto);
|
||||
if (!Occupant<UnitViewProto>(gameStateView.units(), startPos).has_value()) {
|
||||
if (!Occupant(gameStateView.units(), startPos).has_value()) {
|
||||
newUnit.mutable_location() = startPos;
|
||||
newUnit.mutate_hidden(true);
|
||||
newUnit.mutate_player_id(1);
|
||||
@@ -306,11 +325,12 @@ auto GuessedUnit(
|
||||
const UnitViewProto &uv,
|
||||
PlayerId toPid,
|
||||
Coords location,
|
||||
net::eagle0::shardok::storage::fb::UnitStatus status) -> Unit {
|
||||
net::eagle0::shardok::storage::fb::UnitStatus status,
|
||||
const std::unordered_map<PlayerId, AverageStats> &playerAverages) -> Unit {
|
||||
net::eagle0::shardok::storage::fb::Hero attachedHero{};
|
||||
if (uv.has_attached_hero()) { attachedHero = GuessedHero(uv.attached_hero()); }
|
||||
|
||||
net::eagle0::shardok::storage::fb::Unit unit;
|
||||
fb::Unit unit;
|
||||
|
||||
unit.mutate_unit_id(uv.unit_id());
|
||||
unit.mutate_player_id(uv.player_id());
|
||||
@@ -339,7 +359,7 @@ auto GuessedUnit(
|
||||
unit.mutate_status(status);
|
||||
}
|
||||
|
||||
unit.mutable_battalion() = GuessedBattalion(uv.battalion());
|
||||
unit.mutable_battalion() = GuessedBattalion(uv.battalion(), uv.player_id(), playerAverages);
|
||||
|
||||
if (uv.has_remaining_action_points()) {
|
||||
unit.mutate_remaining_action_points(uv.remaining_action_points().value());
|
||||
@@ -467,18 +487,80 @@ auto GuessedHero(const HeroViewProto &hv) -> net::eagle0::shardok::storage::fb::
|
||||
return hero;
|
||||
}
|
||||
|
||||
auto GuessedBattalion(const BattalionViewProto &bv)
|
||||
auto GuessedBattalion(
|
||||
const BattalionViewProto &bv,
|
||||
PlayerId playerId,
|
||||
const std::unordered_map<PlayerId, AverageStats> &playerAverages)
|
||||
-> net::eagle0::shardok::storage::fb::Battalion {
|
||||
// Three-tier fallback for stats:
|
||||
// 1. Use actual stats if present
|
||||
// 2. Use average stats for this player if available
|
||||
// 3. Use kGuessedStat as default
|
||||
|
||||
int8_t armament = kGuessedStat;
|
||||
int8_t training = kGuessedStat;
|
||||
|
||||
if (bv.has_armament()) {
|
||||
armament = bv.armament().value();
|
||||
} else if (auto it = playerAverages.find(playerId); it != playerAverages.end()) {
|
||||
armament = static_cast<int8_t>(std::round(it->second.armament));
|
||||
}
|
||||
|
||||
if (bv.has_training()) {
|
||||
training = bv.training().value();
|
||||
} else if (auto it = playerAverages.find(playerId); it != playerAverages.end()) {
|
||||
training = static_cast<int8_t>(std::round(it->second.training));
|
||||
}
|
||||
|
||||
const net::eagle0::shardok::storage::fb::Battalion batt(
|
||||
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(bv.type()),
|
||||
bv.size(),
|
||||
bv.has_morale() ? bv.morale().value() : kGuessedStat,
|
||||
bv.has_training() ? bv.training().value() : kGuessedStat,
|
||||
bv.has_armament() ? bv.armament().value() : kGuessedStat,
|
||||
training,
|
||||
armament,
|
||||
-1,
|
||||
kGuessedStat);
|
||||
|
||||
return batt;
|
||||
}
|
||||
|
||||
auto KnownUnitStats(const GameStateView &gameStateView)
|
||||
-> std::unordered_map<PlayerId, AverageStats> {
|
||||
struct PlayerStats {
|
||||
double armamentSum = 0.0;
|
||||
double trainingSum = 0.0;
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
std::unordered_map<PlayerId, PlayerStats> playerStatsMap;
|
||||
|
||||
// Single pass to collect all stats
|
||||
for (const auto &unit : gameStateView.units()) {
|
||||
if (unit.player_id() < 0) continue; // Skip reserve units
|
||||
|
||||
if (unit.battalion().has_armament() && unit.battalion().has_training()) {
|
||||
auto &stats = playerStatsMap[unit.player_id()];
|
||||
stats.count++;
|
||||
stats.armamentSum += unit.battalion().armament().value();
|
||||
stats.trainingSum += unit.battalion().training().value();
|
||||
}
|
||||
}
|
||||
|
||||
// Build result map with averages
|
||||
std::unordered_map<PlayerId, AverageStats> result;
|
||||
result.reserve(playerStatsMap.size()); // Pre-allocate for efficiency
|
||||
|
||||
for (const auto &[playerId, stats] : playerStatsMap) {
|
||||
if (stats.count > 0) {
|
||||
result.emplace(
|
||||
playerId,
|
||||
AverageStats{
|
||||
.armament = stats.armamentSum / stats.count,
|
||||
.training = stats.trainingSum / stats.count});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -5,6 +5,8 @@
|
||||
#ifndef EAGLE0_GAMESTATEGUESSER_HPP
|
||||
#define EAGLE0_GAMESTATEGUESSER_HPP
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
@@ -13,6 +15,14 @@ namespace shardok {
|
||||
|
||||
using GameStateView = net::eagle0::shardok::api::GameStateView;
|
||||
|
||||
struct AverageStats {
|
||||
double armament;
|
||||
double training;
|
||||
};
|
||||
|
||||
auto KnownUnitStats(const GameStateView &gameStateView)
|
||||
-> std::unordered_map<PlayerId, AverageStats>;
|
||||
|
||||
class GameStateGuesser {
|
||||
public:
|
||||
[[nodiscard]] static auto GuessedState(
|
||||
|
||||
@@ -474,8 +474,7 @@ auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit {
|
||||
commonUnit.mutable_battalion()->set_eagle_battalion_id(unit.battalion().eagle_battalion_id());
|
||||
commonUnit.mutable_battalion()->set_size(unit.battalion().size());
|
||||
commonUnit.mutable_battalion()->set_type(
|
||||
(net::eagle0::common::CommonBattalionTypeId)unit.battalion().type());
|
||||
commonUnit.mutable_battalion()->set_morale(unit.battalion().morale());
|
||||
static_cast<net::eagle0::common::CommonBattalionTypeId>(unit.battalion().type()));
|
||||
commonUnit.mutable_battalion()->set_armament(unit.battalion().armament());
|
||||
commonUnit.mutable_battalion()->set_training(unit.battalion().training());
|
||||
|
||||
|
||||
@@ -250,6 +250,7 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/HandleRiotGiveCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/ProvinceUtils.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ProvinceConqueredNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/ProvinceStatUtils.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/ExchangedHeroRowController.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/ExtraTroopsRowController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/PrisonerEscapedNotificationGenerator.cs" />
|
||||
|
||||
+13
-13
@@ -25,7 +25,7 @@ namespace eagle {
|
||||
private ImprovementType SelectedType =>
|
||||
ImproveAvailableCommand.AvailableTypes[SelectedTypeIndex];
|
||||
|
||||
private double OriginalImprovementValueForType(ImprovementType type) {
|
||||
private float OriginalImprovementValueForType(ImprovementType type) {
|
||||
var province = _model.Provinces[ActingProvinceId];
|
||||
switch (type) {
|
||||
case ImprovementType.Agriculture: return province.FullInfo.Agriculture;
|
||||
@@ -39,7 +39,7 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
private double EffectiveImprovementValueForType(ImprovementType type) {
|
||||
private float EffectiveImprovementValueForType(ImprovementType type) {
|
||||
var province = _model.Provinces[ActingProvinceId];
|
||||
switch (type) {
|
||||
case ImprovementType.Agriculture:
|
||||
@@ -59,10 +59,10 @@ namespace eagle {
|
||||
|
||||
private int MinimumImprovementIndex(ProvinceView province) {
|
||||
var minIndex = 0;
|
||||
double minValue =
|
||||
float minValue =
|
||||
OriginalImprovementValueForType(ImproveAvailableCommand.AvailableTypes[0]);
|
||||
for (int i = 1; i < ImproveAvailableCommand.AvailableTypes.Count; i++) {
|
||||
double thisVal =
|
||||
float thisVal =
|
||||
OriginalImprovementValueForType(ImproveAvailableCommand.AvailableTypes[i]);
|
||||
if (thisVal < minValue) {
|
||||
minIndex = i;
|
||||
@@ -139,21 +139,21 @@ namespace eagle {
|
||||
};
|
||||
|
||||
private string DropdownStringForType(ImprovementType type) {
|
||||
var originalStat = RoundedNonzeroStat(OriginalImprovementValueForType(type));
|
||||
var originalStat =
|
||||
type == ImprovementType.Devastation
|
||||
? ProvinceStatUtils.RoundedDevastation(
|
||||
OriginalImprovementValueForType(type))
|
||||
: ProvinceStatUtils.RoundedStat(OriginalImprovementValueForType(type));
|
||||
if (type == ImprovementType.Devastation) {
|
||||
return $"{type} ({GUIUtils.ColoredString(UnityEngine.Color.red, originalStat)})";
|
||||
return $"{type} ({GUIUtils.ColoredString(UnityEngine.Color.red, originalStat.ToString())})";
|
||||
}
|
||||
var devastatedStat = RoundedNonzeroStat(EffectiveImprovementValueForType(type));
|
||||
var devastatedStat =
|
||||
ProvinceStatUtils.RoundedStat(EffectiveImprovementValueForType(type));
|
||||
if (devastatedStat == originalStat) {
|
||||
return $"{type} ({devastatedStat})";
|
||||
} else {
|
||||
return $"{type} ({GUIUtils.ColoredString(UnityEngine.Color.red, devastatedStat)} / {originalStat})";
|
||||
return $"{type} ({GUIUtils.ColoredString(UnityEngine.Color.red, devastatedStat.ToString())} / {originalStat})";
|
||||
}
|
||||
}
|
||||
|
||||
private string RoundedNonzeroStat(double stat) {
|
||||
var rounded = Math.Max(1, Math.Round(stat, MidpointRounding.AwayFromZero));
|
||||
return rounded.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using common;
|
||||
using EagleGUIUtils;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
@@ -235,8 +236,8 @@ namespace eagle {
|
||||
allowed,
|
||||
OrganizeTroopsAvailableCommand.AvailableBattalionTypes.First(
|
||||
tp => tp.TypeId == battalionTypeId),
|
||||
Province.FullInfo.Agriculture,
|
||||
Province.FullInfo.Economy);
|
||||
ProvinceStatUtils.RoundedStat(Province.FullInfo.Agriculture),
|
||||
ProvinceStatUtils.RoundedStat(Province.FullInfo.Economy));
|
||||
|
||||
parent.GetComponentInChildren<Button>().interactable = allowed;
|
||||
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ namespace eagle {
|
||||
|
||||
battalionDropdownController.AvailableBattalions = availableBattalions;
|
||||
|
||||
var maxSize = availableBattalions.Max(x => x.Size);
|
||||
var maxSize = availableBattalions.Any() ? availableBattalions.Max(x => x.Size) : 0;
|
||||
battalionDropdownController.SelectedBattalionId =
|
||||
availableBattalions.Where(batt => batt.Size == maxSize)
|
||||
.Select(batt => (BattalionId?)batt.Id)
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ namespace eagle {
|
||||
|
||||
private readonly Func<ProvinceView, String> _nameSortFunc = p => p.Name;
|
||||
|
||||
private readonly Dictionary<SortKey, Func<ProvinceView, int>> _sortFuncs = new() {
|
||||
private readonly Dictionary<SortKey, Func<ProvinceView, IComparable>> _sortFuncs = new() {
|
||||
{ SortKey.Support, p => p.FullInfo.Support.Stat },
|
||||
{ SortKey.Commanders, p => p.FullInfo.RulingFactionHeroIds.Count },
|
||||
{ SortKey.Battalions, p => p.FullInfo.Battalions.Count },
|
||||
|
||||
+10
-7
@@ -225,25 +225,27 @@ namespace eagle {
|
||||
ConsumedField.text = $"{Province.FullInfo.FoodConsumption}";
|
||||
priceIndexField.text = $"{Province.FullInfo.PriceIndex,4:F2}";
|
||||
|
||||
EconomyField.text = $"{Province.FullInfo.Economy}";
|
||||
EconomyField.text = $"{ProvinceStatUtils.RoundedStat(Province.FullInfo.Economy)}";
|
||||
if (Province.FullInfo.EconomyDevastation > 0) {
|
||||
var devastatedEconomy =
|
||||
$"{Math.Max(0.0, Province.FullInfo.Economy - Province.FullInfo.EconomyDevastation)}";
|
||||
$"{ProvinceStatUtils.RoundedStat(Math.Max(0.0f, Province.FullInfo.Economy - Province.FullInfo.EconomyDevastation))}";
|
||||
EconomyField.text += $" ({GUIUtils.ColoredString(Color.red, devastatedEconomy)})";
|
||||
}
|
||||
|
||||
AgricultureField.text = $"{Province.FullInfo.Agriculture}";
|
||||
AgricultureField.text =
|
||||
$"{ProvinceStatUtils.RoundedStat(Province.FullInfo.Agriculture)}";
|
||||
if (Province.FullInfo.AgricultureDevastation > 0) {
|
||||
var devastatedAgriculture =
|
||||
$"{Math.Max(0.0, Province.FullInfo.Agriculture - Province.FullInfo.AgricultureDevastation)}";
|
||||
$"{ProvinceStatUtils.RoundedStat(Math.Max(0.0f, Province.FullInfo.Agriculture - Province.FullInfo.AgricultureDevastation))}";
|
||||
AgricultureField.text +=
|
||||
$" ({GUIUtils.ColoredString(Color.red, devastatedAgriculture)})";
|
||||
}
|
||||
|
||||
InfrastructureField.text = $"{Province.FullInfo.Infrastructure}";
|
||||
InfrastructureField.text =
|
||||
$"{ProvinceStatUtils.RoundedStat(Province.FullInfo.Infrastructure)}";
|
||||
if (Province.FullInfo.InfrastructureDevastation > 0) {
|
||||
var devastatedInfrastructure =
|
||||
$"{Math.Max(0.0, Province.FullInfo.Infrastructure - Province.FullInfo.InfrastructureDevastation)}";
|
||||
$"{ProvinceStatUtils.RoundedStat(Math.Max(0.0f, Province.FullInfo.Infrastructure - Province.FullInfo.InfrastructureDevastation))}";
|
||||
InfrastructureField.text +=
|
||||
$" ({GUIUtils.ColoredString(Color.red, devastatedInfrastructure)})";
|
||||
}
|
||||
@@ -251,7 +253,8 @@ namespace eagle {
|
||||
var totalDevastation = Province.FullInfo.EconomyDevastation +
|
||||
Province.FullInfo.AgricultureDevastation +
|
||||
Province.FullInfo.InfrastructureDevastation;
|
||||
var totalDevastationString = $"{totalDevastation}";
|
||||
var totalDevastationString =
|
||||
$"{ProvinceStatUtils.RoundedDevastation(totalDevastation)}";
|
||||
if (totalDevastation > 0) {
|
||||
DevastationField.text =
|
||||
$"{GUIUtils.ColoredString(Color.red, totalDevastationString)}";
|
||||
|
||||
+5
-5
@@ -37,14 +37,14 @@ namespace eagle {
|
||||
private ProvinceId ProvinceId;
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
|
||||
private void SetDevastatedValue(TMP_Text textField, double baseValue, double devastation) {
|
||||
if (devastation == 0.0) {
|
||||
private void SetDevastatedValue(TMP_Text textField, float baseValue, float devastation) {
|
||||
if (devastation == 0.0f) {
|
||||
textField.color = Color.black;
|
||||
textField.text = baseValue.ToString();
|
||||
textField.text = ProvinceStatUtils.RoundedStat(baseValue).ToString();
|
||||
} else {
|
||||
textField.color = Color.red;
|
||||
var devastatedValue = Math.Max(0.0, baseValue - devastation);
|
||||
textField.text = devastatedValue.ToString();
|
||||
var devastatedValue = Math.Max(0.0f, baseValue - devastation);
|
||||
textField.text = ProvinceStatUtils.RoundedStat(devastatedValue).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,18 +153,16 @@ public class ShardokGameModel {
|
||||
}
|
||||
foreach (ActionResultView entry in newHistory) { HandleNewHistoryEntry(entry); }
|
||||
|
||||
if (newHistory.Any() && UpdateAction != null) { UpdateAction.Invoke(); }
|
||||
if (newHistoryCount > 0 && UpdateAction != null) { UpdateAction.Invoke(); }
|
||||
}
|
||||
|
||||
public void HandleAvailableCommands(AvailableCommands newCommands) {
|
||||
if (newCommands == null) {
|
||||
AvailableCommands = new List<CommandDescriptor>();
|
||||
} else {
|
||||
if (newCommands.CurrentCommand.Count > 0) {
|
||||
AvailableCommands = newCommands.CurrentCommand.ToList();
|
||||
} else {
|
||||
AvailableCommands = newCommands.PreviewCommand.ToList();
|
||||
}
|
||||
AvailableCommands = newCommands.CurrentCommand.Count > 0
|
||||
? newCommands.CurrentCommand.ToList()
|
||||
: newCommands.PreviewCommand.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Utility methods for rounding province statistics for display.
|
||||
/// These methods match the rounding logic from ProvinceViewFilter.scala
|
||||
/// </summary>
|
||||
public static class ProvinceStatUtils {
|
||||
/// <summary>
|
||||
/// Rounds a stat value using the same logic as roundedStat in ProvinceViewFilter.scala:
|
||||
/// - If stat == 0, return 0
|
||||
/// - If stat < 1, return 1
|
||||
/// - Otherwise, return floor of stat
|
||||
/// </summary>
|
||||
public static int RoundedStat(float stat) {
|
||||
if (stat == 0) return 0;
|
||||
else if (stat < 1)
|
||||
return 1;
|
||||
else
|
||||
return (int)Math.Floor(stat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds a devastation value using the same logic as roundedDevastation in
|
||||
/// ProvinceViewFilter.scala:
|
||||
/// - Return ceiling of stat
|
||||
/// </summary>
|
||||
public static int RoundedDevastation(float stat) { return (int)Math.Ceiling(stat); }
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6672bb4c46b44b10abe885e064119d0
|
||||
@@ -81,9 +81,8 @@ message CommonBattalion {
|
||||
CommonBattalionTypeId type = 2;
|
||||
int32 size = 3;
|
||||
|
||||
double morale = 4;
|
||||
double training = 5;
|
||||
double armament = 6;
|
||||
double training = 4;
|
||||
double armament = 5;
|
||||
}
|
||||
|
||||
enum UnitStatus {
|
||||
|
||||
@@ -18,9 +18,8 @@ message Battalion {
|
||||
.net.eagle0.eagle.common.BattalionTypeId type = 2;
|
||||
int32 size = 3;
|
||||
|
||||
double morale = 4;
|
||||
double training = 5;
|
||||
double armament = 6;
|
||||
double training = 4;
|
||||
double armament = 5;
|
||||
|
||||
string name = 7;
|
||||
string name = 6;
|
||||
}
|
||||
|
||||
@@ -19,18 +19,16 @@ message BattalionView {
|
||||
.net.eagle0.eagle.common.BattalionTypeId type = 2;
|
||||
int32 size = 3;
|
||||
|
||||
.google.protobuf.DoubleValue morale = 4;
|
||||
.google.protobuf.DoubleValue training = 5;
|
||||
.google.protobuf.DoubleValue armament = 6;
|
||||
.google.protobuf.DoubleValue training = 4;
|
||||
.google.protobuf.DoubleValue armament = 5;
|
||||
|
||||
string name = 7;
|
||||
string name = 6;
|
||||
}
|
||||
|
||||
message BattalionViewDiff {
|
||||
int32 id = 1;
|
||||
|
||||
.google.protobuf.Int32Value new_size = 2;
|
||||
.google.protobuf.DoubleValue new_morale = 3;
|
||||
.google.protobuf.DoubleValue new_training = 4;
|
||||
.google.protobuf.DoubleValue new_armament = 5;
|
||||
.google.protobuf.DoubleValue new_training = 3;
|
||||
.google.protobuf.DoubleValue new_armament = 4;
|
||||
}
|
||||
|
||||
@@ -59,12 +59,12 @@ message FullProvinceInfo {
|
||||
|
||||
ArmyView defending_army = 7;
|
||||
|
||||
int32 economy = 8;
|
||||
int32 agriculture = 9;
|
||||
int32 infrastructure = 10;
|
||||
int32 economy_devastation = 25;
|
||||
int32 agriculture_devastation = 26;
|
||||
int32 infrastructure_devastation = 27;
|
||||
float economy = 8;
|
||||
float agriculture = 9;
|
||||
float infrastructure = 10;
|
||||
float economy_devastation = 25;
|
||||
float agriculture_devastation = 26;
|
||||
float infrastructure_devastation = 27;
|
||||
|
||||
int32 gold = 11;
|
||||
int32 food = 12;
|
||||
@@ -130,12 +130,12 @@ message FullProvinceInfoDiff {
|
||||
|
||||
.google.protobuf.DoubleValue new_price_index = 36;
|
||||
|
||||
.google.protobuf.Int32Value economy = 16;
|
||||
.google.protobuf.Int32Value agriculture = 17;
|
||||
.google.protobuf.Int32Value infrastructure = 18;
|
||||
.google.protobuf.Int32Value economy_devastation = 30;
|
||||
.google.protobuf.Int32Value agriculture_devastation = 31;
|
||||
.google.protobuf.Int32Value infrastructure_devastation = 32;
|
||||
.google.protobuf.FloatValue economy = 16;
|
||||
.google.protobuf.FloatValue agriculture = 17;
|
||||
.google.protobuf.FloatValue infrastructure = 18;
|
||||
.google.protobuf.FloatValue economy_devastation = 30;
|
||||
.google.protobuf.FloatValue agriculture_devastation = 31;
|
||||
.google.protobuf.FloatValue infrastructure_devastation = 32;
|
||||
|
||||
StatWithCondition support = 19;
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 300,
|
||||
"morale": 50,
|
||||
"training": 10,
|
||||
"armament": 10
|
||||
}
|
||||
@@ -47,7 +46,6 @@
|
||||
{
|
||||
"type": "LIGHT_CAVALRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 100,
|
||||
"armament": 100,
|
||||
"name": "The Eagle's Scouts"
|
||||
@@ -55,7 +53,6 @@
|
||||
{
|
||||
"type": "HEAVY_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 100,
|
||||
"armament": 100,
|
||||
"name": "The Eagle's Chargers"
|
||||
@@ -63,7 +60,6 @@
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 100,
|
||||
"armament": 100,
|
||||
"name": "The Eagle's Spearmen"
|
||||
@@ -71,7 +67,6 @@
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 100,
|
||||
"armament": 100,
|
||||
"name": "The Eagle's Grunts"
|
||||
@@ -94,7 +89,6 @@
|
||||
{
|
||||
"type": "LIGHT_CAVALRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 100,
|
||||
"armament": 100,
|
||||
"name": "Tarn's Dragoons"
|
||||
@@ -102,7 +96,6 @@
|
||||
{
|
||||
"type": "HEAVY_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 100,
|
||||
"armament": 100,
|
||||
"name": "Tarn's Stormtroopers"
|
||||
@@ -110,7 +103,6 @@
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 100,
|
||||
"armament": 100,
|
||||
"name": "Tarn's Skirmishers"
|
||||
@@ -118,7 +110,6 @@
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 100,
|
||||
"armament": 100,
|
||||
"name": "Tarn's Grunts"
|
||||
@@ -126,7 +117,6 @@
|
||||
{
|
||||
"type": "HEAVY_CAVALRY",
|
||||
"size": 600,
|
||||
"morale": 50,
|
||||
"training": 100,
|
||||
"armament": 100,
|
||||
"name": "Tarn's Knights"
|
||||
@@ -159,7 +149,6 @@
|
||||
{
|
||||
"type": "LIGHT_CAVALRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Royal Scouts"
|
||||
@@ -167,7 +156,6 @@
|
||||
{
|
||||
"type": "HEAVY_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Royal Guards"
|
||||
@@ -175,7 +163,6 @@
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Royal Conscripts"
|
||||
@@ -183,7 +170,6 @@
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Royal Spearmen"
|
||||
@@ -191,7 +177,6 @@
|
||||
{
|
||||
"type": "LONGBOWMEN",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "King's Sharpshooters"
|
||||
@@ -199,7 +184,6 @@
|
||||
{
|
||||
"type": "LONGBOWMEN",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "King's Rangers"
|
||||
@@ -222,7 +206,6 @@
|
||||
{
|
||||
"type": "LIGHT_CAVALRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Kelaam Narr's Scouts"
|
||||
@@ -230,7 +213,6 @@
|
||||
{
|
||||
"type": "HEAVY_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Kelaam Narr's Bodyguards"
|
||||
@@ -238,7 +220,6 @@
|
||||
{
|
||||
"type": "HEAVY_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Kelaam Narr's Shock"
|
||||
@@ -246,7 +227,6 @@
|
||||
{
|
||||
"type": "HEAVY_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Kelaam Narr's Heavy Foot"
|
||||
@@ -254,7 +234,6 @@
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Kelaam Narr's Cannon Fodder"
|
||||
@@ -262,7 +241,6 @@
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 800,
|
||||
"morale": 50,
|
||||
"training": 25,
|
||||
"armament": 25,
|
||||
"name": "Kelaam Narr's Conscripts"
|
||||
|
||||
@@ -214,4 +214,6 @@ saveAll FALSE bool
|
||||
holyWaveVigorCost 10 double
|
||||
minLookaheadTurns 1 int8
|
||||
lookaheadTimeBudgetCloseInSeconds 3 double
|
||||
lookaheadTimeBudgetFarInSeconds 1.5 double
|
||||
lookaheadTimeBudgetFarInSeconds 1.5 double
|
||||
aiMinimumFleeOddsThreshold 30 int16
|
||||
aiDesperateFleeThreshold 10 int16
|
||||
|
+4
-3
@@ -14,7 +14,7 @@ import java.time.{Duration, ZonedDateTime}
|
||||
import java.util.function.Consumer
|
||||
|
||||
object OpenAIChatCompletionsServiceImpl {
|
||||
val gpt4: String = "gpt-4.1"
|
||||
val gpt5: String = "gpt-5"
|
||||
|
||||
private val apiKey = ApiKeys.openAI
|
||||
private val baseURL = new URL("https://api.openai.com/v1/chat/completions")
|
||||
@@ -25,7 +25,8 @@ object OpenAIChatCompletionsServiceImpl {
|
||||
): Map[String, Any] = Map(
|
||||
"model" -> modelName,
|
||||
"temperature" -> temperature,
|
||||
"stream" -> true
|
||||
"stream" -> true,
|
||||
"reasoning_effort" -> "minimal"
|
||||
)
|
||||
|
||||
private def messageVector(
|
||||
@@ -57,7 +58,7 @@ object OpenAIChatCompletionsServiceImpl {
|
||||
|
||||
class OpenAIChatCompletionsServiceImpl(
|
||||
timeoutSeconds: Int = 10,
|
||||
val defaultModelName: String = "gpt-3.5-turbo"
|
||||
val defaultModelName: String = "gpt-5"
|
||||
) extends ExternalTextGenerationServiceImpl {
|
||||
implicit val jsonFormats: DefaultFormats.type = DefaultFormats
|
||||
|
||||
|
||||
@@ -307,12 +307,14 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_diplomacy_resolution_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:ransom_invalidated_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
|
||||
+9
-1
@@ -24,12 +24,16 @@ import net.eagle0.eagle.model.action_result.types.{
|
||||
RansomInvalidatedResultType
|
||||
}
|
||||
import net.eagle0.eagle.model.action_result.{ActionResultT, NotificationT}
|
||||
import net.eagle0.eagle.model.proto_converters.NotificationConverter
|
||||
import net.eagle0.eagle.model.proto_converters.{
|
||||
BattalionConverter,
|
||||
NotificationConverter
|
||||
}
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.model.state.RoundPhase.ReconResolution
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.diplomacy_offer.status.{
|
||||
Accepted,
|
||||
@@ -63,6 +67,9 @@ case class EndDiplomacyResolutionPhaseAction(
|
||||
private lazy val heroTs: Vector[HeroT] =
|
||||
gameState.heroes.values.map(HeroConverter.fromProto).toVector
|
||||
|
||||
private lazy val battalionTs: Vector[BattalionT] =
|
||||
gameState.battalions.values.map(BattalionConverter.fromProto).toVector
|
||||
|
||||
private lazy val currentDateT: Date =
|
||||
DateConverter.fromProto(gameState.currentDate)
|
||||
|
||||
@@ -335,6 +342,7 @@ case class EndDiplomacyResolutionPhaseAction(
|
||||
currentDate = currentDateT,
|
||||
provinces = provinceTs,
|
||||
factions = factionTs,
|
||||
battalions = battalionTs,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
case Rejected =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user