Compare commits

...
69 Commits
Author SHA1 Message Date
admin 8b2fb3992f copy the ShardokEngine instead of using a shared_ptr across multiple threads 2025-08-22 16:11:02 -07:00
adminandGitHub b396476096 Fix a memory leak in FlatbufferWrapper and some other small fixes (#4345)
* more small fixes

* more ReSharper disables

* and the cpp

* wrapper

* switch to FNV1a hash and defer to that
2025-08-22 09:16:34 -07:00
adminandGitHub c59aecf0b8 just use a queue (#4343) 2025-08-19 21:49:00 -07:00
adminandGitHub f896d2d517 add some metrics to the threadpool and use thread pools for lower depths (#4342)
* add some metrics to the threadpool

* cleanup

* that's better

* address comments
2025-08-19 21:39:30 -07:00
adminandGitHub a542361ae5 don't use a sentinel value (#4341) 2025-08-15 16:37:40 -07:00
3b25ba3f97 Pipe deadline to AIScoreCalculator and use the thread pool (#4340)
* only leaf nodes go async

* honor the deadline in AIScoreCalculator calls

* use the thread pool

* NaN sentinel

* return TaskResult

* Improve timeout handling with cleaner hybrid approach

Enhanced the timeout handling implementation with:

- Added ConvertScoreToTaskResult() helper function for explicit conversion
- Improved documentation explaining the hybrid approach
- Clear separation between internal NaN sentinel and external TaskResult API
- Added comprehensive comments explaining design decisions

The hybrid approach keeps:
- Internal algorithms using ScoreValue with NaN sentinel (efficient, no cascading changes)
- External API using TaskResult for explicit success/failure semantics
- Clear conversion boundary in CommandScore function

This provides clean timeout semantics to callers while maintaining
performance and avoiding extensive refactoring of existing algorithms.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-15 11:48:28 -07:00
adminandGitHub a355455e88 Real thread pool (#4336)
* add back the thread pool

* hrml

* just revert that shit

* dead target
2025-08-15 07:10:49 -07:00
adminandGitHub fce34e6d97 only leaf nodes go async (#4339) 2025-08-15 06:53:45 -07:00
adminandGitHub 1bc8fa418e defer another get() (#4338) 2025-08-15 06:42:18 -07:00
adminandGitHub 3da5b576a0 More wait (#4335)
* add comments

* return a future from the AIScoreCalculator api

* is this a deadlock

* avoid the deadlock
2025-08-14 21:02:20 -07:00
adminandGitHub 51e41219ac wait on a future (#4334)
* wait on a future

* move the private static functions into the implementation file
2025-08-14 20:25:25 -07:00
d6fe2f415d Modernize remaining container utils (#4333)
* Remove unused container utility functions from ContainerUtils.hpp

Removed the following unused template functions:
- CountIf (no usages found)
- Filtered and FilteredToVector (no usages found)
- Map and MapToVector (no usages found)
- FlatMap and FlatMapToVector (no usages found)
- ToVector (no usages found)
- Append (no usages found)

Kept FilterInPlace as it's still used in several files but marked
it as deprecated with a comment to use std::erase_if instead.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Replace FilterInPlace with std::erase_if and remove from ContainerUtils

- Replaced all FilterInPlace usages with std::erase_if in:
  * AvailableCommandsFactory.cpp (5 usages)
  * ActionResultApplier.cpp (1 usage)
- Removed FilterInPlace function from ContainerUtils.hpp entirely
- Simplified ContainerUtils_test.cpp by removing all tests for removed functions
- Note: FilterInPlace for CoordsSet remains in CoordsSet.hpp as it's for custom type

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* remove ContainerUtils and ContainerUtils_test

* Restore Map, MapToVector, and FlatMapToVector functions for remaining usages

- Recreated ContainerUtils.hpp with only the functions still in use:
  * Map (used in AIAttackGroups.cpp and ShardokGameController.cpp)
  * MapToVector (used in EagleInterfaceGrpcServer.cpp)
  * FlatMapToVector (used in EagleInterfaceGrpcServer.cpp)
- Added missing #includes and BUILD dependencies to all files using these functions
- All functions marked as deprecated with comments suggesting C++20/23 alternatives
- Used C++20 concepts for conditional reserve() calls

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Replace all common::Map function calls with std::ranges::transform

- Replaced common::Map in AIAttackGroups.cpp with std::ranges::transform + back_inserter
- Replaced common::Map in ShardokGameController.cpp with std::ranges::transform + back_inserter
- Replaced 3 common::MapToVector calls in EagleInterfaceGrpcServer.cpp with std::ranges::transform + back_inserter
- Replaced common::FlatMapToVector with nested std::ranges::any_of for more idiomatic ranges code
- Added proper reserve() calls for performance
- Removed all Map functions from ContainerUtils.hpp
- Updated includes to use <iterator> and <ranges> instead of ContainerUtils.hpp
- Removed container_utils dependencies from BUILD files

All custom container utility functions have now been fully replaced with C++20/23 standard library equivalents.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove ContainerUtils.hpp file and BUILD target

- Deleted src/main/cpp/net/eagle0/common/ContainerUtils.hpp (now empty)
- Removed container_utils BUILD target from common/BUILD.bazel
- All container utility functions have been fully replaced with C++20/23 standard library equivalents

The modernization is now complete - no custom container utilities remain in the codebase.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* typo

* gazelle

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-14 19:36:30 -07:00
dab304b595 Replace custom container utilities with C++20/23 standard library equivalents (#4332)
* Replace custom container utilities with C++20/23 standard library equivalents

- Replace common::Contains with std::ranges::contains (C++23)
- Replace common::ContainsWhere with std::ranges::any_of (C++20)
- Replace common::FindIf with std::ranges::find_if (C++20)
- Mark deprecated custom helper functions in ContainerUtils.hpp
- Add #include <ranges> and <algorithm> to affected files

This modernizes the codebase to use standard library algorithms instead of
custom implementations, improving maintainability and leveraging optimized
standard library implementations. The custom functions remain for compatibility
but are marked as deprecated to encourage migration to standard equivalents.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Complete replacement of all remaining common::Contains usages

- UpdateGameStatusAction.cpp: Replace common::Contains with std::ranges::contains
- AvailableCommands_test.cpp: Replace usage in test and add ranges include
- GtestExtensions.hpp: Update test helper function to use std::ranges::contains
- HideCommandFactory.cpp: Replace common::Contains in hide command logic
- MoveCommand.cpp: Replace all usages in move command ally checking
- HideCommand.cpp: Replace usage in allied player checking
- HolyWaveCommand.cpp: Replace usage in holy wave targeting
- ShardokEngine.cpp: Fix iterator dereference after FindIf conversion

All custom common::Contains usages have been eliminated in favor of
C++23 std::ranges::contains for better performance and standards compliance.

* remove those functions

* fix GtestExtensions.hpp

* Fix test template to handle both standard containers and custom types

Use C++20 concepts with if constexpr to detect whether a type has a
Contains member function (like CoordsSet) or should use std::ranges::contains
for standard containers. This allows the test helper to work correctly with
both standard library containers and custom container-like classes.

All 105 C++ tests now pass successfully.

* Use const auto for iterator in ShardokGameController

Make iterator constness explicit since it's in a const member function
and the iterator is never modified. This improves code clarity about intent.

* Use const auto for all iterator variables in ShardokEngine

Make iterator constness explicit in all find_if operations since these
iterators are never modified after creation. This improves code clarity
and const correctness throughout the engine placement logic.

* more deprecated removal

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-14 16:36:45 -07:00
44044eb981 Modernize range-based loops with C++17 structured bindings (#4331)
Replace traditional key-value pair iteration patterns with structured bindings:
- HexMapUtils.hpp: Modernize template functions with [unitId, unit] bindings
- GameSettings.cpp: Use [settingName, valueString] destructuring
- PlayerSetupCommandFactory.cpp: Replace kv.second with unit binding
- MapInfoCalculatorRunner.cpp: Use [position, count] for JSON output

This improves code readability by eliminating repetitive .first/.second
member access and makes the intent more explicit. Structured bindings
were introduced in C++17 and provide cleaner, more expressive iteration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-14 06:33:09 -07:00
0016fc86bc Modernize map operations using C++20 contains() method (#4330)
Replace find() \!= end() patterns with more readable contains() + at() approach:
- ActionPointDistancesCache.cpp: Update cache lookup logic
- GameStateGuesser.cpp: Modernize player averages lookup

This improves code readability while maintaining identical performance
characteristics. The contains() method was introduced in C++20 and provides
a cleaner, more expressive way to check map membership.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-14 06:33:01 -07:00
adminandGitHub 06538f3493 update to C++23 (#4329) 2025-08-13 22:03:08 -07:00
45c5183ecb Update LLVM version from 19.1.0 to 20.1.2 (#4328)
- Updates to latest supported LLVM version in toolchains_llvm 1.4.0
- All C++ builds and tests pass successfully with Clang/LLVM 20.1.2
- Shardok server builds successfully in optimized mode

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-13 21:41:42 -07:00
3f304fe57e Update toolchains_llvm from 1.2.0 to 1.4.0 (#4327)
- Updates LLVM toolchain to latest stable version from Bazel Central Registry
- All builds and tests pass successfully with new version

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-13 17:02:16 -07:00
35cb38be65 Update rules_go from 0.50.1 to 0.56.1 (#4325)
- Updated rules_go to latest stable version (0.56.1)
- Verified Go builds complete successfully
- Confirmed Go tests continue to pass

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-13 17:01:26 -07:00
b7f86a2029 Update gazelle from 0.40.0 to 0.45.0 (#4326)
* Update gazelle from 0.40.0 to 0.45.0

- Updated gazelle to latest stable version (0.45.0)
- Verified Go builds complete successfully
- Confirmed Go tests continue to pass

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* run gazelle

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-13 14:02:30 -07:00
57ff4c14fe Update bazel_skylib from 1.7.1 to 1.8.1 (#4323)
- Updated bazel_skylib to latest stable version (1.8.1)
- Verified Eagle server builds successfully
- Confirmed tests continue to pass

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-13 13:15:14 -07:00
9a5ce10600 Update googletest from 1.15.2 to 1.17.0 (#4324)
- Updated googletest to latest stable version (1.17.0)
- Verified Shardok C++ tests pass successfully
- Confirmed no breaking changes in test framework

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-13 12:54:28 -07:00
3f8c999446 Update rules_pkg from 1.0.1 to 1.1.0 (#4322)
- Updated rules_pkg to latest stable version (1.1.0)
- Verified Eagle server builds successfully
- Confirmed tests continue to pass

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-13 09:44:04 -07:00
adminandGitHub 3c8bd1d804 Re-enable another warning (#4321)
* re-enable another warning

* more fixes

* more fixes
2025-08-13 09:40:06 -07:00
adminandGitHub 63e7c04276 ReturnCommand goes protoless (#4320) 2025-08-13 09:14:43 -07:00
adminandGitHub c27f1ec93f rest command goes protoless (#4319)
* rest command goes protoless

* cleanup

* fix the tests too

* missing one

* moar
2025-08-13 08:33:46 -07:00
adminandGitHub 21c11c9afb Yet more warnings (#4318)
* unused parameters

* more

* moar

* moar

* fix some test warnings

* fix some test warnings

* another

* another
2025-08-13 08:01:12 -07:00
adminandGitHub b12a7584a5 fix some warnings and add more copts (#4317)
* fix some warnings and add more copts

* more fixes

* fix more deprecations

* remove that

* cleanup

* cleanup

* a bit more
2025-08-13 07:08:12 -07:00
adminandGitHub d1b752bd56 SuppressBeastsCommand goes protoless (#4316)
* partially working

* legacy

* it builds

* fix existing tests

* and the call site

* moar

* restore the tests

* fix the tests

* build file fix

* cleanup
2025-08-13 06:46:41 -07:00
adminandGitHub bfb78c2b85 No eagle morale (#4315)
* remove all morale references

* remove from CommonUnit too

* and fix unit conversions

* cleanup
2025-08-11 20:13:01 -07:00
adminandGitHub bc3c14bde7 Fix attack decision (#4313)
* fix the attack decision

* better

* implement the tests

* include tests

* closer on tests

* one more
2025-08-11 19:46:16 -07:00
adminandGitHub 353fb08592 cleanup (#4314) 2025-08-10 10:48:01 -07:00
adminandGitHub 74c8ca80bc fix a crasher in SuppressBeastsCommandSelector (#4311) 2025-08-09 18:58:01 -07:00
adminandGitHub f668328983 make a lower assumption about stats until we have some data about the… (#4312)
* make a lower assumption about stats until we have some data about the player's other units

* add tests
2025-08-08 11:13:34 -07:00
adminandGitHub 9fa948d63f Fleeing way too often (#4307)
* what did you do

* kinda messed up

* let's try this way

* fix tests

* put back the check and start fixing the test

* tidies

* fix one test

* more passing

* fix tests
2025-08-07 22:15:08 -07:00
adminandGitHub 86a0212062 more gpt-5 defaulting (#4310) 2025-08-07 20:22:09 -07:00
adminandGitHub f910661c32 change AIScoreUtilities to take a GameStateW& (#4309) 2025-08-07 20:16:16 -07:00
adminandGitHub cd28e2dfcf Use gpt-5 (#4308)
* hmm

* make gpt-5 the default
2025-08-07 19:36:35 -07:00
adminandGitHub 9bccccc3fb only get return prisoner quests for faction leaders (#4306) 2025-08-05 20:49:42 -07:00
adminandGitHub 5603d57e76 No raw GameState pointers in shardok/ai/ (#4305)
* more

* AIWaterCrossing too

* fix build
2025-08-05 19:46:28 -07:00
adminandGitHub 359eceff97 use new flee logic when deciding to flee early (#4304)
* use new flee logic when deciding to flee early

* fix tests

* not so hopeless

* use unit power

* dupes

* fix the overload removals
2025-08-05 19:17:56 -07:00
adminandGitHub acf1af5fcc much simpler (#4303) 2025-08-01 06:45:33 -07:00
adminandGitHub f4e35bf4f0 less likely to flee if odds are lower (#4300)
* less likely to flee if odds are lower

* into settings

* move to another file

* tests

* fix the remaining tests
2025-07-31 21:27:31 -07:00
adminandGitHub a3383f8871 fix a crasher from a bad CLion suggestion (#4302)
* fix a crasher from a bad CLion suggestion

* disable bad advice
2025-07-31 21:23:06 -07:00
adminandGitHub 366d4790cd don't bring more battalions than heroes from a particular province (#4298)
* don't bring more battalions than heroes from a particular province

* unit tests

* gazelle

* more idiomatic

* update tests
2025-07-30 07:48:12 -07:00
adminandGitHub 0dc8b75906 fix a battalion power bug (#4299) 2025-07-30 07:46:30 -07:00
adminandGitHub 363d28984a remove unused code (#4296) 2025-07-28 17:16:35 -07:00
adminandGitHub 4c23716a1e Cache optimizations (#4293)
* eliminate the slow TLS access

* pre-fetch the starting cache values

* hash reserving
2025-07-27 21:15:04 -07:00
adminandGitHub 4a5748552f Tri-level cache (#4292)
* use the same cache key strategy for thread-local vs shared maps

* cleanup

* have a thread-safe universal cache

* use caching in the performance runner

* turn off the cache logging for now

* clear the thread-local cache when consolidating

* hashing optimizations
2025-07-27 08:48:22 -07:00
adminandGitHub 1972e71ff4 some caching in AIScoreCalculator (#4290)
* some caching in AIScoreCalculator

* over-reserve a little
2025-07-23 09:25:14 -07:00
eb58ddba04 Another occupants attempt (#4287)
* put Occupants vector into the gamestate

* Complete embedded occupants vector implementation

- Added GetOccupant() and UpdateOccupant() methods to GameStateW
- Updated AICommandFilter with TODO for future O(1) lookup conversion
- Ready for performance testing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* why is this still slower

* report

* AICommandFilter.cpp

* fix broken tests

* fix tests

* try as a bitfield

* bitfield optimized MoveCommand

* working with move command

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-22 22:05:07 -07:00
adminandGitHub 6b15b63031 make the player id an int8 (#4289) 2025-07-22 11:09:32 -07:00
adminandGitHub 36a2d1b804 GetCurrentGameState() returns a const reference instead of a const pointer (#4288)
* replaced some

* replace them all

* rename back
2025-07-22 07:01:35 -07:00
adminandGitHub fea5888f11 no professions for starting random heroes (#4286) 2025-07-20 21:17:36 -07:00
adminandGitHub 45a9081b46 more flat_hash_map (#4285) 2025-07-20 18:01:40 -07:00
adminandGitHub ff4576eb85 reserve space for extra units (#4284)
* reserve space

* grab a reserved slot

* add to the guessed state as well

* fix the tests

* optimize MutatingAddUnits

* early exit
2025-07-20 17:25:32 -07:00
adminandGitHub 9ae3aad7a4 speed up vector pushes in MoveCommand (#4283) 2025-07-18 16:11:45 -07:00
adminandGitHub 8e9cebaffa clear ice before generating distances (#4281)
* clear ice before generating distances

* fix these types

* avoid copy when possible

* more optimizations

* remove ice from the hash

* use fixed64

* minor comment

* cleanup

* tiny bit more

* cleanup

* don't check for ice if we don't have to
2025-07-18 09:34:15 -07:00
89f638a599 change ByteHasher to use uint64_t values (#4282)
* use uint64_t values

* Update src/main/cpp/net/eagle0/common/ByteHasher.hpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-18 07:17:01 -07:00
adminandGitHub 9735374c70 Better handling of LLM failures (#4280)
* re-increment counter

* proper retry handling
2025-07-17 20:50:20 -07:00
adminandGitHub dd2a397c55 perf-test (#4279) 2025-07-17 19:24:39 -07:00
adminandGitHub 4415ce175e update claude.md (#4278) 2025-07-17 17:40:47 -07:00
adminandGitHub 05dd0f5c39 Better metrics (#4276)
* pass through whether we completed all meaningful commands

* add an asterisk

* correct depth eval
2025-07-16 17:07:15 -07:00
54494c973b Performance test (#4275)
* missing dep

* cleanup

* Add AI Performance Runner implementation plan

Create comprehensive plan for automated AI performance testing tool that
replicates the manual "Perf" button testing from Unity client. The tool
will provide reproducible performance measurements without requiring
client interaction.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* slow progress

* getting there

* it runs

* it runs

* fully runs

* fully runs

* omg is it working

* removed a lot of loggin

* summary data

* Update AI performance runner to use CommandChoiceResults metrics

- Replace timing-based metrics with search depth and evaluation counts
- Use CommandChoiceResults returned by ShardokAIClient methods
- Display key performance metrics: depth achieved, commands evaluated vs available
- Calculate average search depth and evaluation rate across turns
- Show turn-by-turn breakdown with command types chosen
- Remove obsolete timing measurements in favor of AI budget-based metrics

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add evaluation rate by depth analysis

- Replace meaningless average evaluation rate with depth-specific rates
- Show evaluation percentage at each depth level achieved
- Account for turns that reached higher depths (100% assumed for lower depths)
- Display how many turns reached each depth level
- Provides meaningful insight into time budget utilization at each search level

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Force optimization for AI performance runner binary

- Add -O3 and -DNDEBUG flags to copts for ai_performance_runner binary
- Ensures the performance testing tool always runs optimized regardless of build mode
- Critical for accurate AI performance measurements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* bad eval

* run gazelle

* Revert copts optimization and add ai_perf_test.sh script

- Revert BUILD.bazel copts changes (insufficient for global optimization)
- Add scripts/ai_perf_test.sh that runs with "bazel run -c opt"
- Script defaults to 10 turns and accepts additional arguments
- Global -c opt dramatically improves AI performance (depth 3 vs depth 2)
- Ensures all AI dependencies are optimized for accurate performance testing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* review comments

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-16 16:20:49 -07:00
a9d41b59fd Return perf data from ShardokAIClient (#4274)
* capture the metrics in ShardokAIClient

* clean up logging

* Address PR review comments

- Replace macro with constexpr bool for performance logging
- Add documentation comments for CommandChoiceResults struct
- Use if constexpr instead of preprocessor directives

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-16 14:29:15 -07:00
adminandGitHub bf1b87612c Make GameStateW a class (#4273)
* replace the typedef/using declarations with a real GameStateW class

* missing dep

* addres comments

* cleanup

* fix test
2025-07-15 10:53:35 -07:00
adminandGitHub 713715620c Don't make mutations to the running GameStateW in MoveCommand (#4272)
* move command test is failing

* fix the test
2025-07-15 07:19:28 -07:00
adminandGitHub c64c3edbe6 remove one mutation (#4270) 2025-07-15 06:42:16 -07:00
adminandGitHub 70e43e693d perf: change Execute() to take a const shared_ptr reference to avoid reference counting (#4266)
* avoid reference counting in .Execute()

* fix the tests

* add the performance plan
2025-07-13 11:31:34 -07:00
344 changed files with 7609 additions and 3009 deletions
+48
View File
@@ -72,6 +72,18 @@ bazel run gazelle # Update Go build files
./scripts/updateActionResultTypes.sh # Update protocol buffer mappings
```
### Code Formatting
```bash
# ALWAYS run clang-format after making any C++ or C# code changes
clang-format -i <modified_files>
# Format all C++ files in a directory:
find . -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i
# Format all C# files in a directory:
find . -name "*.cs" | xargs clang-format -i
```
## Language-Specific Patterns
**Scala (Strategic Layer):**
@@ -110,6 +122,42 @@ bazel run gazelle # Update Go build files
- Map validation tests ensure game content integrity
- Use `GameSettings_test_utils.cpp` and `ShardokEngineBasedTestData.cpp` for C++ test helpers
## Performance Testing
When making performance-related changes to the AI or engine:
```bash
# 1. Commit your changes to a feature branch
git checkout -b performance-improvement-feature
git add . && git commit -m "Implement performance improvement"
# 2. Run performance tests multiple times on your branch to reduce noise
for i in 1 2 3; do
echo "=== Run $i ==="
./scripts/ai_perf_test.sh 2>&1 | grep -A 20 "AI Search Performance Summary"
done
# Save or note the results
# 3. Switch to main branch and run the same tests
git checkout main
for i in 1 2 3; do
echo "=== Run $i ==="
./scripts/ai_perf_test.sh 2>&1 | grep -A 20 "AI Search Performance Summary"
done
# 4. Compare the results between your branch and main
# Key metrics to compare:
# - Commands evaluated at each depth (e.g., "Depth 3: 169/523 commands")
# - Average search depth achieved
# - Completion rates at each depth
```
**Important notes:**
- Run tests multiple times (3-5) to account for performance variance
- Focus on commands evaluated at each depth rather than total commands
- Commands at different depths aren't directly comparable (depth 3 is more valuable than depth 2)
- **Always test performance changes** - what seems like an optimization may sometimes have unexpected overhead or behavior changes.
## Game Content
**Maps:** `.e0mj` files in `/src/main/resources/net/eagle0/shardok/maps/`
+7 -7
View File
@@ -4,14 +4,14 @@ bazel_dep(name = "apple_support", repo_name = "build_bazel_apple_support", versi
# bazel-toolchain
#
bazel_dep(name = "toolchains_llvm", version = "1.2.0")
bazel_dep(name = "toolchains_llvm", version = "1.4.0")
# Configure and register the toolchain.
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
llvm.toolchain(
name = "llvm_toolchain",
llvm_version = "19.1.0",
llvm_version = "20.1.2",
)
use_repo(llvm, "llvm_toolchain")
@@ -22,14 +22,14 @@ register_toolchains(
dev_dependency = True,
)
bazel_dep(name = "rules_pkg", version = "1.0.1")
bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(name = "rules_pkg", version = "1.1.0")
bazel_dep(name = "bazel_skylib", version = "1.8.1")
bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "29.2")
bazel_dep(name = "grpc", version = "1.71.0")
bazel_dep(name = "grpc-java", version = "1.71.0")
bazel_dep(name = "googletest", version = "1.15.2")
bazel_dep(name = "rules_go", repo_name = "io_bazel_rules_go", version = "0.50.1")
bazel_dep(name = "gazelle", repo_name = "bazel_gazelle", version = "0.40.0")
bazel_dep(name = "googletest", version = "1.17.0")
bazel_dep(name = "rules_go", repo_name = "io_bazel_rules_go", version = "0.56.1")
bazel_dep(name = "gazelle", repo_name = "bazel_gazelle", version = "0.45.0")
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
+61 -35
View File
@@ -13,7 +13,8 @@
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16",
"https://bcr.bazel.build/modules/abseil-cpp/20240722.0/MODULE.bazel": "88668a07647adbdc14cb3a7cd116fb23c9dda37a90a1681590b6c9d8339a5b84",
"https://bcr.bazel.build/modules/abseil-cpp/20240722.0/source.json": "59af9f8a8a4817092624e21263fe1fb7d7951a3b06f0570c610c7e5a9caf5f29",
"https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1",
"https://bcr.bazel.build/modules/abseil-cpp/20250127.1/source.json": "03c90ee57977264436d3231676dcddae116c4769a5d02b6fc16c2c9e019b583a",
"https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896",
"https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85",
"https://bcr.bazel.build/modules/apple_support/1.17.1/MODULE.bazel": "655c922ab1209978a94ef6ca7d9d43e940cd97d9c172fb55f94d91ac53f8610b",
@@ -40,6 +41,7 @@
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
"https://bcr.bazel.build/modules/bazel_features/1.27.0/source.json": "ed8cf0ef05c858dce3661689d0a2b110ff398e63994e178e4f1f7555a8067fed",
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
@@ -57,7 +59,8 @@
"https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/source.json": "7ebaefba0b03efe59cac88ed5bbc67bcf59a3eff33af937345ede2a38b2d368a",
"https://bcr.bazel.build/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "6ee6353f8b1a701fe2178e1d925034294971350b6d3ac37e67e5a7d463267834",
"https://bcr.bazel.build/modules/boringssl/0.0.0-20230215-5c22014/MODULE.bazel": "4b03dc0d04375fa0271174badcd202ed249870c8e895b26664fd7298abea7282",
"https://bcr.bazel.build/modules/boringssl/0.0.0-20240530-2db0eb3/MODULE.bazel": "d0405b762c5e87cd445b7015f2b8da5400ef9a8dbca0bfefa6c1cea79d528a97",
@@ -91,8 +94,8 @@
"https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a",
"https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0",
"https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel": "d1327ba0907d0275ed5103bfbbb13518f6c04955b402213319d0d6c0ce9839d4",
"https://bcr.bazel.build/modules/gazelle/0.40.0/MODULE.bazel": "42ba5378ebe845fca43989a53186ab436d956db498acde790685fe0e8f9c6146",
"https://bcr.bazel.build/modules/gazelle/0.40.0/source.json": "1e5ef6e4d8b9b6836d93273c781e78ff829ea2e077afef7a57298040fa4f010a",
"https://bcr.bazel.build/modules/gazelle/0.45.0/MODULE.bazel": "ecd19ebe9f8e024e1ccffb6d997cc893a974bcc581f1ae08f386bdd448b10687",
"https://bcr.bazel.build/modules/gazelle/0.45.0/source.json": "111d182facc5f5e80f0b823d5f077b74128f40c3fd2eccc89a06f34191bd3392",
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
"https://bcr.bazel.build/modules/google_benchmark/1.8.4/MODULE.bazel": "c6d54a11dcf64ee63545f42561eda3fd94c1b5f5ebe1357011de63ae33739d5e",
"https://bcr.bazel.build/modules/google_benchmark/1.8.5/MODULE.bazel": "9ba9b31b984022828a950e3300410977eda2e35df35584c6b0b2d0c2e52766b7",
@@ -104,7 +107,8 @@
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
"https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108",
"https://bcr.bazel.build/modules/googletest/1.15.2/source.json": "dbdda654dcb3a0d7a8bc5d0ac5fc7e150b58c2a986025ae5bc634bb2cb61f470",
"https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46",
"https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713",
"https://bcr.bazel.build/modules/grpc-java/1.62.2/MODULE.bazel": "99b8771e8c7cacb130170fed2a10c9e8fed26334a93e73b42d2953250885a158",
"https://bcr.bazel.build/modules/grpc-java/1.66.0/MODULE.bazel": "86ff26209fac846adb89db11f3714b3dc0090fb2fb81575673cc74880cda4e7e",
"https://bcr.bazel.build/modules/grpc-java/1.69.0/MODULE.bazel": "53887af6a00b3b406d70175d3d07e84ea9362016ff55ea90b9185f0227bfaf98",
@@ -146,14 +150,18 @@
"https://bcr.bazel.build/modules/opentelemetry-proto/1.5.0/source.json": "046b721ce203e88cdaad44d7dd17a86b7200eab9388b663b234e72e13ff7b143",
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/MODULE.bazel": "b3925269f63561b8b880ae7cf62ccf81f6ece55b62cd791eda9925147ae116ec",
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed",
"https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4",
"https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85",
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
"https://bcr.bazel.build/modules/platforms/0.0.10/source.json": "f22828ff4cf021a6b577f1bf6341cb9dcd7965092a439f64fc1bb3b7a5ae4bd5",
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
"https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
"https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
"https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
"https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814",
"https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d",
"https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc",
"https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580",
"https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96",
"https://bcr.bazel.build/modules/prometheus-cpp/1.2.4/MODULE.bazel": "0fbe5dcff66311947a3f6b86ebc6a6d9328e31a28413ca864debc4a043f371e5",
"https://bcr.bazel.build/modules/prometheus-cpp/1.3.0/MODULE.bazel": "ce82e086bbc0b60267e970f6a54b2ca6d0f22d3eb6633e00e2cc2899c700f3d8",
"https://bcr.bazel.build/modules/prometheus-cpp/1.3.0/source.json": "8cb66b4e535afc718e9d104a3db96ccb71a42ee816a100e50fd0d5ac843c0606",
@@ -166,7 +174,9 @@
"https://bcr.bazel.build/modules/protobuf/27.0-rc2/MODULE.bazel": "b2b0dbafd57b6bec0ca9b251da02e628c357dab53a097570aa7d79d020f107cf",
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
"https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d",
"https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c",
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
"https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e",
"https://bcr.bazel.build/modules/protobuf/29.2/MODULE.bazel": "5435497c190d86f79b0568698c45044df7c8d97692886cda9fe9cf9053aea712",
"https://bcr.bazel.build/modules/protobuf/29.2/source.json": "fe7090cc34072609b26d9beafb122916dabc1d47ba61b242c26c4b06c51384ab",
@@ -185,8 +195,9 @@
"https://bcr.bazel.build/modules/re2/2021-09-01/MODULE.bazel": "bcb6b96f3b071e6fe2d8bed9cc8ada137a105f9d2c5912e91d27528b3d123833",
"https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
"https://bcr.bazel.build/modules/re2/2024-05-01/MODULE.bazel": "55a3f059538f381107824e7d00df5df6d061ba1fb80e874e4909c0f0549e8f3e",
"https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a",
"https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/source.json": "2ff292be6ef3340325ce8a045ecc326e92cbfab47c7cbab4bd85d28971b97ac4",
"https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa",
"https://bcr.bazel.build/modules/re2/2024-07-02/source.json": "547d0111a9d4f362db32196fef805abbf3676e8d6afbe44d395d87816c1130ca",
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
"https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
@@ -220,7 +231,9 @@
"https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd",
"https://bcr.bazel.build/modules/rules_go/0.48.0/MODULE.bazel": "d00ebcae0908ee3f5e6d53f68677a303d6d59a77beef879598700049c3980a03",
"https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0",
"https://bcr.bazel.build/modules/rules_go/0.50.1/source.json": "205765fd30216c70321f84c9a967267684bdc74350af3f3c46c857d9f80a4fa2",
"https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a",
"https://bcr.bazel.build/modules/rules_go/0.56.1/MODULE.bazel": "d5b835c548ac917345f1780cd2da52edc1130a908fe091c92096895303ae78a0",
"https://bcr.bazel.build/modules/rules_go/0.56.1/source.json": "0c902f7272e8d4e47e459af97be472bc19dadbbe6023a0719d1adce8483ac75a",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.1.0/MODULE.bazel": "324b6478b0343a3ce7a9add8586ad75d24076d6d43d2f622990b9c1cfd8a1b15",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
@@ -232,12 +245,14 @@
"https://bcr.bazel.build/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64",
"https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
"https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
"https://bcr.bazel.build/modules/rules_java/7.12.2/source.json": "b0890f9cda8ff1b8e691a3ac6037b5c14b7fd4134765a3946b89f31ea02e5884",
"https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
"https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2",
"https://bcr.bazel.build/modules/rules_java/7.4.0/MODULE.bazel": "a592852f8a3dd539e82ee6542013bf2cadfc4c6946be8941e189d224500a8934",
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
"https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1",
"https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
"https://bcr.bazel.build/modules/rules_java/8.5.1/source.json": "db1a77d81b059e0f84985db67a22f3f579a529a86b7997605be3d214a0abe38e",
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
"https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
@@ -259,7 +274,8 @@
"https://bcr.bazel.build/modules/rules_nodejs/6.3.3/source.json": "45bd343155bdfed2543f0e39b80ff3f6840efc31975da4b5795797f4c94147ad",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/MODULE.bazel": "9db8031e71b6ef32d1846106e10dd0ee2deac042bd9a2de22b4761b0c3036453",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/source.json": "fef768df13a92ce6067e1cd0cdc47560dace01354f1d921cfb1d632511f7d608",
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
@@ -281,7 +297,8 @@
"https://bcr.bazel.build/modules/rules_python/0.37.1/MODULE.bazel": "3faeb2d9fa0a81f8980643ee33f212308f4d93eea4b9ce6f36d0b742e71e9500",
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
"https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7",
"https://bcr.bazel.build/modules/rules_python/0.40.0/source.json": "939d4bd2e3110f27bfb360292986bb79fd8dcefb874358ccd6cdaa7bda029320",
"https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
"https://bcr.bazel.build/modules/rules_python/1.0.0/source.json": "b0162a65c6312e45e7912e39abd1a7f8856c2c7e41ecc9b6dc688a6f6400a917",
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
"https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
"https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3",
@@ -297,11 +314,12 @@
"https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel": "7060193196395f5dd668eda046ccbeacebfd98efc77fed418dbe2b82ffaa39fd",
"https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
"https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7",
"https://bcr.bazel.build/modules/stardoc/0.7.1/source.json": "b6500ffcd7b48cd72c29bb67bcac781e12701cc0d6d55d266a652583cfcdab01",
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
"https://bcr.bazel.build/modules/toolchains_llvm/1.2.0/MODULE.bazel": "7b271b71e50de47fa47159a7f58165e80fcebe1196c014a14af0a08a867d1635",
"https://bcr.bazel.build/modules/toolchains_llvm/1.2.0/source.json": "0328cfc67075d6a016980be4011bbc1dcfba933e357568002542dff22abdd3a1",
"https://bcr.bazel.build/modules/toolchains_llvm/1.4.0/MODULE.bazel": "05239402b7374293359c2f22806f420b75aa5d6f4b15a2eaa809a2c214d58b31",
"https://bcr.bazel.build/modules/toolchains_llvm/1.4.0/source.json": "229a516d282b17a82be54c6e3ae220a1b750fb55a8495567e5c7a9d09423f3e2",
"https://bcr.bazel.build/modules/upb/0.0.0-20211020-160625a/MODULE.bazel": "6cced416be2dc5b9c05efd5b997049ba795e5e4e6fafbe1624f4587767638928",
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9",
@@ -663,23 +681,6 @@
"recordedRepoMappingEntries": []
}
},
"@@platforms//host:extension.bzl%host_platform": {
"general": {
"bzlTransitiveDigest": "xelQcPZH8+tmuOHVjL9vDxMnnQNMlwj0SlvgoqBkm4U=",
"usagesDigest": "ibwLwKXW2CiKpWzexj5KO6SHidEgCwJvu+ebsPxirn4=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"host_platform": {
"bzlFile": "@@platforms//host:extension.bzl",
"ruleClassName": "host_platform_repo",
"attributes": {}
}
},
"recordedRepoMappingEntries": []
}
},
"@@rules_foreign_cc~//foreign_cc:extensions.bzl%tools": {
"general": {
"bzlTransitiveDigest": "a7qnESofmIRYId6wwGNPJ9kvExU80KrkxL281P3+lBE=",
@@ -1020,6 +1021,29 @@
]
}
},
"@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": {
"general": {
"bzlTransitiveDigest": "KIX40nDfygEWbU+rq3nYpt3tVgTK/iO8PKh5VMBlN7M=",
"usagesDigest": "pwHZ+26iLgQdwvdZeA5wnAjKnNI3y6XO2VbhOTeo5h8=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"compatibility_proxy": {
"bzlFile": "@@rules_java~//java:rules_java_deps.bzl",
"ruleClassName": "_compatibility_proxy_repo_rule",
"attributes": {}
}
},
"recordedRepoMappingEntries": [
[
"rules_java~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_kotlin~//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
"general": {
"bzlTransitiveDigest": "fus14IFJ/1LGWWGKPH/U18VnJCoMjfDt1ckahqCnM0A=",
@@ -1389,8 +1413,8 @@
},
"@@toolchains_llvm~//toolchain/extensions:llvm.bzl%llvm": {
"general": {
"bzlTransitiveDigest": "dZkcElkfice3BvKPXtFwrvPABm8ac+SwfynSinwK4dQ=",
"usagesDigest": "Phpqeo3bkW3l2tD5aOSKApubwu0KPfl0KuPgjY6+ueM=",
"bzlTransitiveDigest": "afRF0aFOIUrkYl3o040WQ606ep1qciEXzjnAxT3Kek8=",
"usagesDigest": "wl2njI0HdPm89XuKGMWIa1gyCAQ5gvOs+ksjFd4QbZs=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -1404,8 +1428,9 @@
"distribution": "auto",
"exec_arch": "",
"exec_os": "",
"libclang_rt": {},
"llvm_mirror": "",
"llvm_version": "19.1.0",
"llvm_version": "20.1.2",
"llvm_versions": {},
"netrc": "",
"sha256": {},
@@ -1420,6 +1445,7 @@
"absolute_paths": false,
"archive_flags": {},
"compile_flags": {},
"conly_flags": {},
"coverage_compile_flags": {},
"coverage_link_flags": {},
"cxx_builtin_include_directories": {},
@@ -1433,7 +1459,7 @@
"link_flags": {},
"link_libs": {},
"llvm_versions": {
"": "19.1.0"
"": "20.1.2"
},
"opt_compile_flags": {},
"opt_link_flags": {},
+206
View File
@@ -0,0 +1,206 @@
# Occupants Vector Optimization - Conversion Report
## 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.
## Completed Conversions
### Successfully Converted Occupant() Calls (16 total)
#### Commands Directory (11 conversions)
1. **HideCommand.cpp**:
- Line 43: `Occupant(currentState->units(), target)``currentState.GetOccupant(target)`
- Line 59: `Occupant(currentState->units(), adjCoords)``currentState.GetOccupant(adjCoords)`
2. **ScoutCommand.cpp**:
- Line 63: `Occupant(currentState->units(), target)``currentState.GetOccupant(target)`
- Line 73: `Occupant(currentState->units(), adjacentCoords)``currentState.GetOccupant(adjacentCoords)`
3. **ReduceCommand.cpp**:
- Line 66: `Occupant(currentState->units(), target)``currentState.GetOccupant(target)`
4. **RaiseDeadCommand.cpp**:
- Line 53: `Occupant(currentState->units(), target)``currentState.GetOccupant(target)`
5. **HolyWaveCommand.cpp**:
- Line 233: `Occupant(runningState->units(), coords)``runningState.GetOccupant(coords)`
6. **MoveCommand.cpp**:
- Line 66: `Occupant(allUnits, destination)``currentState.GetOccupant(destination)`
- Line 98: `Occupant(allUnits, adj)``currentState.GetOccupant(adj)`
- Line 114: `Occupant(allUnits, adj)``currentState.GetOccupant(adj)`
#### Actions Directory (4 conversions)
1. **UpdateGameStatusAction.cpp**:
- Line 232: `Occupant(gameState->units(), criticalTile)``currentState.GetOccupant(criticalTile)`
2. **MeteorCastAction.cpp**:
- Line 186: `Occupant(runningGameState->units(), target)``runningGameState.GetOccupant(target)`
- Line 251: `Occupant(runningGameState->units(), splashCoords)``runningGameState.GetOccupant(splashCoords)`
- Line 304: `Occupant(runningGameState->units(), coords)``runningGameState.GetOccupant(coords)`
3. **UpdateOpponentKnowledgeAction.cpp**:
- Line 42: `Occupant(currentState->units(), adjCoords)``currentState.GetOccupant(adjCoords)`
#### Engine Directory (1 conversion)
1. **ShardokEngine.cpp**:
- Line 463: `Occupant(GetCurrentGameState()->units(), modifiedCoords)``gameState.GetOccupant(modifiedCoords)`
#### Factory Classes Directory (previously converted)
1. **PlayerSetupCommandFactory.cpp**:
- Line 31: `Occupant(gameState->units(), *possiblePosition)``gameState.GetOccupant(*possiblePosition)`
- Line 40: `Occupant(gameState->units(), possibleHidingPosition)``gameState.GetOccupant(possibleHidingPosition)`
2. **FallIntoWaterAction.cpp**:
- Line 154: `Occupant(currentState->units(), adjWithTerrain.adjacentCoords)`
`currentState.GetOccupant(adjWithTerrain.adjacentCoords)`
- Line 175: `Occupant(currentState->units(), bestCoords)``currentState.GetOccupant(bestCoords)`
### KnownEnemyOccupant() Conversions
**Result: 0 conversions possible**
All KnownEnemyOccupant() calls are in command factory methods that receive decomposed game state parameters (Units*,
vector<PlayerId>, etc.) rather than complete GameStateW objects.
## Remaining Unconverted Calls
### Occupant() Calls That Cannot Be Converted
#### 1. PerformUndeadCommandsAction.cpp (2 calls - No GameStateW access)
- **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`
#### 2. AICommandFilter.cpp (1 call - Raw pointer access)
- **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`
#### 3. UpdateGameStatusAction.cpp - Member Variable Usage
- **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`
#### 4. IceAndSnowAdjustmentActionFactory.cpp (1 call - Factory pattern)
- **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`
### KnownEnemyOccupant() Calls That Cannot Be Converted
#### Command Factory Methods (8 calls - No GameStateW access)
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
**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.
#### Utility Functions (3 calls - Utility function parameters)
1. **HexMapUtils.cpp** - Lines 81, 670
2. **ZoneOfControlCalculator.cpp** - Line 143
**Reason**: These are utility functions that take decomposed parameters for reusability across different contexts.
## Performance Impact
### Achieved Improvements
- **16 Occupant() calls** converted from O(n) iteration to O(1) lookup
- 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
### Trade-offs
- **Memory Overhead**: 168 bytes per GameState (14×12 map = 168 int16 values)
- **Incremental Updates**: ActionResultApplier now maintains occupants vector via UpdateOccupant() calls
- **Copy Cost**: Slightly higher GameState copy overhead offset by O(1) lookup benefits
## Architectural Patterns Identified
### Convertible Patterns
1. **Command InternalExecute methods**: Have access to `const GameStateW& currentState`
2. **Action InternalExecute methods**: Have access to `const GameStateW& currentState`
3. **Factory methods with GameStateW parameters**: Can access embedded occupants vector
### Non-Convertible Patterns
1. **Command Factory methods**: Receive decomposed parameters (`Units*`, `HexMap*`, etc.)
2. **Utility functions**: Take individual components for reusability
3. **Engine methods**: Often work with raw `GameState*` pointers
4. **Legacy member variables**: Classes storing `const GameState*` instead of `GameStateW`
## Recommendations for Future Work
### Potential Additional 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
### Copy-on-Write Consideration
The user suggested implementing copy-on-write (COW) for GameStateW to reduce memory allocation overhead during AI
search. This could provide additional performance benefits by eliminating unnecessary copying of the occupants vector.
## Technical Implementation Details
### 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
### Key Method Signatures
```cpp
// O(1) occupant lookup
auto GameStateW::GetOccupant(const Coords& coords) const -> const Unit*;
// O(1) enemy occupant lookup
auto GameStateW::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);
```
## 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.
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
set -e
# AI Performance Test Runner Script
# Runs the AI performance test with optimized builds and 10 turns
echo "Running AI performance test with optimized build..."
echo "=============================================="
# Run with optimized compilation and 10 turns
bazel run -c opt //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --turns=10 "$@"
+15 -7
View File
@@ -22,13 +22,6 @@ cc_library(
visibility = ["//visibility:public"],
)
cc_library(
name = "container_utils",
hdrs = ["ContainerUtils.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
)
cc_library(
name = "filesystem_utils",
srcs = ["FilesystemUtils.cpp"],
@@ -95,6 +88,21 @@ cc_library(
],
)
cc_library(
name = "task_result",
hdrs = ["TaskResult.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
)
cc_library(
name = "thread_pool",
hdrs = ["ThreadPool.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
deps = [":task_result"],
)
cc_library(
name = "time_utils",
hdrs = ["TimeUtils.hpp"],
+16 -5
View File
@@ -7,12 +7,23 @@
#include <cstdint>
constexpr int64_t FNV_PRIME = 0x100000001b3;
constexpr int64_t FNV_OFFSET_BASIS = 0xcbf29ce484222325;
// FNV-1a 64-bit constants
constexpr uint64_t FNV_PRIME = 0x00000100000001B3ULL;
constexpr uint64_t FNV_OFFSET_BASIS = 0xcbf29ce484222325ULL;
static inline auto MixIn(int64_t& hash, const uint8_t byte) {
hash = hash * FNV_PRIME;
hash = hash ^ byte;
// FNV-1a algorithm: XOR first, then multiply
static inline auto MixIn(uint64_t& hash, const uint8_t byte) {
hash ^= byte;
hash *= FNV_PRIME;
}
// Hash an entire buffer using FNV-1a
static inline auto HashBuffer(const uint8_t* data, size_t size) -> uint64_t {
uint64_t hash = FNV_OFFSET_BASIS;
if (data != nullptr) {
for (size_t i = 0; i < size; ++i) { MixIn(hash, data[i]); }
}
return hash;
}
#endif // EAGLE0_BYTEHASHER_HPP
@@ -1,173 +0,0 @@
//
// Created by Dan Crosby on 12/25/20.
//
#ifndef EAGLE0_CONTAINERUTILS_HPP
#define EAGLE0_CONTAINERUTILS_HPP
#include <algorithm>
#include <functional>
#include <optional>
namespace common {
using std::allocator;
using std::back_inserter;
using std::begin;
using std::copy_if;
using std::count_if;
using std::end;
using std::find;
using std::find_if;
using std::function;
using std::optional;
using std::remove_if;
using std::vector;
template<class T, class Container>
auto Contains(const Container& container, const T& elt) -> bool {
return find(begin(container), end(container), elt) != end(container);
}
template<class Container, class Func>
auto CountIf(const Container& container, Func fn) -> size_t {
Container result{};
return count_if(begin(container), end(container), fn);
}
template<class Container, class Func>
void FilterInPlace(Container& container, Func fn) {
container.erase(
remove_if(begin(container), end(container), [fn](const auto& elt) { return !fn(elt); }),
end(container));
}
template<class Container, class Func>
auto Filtered(const Container& container, Func fn) -> Container {
Container result{};
copy_if(begin(container), end(container), back_inserter(result), fn);
return result;
}
template<class Container, class Func>
auto FilteredToVector(const Container& container, Func fn) -> decltype(auto) {
typedef typename Container::value_type value_type;
vector<value_type> result{};
copy_if(begin(container), end(container), back_inserter(result), fn);
return result;
}
template<typename Container, typename Func>
auto FindIf(const Container& container, Func fn) -> optional<typename Container::value_type> {
const auto& t = find_if(begin(container), end(container), fn);
if (t == end(container)) {
return {};
} else {
return optional<typename Container::value_type>(*t);
}
}
template<typename Container, typename Func>
auto ContainsWhere(const Container& container, Func fn) -> bool {
return find_if(begin(container), end(container), fn) != end(container);
}
template<
template<typename, typename>
class TwoTypeContainer,
typename T,
typename Allocator = allocator<T>,
typename Func>
auto Map(const TwoTypeContainer<T, Allocator>& input, Func fn) -> decltype(auto) {
typedef typename decltype(function(fn))::result_type result_type;
TwoTypeContainer<result_type, allocator<result_type>> result{};
result.reserve(input.size());
transform(begin(input), end(input), back_inserter(result), fn);
return result;
}
template<template<typename> class OneTypeContainer, typename T, typename Func>
auto Map(const OneTypeContainer<T>& input, Func fn) -> decltype(auto) {
typedef typename decltype(function(fn))::result_type result_type;
OneTypeContainer<result_type> result{};
result.reserve(input.size());
transform(begin(input), end(input), back_inserter(result), fn);
return result;
}
template<typename Container, typename Func>
auto MapToVector(const Container& input, Func fn) -> decltype(auto) {
typedef typename decltype(function(fn))::result_type result_type;
vector<result_type> result{};
transform(begin(input), end(input), back_inserter(result), fn);
return result;
}
template<
template<typename, typename>
class TwoTypeContainer,
typename T,
typename Allocator = allocator<T>,
typename Func>
auto FlatMap(const TwoTypeContainer<T, Allocator>& input, Func fn) -> decltype(auto) {
typedef typename decltype(function(fn))::result_type::value_type result_value_type;
TwoTypeContainer<result_value_type, allocator<result_value_type>> result{};
for (const auto& elt : input) {
const auto& outContainer = fn(elt);
for (const auto& outElt : outContainer) { result.push_back(outElt); }
}
return result;
}
template<template<typename> class OneTypeContainer, typename T, typename Func>
auto FlatMap(const OneTypeContainer<T>& input, Func fn) -> decltype(auto) {
typedef typename decltype(function(fn))::result_type::value_type result_value_type;
OneTypeContainer<result_value_type> result{};
for (const auto& elt : input) {
const auto& outContainer = fn(elt);
for (const auto& outElt : outContainer) { result.push_back(outElt); }
}
return result;
}
template<typename Container, typename Func>
auto FlatMapToVector(const Container& input, Func fn) -> decltype(auto) {
typedef typename decltype(function(fn))::result_type::value_type value_type;
vector<value_type> result{};
for (const auto& elt : input) {
const auto& outContainer = fn(elt);
for (const auto& outElt : outContainer) { result.push_back(outElt); }
}
return result;
}
template<typename Container>
auto ToVector(const Container& input) -> decltype(auto) {
typedef typename Container::value_type value_type;
return vector<value_type>(begin(input), end(input));
}
template<typename C1, typename C2>
auto Append(C1& recipient, const C2& newItems) -> C1& {
recipient.insert(end(recipient), begin(newItems), end(newItems));
return recipient;
}
} // namespace common
#endif // EAGLE0_CONTAINERUTILS_HPP
@@ -145,7 +145,7 @@ auto FilesystemUtils::LoadFromPath(const string& path) -> byte_vector {
const std::streamsize size = inputFileStream.tellg();
inputFileStream.seekg(0, std::ios::beg);
auto bv = byte_vector(size);
auto bv = byte_vector(static_cast<size_t>(size));
inputFileStream.read((char*)bv.data(), size);
return bv;
@@ -84,7 +84,9 @@ auto RandomGenerator::ChanceOpenEndedPercentileAtOrAbove(const double value) ->
auto StdLibraryGenerator::DoubleZeroToOne() -> double { return unifDouble(engine); }
StdLibraryGenerator::StdLibraryGenerator() : RandomGenerator() { engine.seed(std::time(nullptr)); }
StdLibraryGenerator::StdLibraryGenerator() : RandomGenerator() {
engine.seed(static_cast<std::mt19937_64::result_type>(std::time(nullptr)));
}
auto StdLibraryGenerator::IntBetween(const int min, const int max) -> int {
std::uniform_int_distribution<int> unifInt(min, max - 1);
@@ -0,0 +1,39 @@
//
// TaskResult.hpp - Result wrapper for task execution with status information
//
#ifndef EAGLE0_TASK_RESULT_HPP
#define EAGLE0_TASK_RESULT_HPP
namespace eagle0::common {
enum class TaskStatus { SUCCESS = 0, DEADLINE_EXCEEDED = 1, CANCELLED = 2 };
template<typename T>
struct TaskResult {
T value;
TaskStatus status;
TaskResult() : value{}, status(TaskStatus::SUCCESS) {}
TaskResult(T val) : value(std::move(val)), status(TaskStatus::SUCCESS) {}
TaskResult(T val, TaskStatus stat) : value(std::move(val)), status(stat) {}
// Convenience methods for checking status
T get() const { return value; }
bool succeeded() const { return status == TaskStatus::SUCCESS; }
bool deadlineExceeded() const { return status == TaskStatus::DEADLINE_EXCEEDED; }
bool cancelled() const { return status == TaskStatus::CANCELLED; }
// Factory methods for cleaner construction
static TaskResult Success(T val) { return TaskResult(std::move(val), TaskStatus::SUCCESS); }
static TaskResult DeadlineExceeded(T val = T{}) {
return TaskResult(std::move(val), TaskStatus::DEADLINE_EXCEEDED);
}
static TaskResult Cancelled(T val = T{}) {
return TaskResult(std::move(val), TaskStatus::CANCELLED);
}
};
} // namespace eagle0::common
#endif // EAGLE0_TASK_RESULT_HPP
@@ -0,0 +1,14 @@
//
// ThreadPool.cpp - Implementation of priority-based thread pool
//
#include "ThreadPool.hpp"
namespace eagle0 {
namespace common {
// Implementation is header-only to support templates
// This file exists for potential future non-template implementations
} // namespace common
} // namespace eagle0
@@ -0,0 +1,307 @@
//
// ThreadPool.hpp - Priority-based thread pool with deadline support
//
#ifndef EAGLE0_THREADPOOL_HPP
#define EAGLE0_THREADPOOL_HPP
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include "TaskResult.hpp"
namespace eagle0::common {
// Metrics structure for ThreadPool session statistics
struct ThreadPoolMetrics {
size_t tasks_enqueued = 0;
size_t tasks_succeeded = 0;
size_t tasks_deadline_exceeded = 0;
size_t tasks_cancelled = 0;
double average_thread_load = 0.0; // Average percentage of threads busy over time
std::chrono::milliseconds session_duration{0};
};
class ThreadPool {
public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
private:
struct Task {
std::function<void()> function;
TimePoint deadline;
bool has_deadline;
Task(std::function<void()> f, TimePoint d, bool has_d)
: function(std::move(f)),
deadline(d),
has_deadline(has_d) {}
};
std::vector<std::thread> workers;
std::deque<Task> tasks; // Simple FIFO queue instead of priority queue
mutable std::mutex queue_mutex; // mutable for const methods like queue_size()
std::condition_variable condition;
std::atomic<bool> stop{false};
// Metrics tracking
mutable std::mutex metrics_mutex; // mutable for const methods like isSessionActive()
bool session_active = false;
TimePoint session_start;
std::atomic<size_t> tasks_enqueued{0};
std::atomic<size_t> tasks_succeeded{0};
std::atomic<size_t> tasks_deadline_exceeded{0};
std::atomic<size_t> tasks_cancelled{0};
std::atomic<size_t> active_threads{0};
// Thread load tracking
std::vector<std::pair<TimePoint, size_t>> thread_load_samples; // (timestamp, active_count)
public:
explicit ThreadPool(size_t num_threads = std::thread::hardware_concurrency()) {
for (size_t i = 0; i < num_threads; ++i) {
workers.emplace_back([this] {
while (true) {
Task task{nullptr, TimePoint{}, false};
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this] { return stop.load() || !tasks.empty(); });
if (stop.load() && tasks.empty()) { return; }
if (!tasks.empty()) {
task = std::move(tasks.front());
tasks.pop_front();
} else {
continue;
}
}
// Execute the task (deadline checking is now handled inside the task)
if (task.function) {
// Track thread activity
active_threads++;
recordThreadLoadSample();
task.function();
active_threads--;
recordThreadLoadSample();
}
}
});
}
}
private:
// Helper to record thread load samples
void recordThreadLoadSample() {
if (session_active) {
std::lock_guard<std::mutex> lock(metrics_mutex);
thread_load_samples.emplace_back(Clock::now(), active_threads.load());
}
}
public:
// Enqueue a task without deadline
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<TaskResult<std::invoke_result_t<F, Args...>>> {
using return_type = std::invoke_result_t<F, Args...>;
using result_type = TaskResult<return_type>;
auto actualTask = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
auto task = std::make_shared<std::packaged_task<result_type()>>(
[this, actualTask = std::move(actualTask)]() mutable -> result_type {
result_type res = result_type(actualTask());
// Track completion status
if (session_active) {
switch (res.status) {
case TaskStatus::SUCCESS: tasks_succeeded++; break;
case TaskStatus::DEADLINE_EXCEEDED: tasks_deadline_exceeded++; break;
case TaskStatus::CANCELLED: tasks_cancelled++; break;
}
}
return res;
});
std::future<result_type> result = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if (stop.load()) { throw std::runtime_error("enqueue on stopped ThreadPool"); }
tasks.emplace_back([task]() { (*task)(); }, TimePoint{}, false);
if (session_active) { tasks_enqueued++; }
}
condition.notify_one();
return result;
}
// Enqueue a task with deadline
template<class F>
auto enqueue_with_deadline(F&& f, TimePoint deadline)
-> std::future<TaskResult<std::invoke_result_t<F>>> {
using return_type = std::invoke_result_t<F>;
using result_type = TaskResult<return_type>;
auto actualTask = std::forward<F>(f);
auto task = std::make_shared<std::packaged_task<result_type()>>(
[this, actualTask = std::move(actualTask), deadline]() mutable -> result_type {
result_type res;
if (Clock::now() > deadline) {
res = result_type(return_type{}, TaskStatus::DEADLINE_EXCEEDED);
} else {
res = result_type(actualTask());
}
// Track completion status
if (session_active) {
switch (res.status) {
case TaskStatus::SUCCESS: tasks_succeeded++; break;
case TaskStatus::DEADLINE_EXCEEDED: tasks_deadline_exceeded++; break;
case TaskStatus::CANCELLED: tasks_cancelled++; break;
}
}
return res;
});
std::future<result_type> result = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if (stop.load()) { throw std::runtime_error("enqueue on stopped ThreadPool"); }
tasks.emplace_back([task]() { (*task)(); }, deadline, true);
if (session_active) { tasks_enqueued++; }
}
condition.notify_one();
return result;
}
// Get current queue size (approximate, for monitoring)
size_t queue_size() const {
std::unique_lock<std::mutex> lock(queue_mutex);
return tasks.size();
}
// Get detailed queue information for debugging
void debug_queue_state() const {
std::unique_lock<std::mutex> lock(queue_mutex);
printf("ThreadPool: Queue size: %zu\n", tasks.size());
if (!tasks.empty()) {
int with_deadline = 0;
int without_deadline = 0;
for (const auto& task : tasks) {
if (task.has_deadline) {
with_deadline++;
} else {
without_deadline++;
}
}
printf("ThreadPool: Tasks with deadline: %d, without deadline: %d\n",
with_deadline,
without_deadline);
}
}
// Start a new metrics session
void beginSession() {
std::lock_guard<std::mutex> lock(metrics_mutex);
session_active = true;
session_start = Clock::now();
// Reset all metrics
tasks_enqueued = 0;
tasks_succeeded = 0;
tasks_deadline_exceeded = 0;
tasks_cancelled = 0;
thread_load_samples.clear();
// Record initial thread load
thread_load_samples.emplace_back(session_start, active_threads.load());
}
// End the current session and return metrics
ThreadPoolMetrics endSession() {
std::lock_guard<std::mutex> lock(metrics_mutex);
if (!session_active) {
return ThreadPoolMetrics{}; // Return empty metrics if no session active
}
auto session_end = Clock::now();
session_active = false;
// Record final thread load
thread_load_samples.emplace_back(session_end, active_threads.load());
// Calculate metrics
ThreadPoolMetrics metrics;
metrics.tasks_enqueued = tasks_enqueued.load();
metrics.tasks_succeeded = tasks_succeeded.load();
metrics.tasks_deadline_exceeded = tasks_deadline_exceeded.load();
metrics.tasks_cancelled = tasks_cancelled.load();
metrics.session_duration =
std::chrono::duration_cast<std::chrono::milliseconds>(session_end - session_start);
// Calculate average thread load
if (thread_load_samples.size() >= 2 && workers.size() > 0) {
double total_load_time = 0.0;
auto total_duration =
std::chrono::duration<double>(
thread_load_samples.back().first - thread_load_samples.front().first)
.count();
for (size_t i = 1; i < thread_load_samples.size(); ++i) {
auto duration =
std::chrono::duration<double>(
thread_load_samples[i].first - thread_load_samples[i - 1].first)
.count();
auto load = static_cast<double>(thread_load_samples[i - 1].second) / workers.size();
total_load_time += load * duration;
}
metrics.average_thread_load =
(total_duration > 0) ? (total_load_time / total_duration) : 0.0;
} else {
metrics.average_thread_load = 0.0;
}
return metrics;
}
// Check if a session is currently active
bool isSessionActive() const {
std::lock_guard<std::mutex> lock(metrics_mutex);
return session_active;
}
~ThreadPool() {
stop.store(true);
condition.notify_all();
for (std::thread& worker : workers) {
if (worker.joinable()) { worker.join(); }
}
}
};
} // namespace eagle0::common
#endif // EAGLE0_THREADPOOL_HPP
@@ -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,9 +17,9 @@ 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_armament(battalion.armament());
shardokBattalion.mutate_training(battalion.training());
shardokBattalion.mutate_morale(kDefaultMorale);
shardokBattalion.mutate_armament(static_cast<float>(battalion.armament()));
shardokBattalion.mutate_training(static_cast<float>(battalion.training()));
return shardokBattalion;
}
@@ -37,28 +39,28 @@ auto ConvertHero(const net::eagle0::common::CommonHero &hero) -> Hero {
shardokHero.mutable_control_info().mutate_controlled_unit_id(-1);
shardokHero.mutable_control_info().mutate_controlled_this_round(false);
shardokHero.mutate_strength(hero.strength());
shardokHero.mutate_strength_xp(hero.strength_xp());
shardokHero.mutate_strength(static_cast<int8_t>(hero.strength()));
shardokHero.mutate_strength_xp(static_cast<int16_t>(hero.strength_xp()));
shardokHero.mutate_agility(hero.agility());
shardokHero.mutate_agility_xp(hero.agility_xp());
shardokHero.mutate_agility(static_cast<int8_t>(hero.agility()));
shardokHero.mutate_agility_xp(static_cast<int16_t>(hero.agility_xp()));
shardokHero.mutate_constitution(hero.constitution());
shardokHero.mutate_constitution_xp(hero.constitution_xp());
shardokHero.mutate_constitution(static_cast<int8_t>(hero.constitution()));
shardokHero.mutate_constitution_xp(static_cast<int16_t>(hero.constitution_xp()));
shardokHero.mutate_charisma(hero.charisma());
shardokHero.mutate_charisma_xp(hero.charisma_xp());
shardokHero.mutate_charisma(static_cast<int8_t>(hero.charisma()));
shardokHero.mutate_charisma_xp(static_cast<int16_t>(hero.charisma_xp()));
shardokHero.mutate_wisdom(hero.wisdom());
shardokHero.mutate_wisdom_xp(hero.wisdom_xp());
shardokHero.mutate_wisdom(static_cast<int8_t>(hero.wisdom()));
shardokHero.mutate_wisdom_xp(static_cast<int16_t>(hero.wisdom_xp()));
shardokHero.mutate_integrity(hero.integrity());
shardokHero.mutate_ambition(hero.ambition());
shardokHero.mutate_gregariousness(hero.gregariousness());
shardokHero.mutate_bravery(hero.bravery());
shardokHero.mutate_integrity(static_cast<int8_t>(hero.integrity()));
shardokHero.mutate_ambition(static_cast<int8_t>(hero.ambition()));
shardokHero.mutate_gregariousness(static_cast<int8_t>(hero.gregariousness()));
shardokHero.mutate_bravery(static_cast<int8_t>(hero.bravery()));
shardokHero.mutate_vigor(hero.vigor());
shardokHero.mutate_starting_vigor(hero.vigor());
shardokHero.mutate_vigor(static_cast<float>(hero.vigor()));
shardokHero.mutate_starting_vigor(static_cast<float>(hero.vigor()));
return shardokHero;
}
@@ -70,7 +72,14 @@ auto ConvertUnit(
Unit shardokUnit{};
shardokUnit.mutate_player_id(shardokPlayerId);
shardokUnit.mutate_eagle_player_id(unit.eagle_player_id());
// Range check eagle_player_id for int8 conversion
int32_t eagle_id = unit.eagle_player_id();
if (eagle_id < -128 || eagle_id > 127) {
throw std::runtime_error(
"eagle_player_id " + std::to_string(eagle_id) + " out of int8 range");
}
shardokUnit.mutate_eagle_player_id(static_cast<int8_t>(eagle_id));
shardokUnit.mutate_hidden(false);
shardokUnit.mutate_fortified(false);
if (unit.has_hero()) {
@@ -86,19 +95,22 @@ auto ConvertUnit(
shardokUnit.mutate_stun_rounds_remaining(0);
for (const PlayerId pid : allPlayerIds) {
shardokUnit.mutable_opponent_knowledge()->Mutate(pid, 0);
shardokUnit.mutable_opponent_knowledge()->Mutate(
static_cast<flatbuffers::uoffset_t>(pid),
0);
}
shardokUnit.mutate_has_moved_in_zoc(false);
shardokUnit.mutate_targeted_unit(-1);
shardokUnit.mutate_volleys_remaining(0);
shardokUnit.mutate_food_remaining(unit.food());
shardokUnit.mutate_food_remaining(static_cast<float>(unit.food()));
shardokUnit.mutate_can_flee(unit.can_flee());
shardokUnit.mutate_can_archery(unit.can_archery());
shardokUnit.mutate_can_start_fire(unit.can_start_fire());
if (unit.has_starting_position_index()) {
shardokUnit.mutate_starting_position_index(unit.starting_position_index().value());
shardokUnit.mutate_starting_position_index(
static_cast<int8_t>(unit.starting_position_index().value()));
} else {
shardokUnit.mutate_starting_position_index(-1);
}
@@ -9,7 +9,10 @@
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
#include "src/main/protobuf/net/eagle0/common/common_unit.pb.h"
#pragma GCC diagnostic pop
namespace shardok {
+1 -2
View File
@@ -51,8 +51,7 @@ cc_binary(
deps = [
"//src/main/cpp/net/eagle0/common:byte_vector",
"//src/main/cpp/net/eagle0/common:filesystem_utils",
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
],
)
@@ -3,13 +3,10 @@
//
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/protobuf/net/eagle0/common/shardok_internal_interface.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/storage/game.pb.h"
using GameStateW = shardok::Wrapper<net::eagle0::shardok::storage::fb::GameState>;
auto main(int argc, char** argv) -> int {
char* path = argv[1];
@@ -27,8 +24,8 @@ auto main(int argc, char** argv) -> int {
printf("There are %d results\n", arCount);
for (int arIndex = 0; arIndex < arCount; arIndex++) {
GameStateW gameState =
GameStateW::FromByteString(game.action_result(arIndex).state_after_fb());
shardok::GameStateW gameState =
shardok::GameStateW::FromByteString(game.action_result(arIndex).state_after_fb());
const auto* hexMap = gameState->hex_map();
for (int terrainIndex = 0; terrainIndex < hexMap->terrain()->size(); terrainIndex++) {
@@ -36,7 +36,7 @@ auto CalculateMap(
.name = mapName,
.positionsRequiringCrossing = {}};
for (int i = 0; i < hexMap->attacker_starting_positions()->size(); i++) {
for (unsigned int i = 0; i < hexMap->attacker_starting_positions()->size(); i++) {
const auto* positionList = hexMap->attacker_starting_positions()->Get(i);
if (positionList->positions()->size() < 1) continue;
if (positionList->positions()->size() != 10) {
@@ -5,7 +5,9 @@
#ifndef EAGLE0_MAPINFOCALCULATOR_HPP
#define EAGLE0_MAPINFOCALCULATOR_HPP
#include <cstdint>
#include <map>
#include <memory>
#include <string>
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
@@ -3,6 +3,7 @@
//
#include <iostream>
#include <memory>
#include "MapInfoCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -52,7 +53,7 @@ auto main(const int argc, char** argv) -> int {
outputStream << " \"positions\": {";
bool firstPosition = true;
for (const auto& kv : mapInfo.positionsRequiringCrossing) {
for (const auto& [position, count] : mapInfo.positionsRequiringCrossing) {
if (firstPosition) {
outputStream << endl;
firstPosition = false;
@@ -60,7 +61,7 @@ auto main(const int argc, char** argv) -> int {
outputStream << "," << endl;
}
outputStream << " \"" << kv.first << "\": " << kv.second;
outputStream << " \"" << position << "\": " << count;
}
outputStream << endl << " }" << endl << " }";
}
@@ -4,6 +4,8 @@
#include "AIAttackGroups.hpp"
#include <iterator>
#include <ranges>
#include <unordered_map>
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -221,11 +223,15 @@ auto GenerateTargetPriorities(
Power(unit);
}
tpl.priorityOrder = common::Map(targetsWithDistance, [](const TargetAndDistance& tad) {
return TargetAndAttackLocations{
.target = tad.target,
.attackLocations = tad.attackLocations};
});
tpl.priorityOrder.reserve(targetsWithDistance.size());
std::ranges::transform(
targetsWithDistance,
std::back_inserter(tpl.priorityOrder),
[](const TargetAndDistance& tad) {
return TargetAndAttackLocations{
.target = tad.target,
.attackLocations = tad.attackLocations};
});
}
return allTargetsUnitsAndDistances;
@@ -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,21 +12,21 @@ 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;
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,
const SettingsGetter& settings,
const AIWaterCrossingCommandChooser& waterCrossingCommandChooser,
const vector<CommandProto>& availableCommands) -> AIStrategy {
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 +41,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 +49,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 +58,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,
@@ -5,7 +5,6 @@
#include "AICommandFilter.hpp"
#include <algorithm>
#include <cmath>
#include "src/main/cpp/net/eagle0/shardok/library/BattalionType.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexCubeUtils.hpp"
@@ -14,15 +13,15 @@
namespace shardok {
using fb::Unit;
using net::eagle0::shardok::common::CommandType;
using net::eagle0::shardok::storage::fb::Unit;
CoordsSet AICommandFilter::BuildEnemyLocations(const GameState* gameState, PlayerId pid) {
CoordsSet AICommandFilter::BuildEnemyLocations(const GameStateW& gameState, PlayerId pid) {
CoordsSet enemyLocations(gameState->hex_map());
const auto* units = gameState->units();
for (int i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(i);
for (size_t i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(static_cast<unsigned int>(i));
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->player_id() != pid && !unit->hidden() && unit->location().column() != -1) {
enemyLocations.Add(unit->location());
@@ -36,7 +35,7 @@ std::vector<size_t> AICommandFilter::FilterCommands(
const CommandListSPtr& commands,
PlayerId pid,
bool isDefender,
const GameState* gameState,
const GameStateW& gameState,
const SettingsGetter& settings,
const APDCache& apdCache) {
std::vector<size_t> filteredIndices;
@@ -104,16 +103,14 @@ bool AICommandFilter::IsWastefulAction(
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameState* gameState,
const GameStateW& gameState,
const SettingsGetter& settings,
const APDCache& apdCache,
const CoordsSet& enemyLocations,
const CoordsSet& castleLocations,
double minDistToEnemies) {
const auto cmdType = cmd.GetCommandType();
// Handle different spell types
switch (cmdType) {
switch (cmd.GetCommandType()) {
case CommandType::METEOR_START_COMMAND: {
// Meteor preparation filtering
// Meteor takes 3 rounds (start -> target -> cast) and locks the mage in place
@@ -212,8 +209,8 @@ bool AICommandFilter::IsWastefulAction(
bool nearObjective = false;
for (const auto& enemyCoords : enemyLocations) {
const Cube enemyCube = OffsetToCube(enemyCoords);
const int hexDistance = CubeDistance(unitCube, enemyCube);
if (hexDistance <= 3) {
if (const int hexDistance = CubeDistance(unitCube, enemyCube);
hexDistance <= 3) {
nearObjective = true;
break;
}
@@ -392,9 +389,8 @@ bool AICommandFilter::IsWastefulAction(
static_cast<int8_t>(targetCoords.column())};
// Check if any enemy occupies the fire location - let them burn!
const auto* units = gameState->units();
std::vector<PlayerId> allyPids; // Empty for now - assume 2-player game
if (KnownEnemyOccupant(pid, units, allyPids, fireLocation)) {
if (gameState.GetKnownEnemyOccupant(pid, allyPids, fireLocation)) {
return true; // Don't extinguish fires under enemies
}
break;
@@ -410,7 +406,7 @@ bool AICommandFilter::IsWastefulMovement(
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameState* gameState,
const GameStateW& gameState,
const SettingsGetter& settings,
const APDCache& apdCache,
const CoordsSet& enemyLocations,
@@ -490,12 +486,12 @@ bool AICommandFilter::IsWastefulMovement(
}
bool AICommandFilter::IsStrategicBlunder(
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameState* gameState,
const SettingsGetter& settings,
double minDistToEnemies) {
const ShardokCommand& /*cmd*/,
PlayerId /*pid*/,
bool /*isDefender*/,
const GameStateW& /*gameState*/,
const SettingsGetter& /*settings*/,
double /*minDistToEnemies*/) {
// Simplified strategic blunder detection for now
// TODO: Implement proper castle abandonment detection
// TODO: Use minDistToEnemies for strategic blunder logic
@@ -503,15 +499,15 @@ bool AICommandFilter::IsStrategicBlunder(
}
double AICommandFilter::MinDistanceToEnemyUnits(
const GameState* gameState,
const GameStateW& gameState,
PlayerId pid,
const CoordsSet& enemyLocations) {
// Calculate minimum distance from any player unit to any enemy unit
double minDistance = std::numeric_limits<double>::max();
const auto* units = gameState->units();
for (int i = 0; i < units->size(); ++i) {
const auto* playerUnit = units->Get(i);
for (size_t i = 0; i < units->size(); ++i) {
const auto* playerUnit = units->Get(static_cast<unsigned int>(i));
if (playerUnit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
playerUnit->player_id() == pid) {
const auto& playerCoords = playerUnit->location();
@@ -529,7 +525,7 @@ double AICommandFilter::MinDistanceToEnemyUnits(
}
double AICommandFilter::MinDistanceToCastles(
const GameState* gameState,
const GameStateW& gameState,
PlayerId pid,
const CoordsSet& castleLocations) {
// Calculate minimum distance from any player unit to any castle
@@ -541,8 +537,8 @@ double AICommandFilter::MinDistanceToCastles(
}
// Find minimum hex distance from any player unit to any castle
for (int i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(i);
for (size_t i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(static_cast<unsigned int>(i));
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->player_id() == pid) {
const auto& unitCoords = unit->location();
@@ -560,7 +556,7 @@ double AICommandFilter::MinDistanceToCastles(
}
bool AICommandFilter::IsPlayerOutnumbered(
const GameState* gameState,
const GameStateW& gameState,
PlayerId pid,
double threshold) {
const int playerUnitCount = CountPlayerUnits(gameState, pid);
@@ -572,12 +568,12 @@ bool AICommandFilter::IsPlayerOutnumbered(
return ratio < threshold;
}
int AICommandFilter::CountPlayerUnits(const GameState* gameState, PlayerId pid) {
int AICommandFilter::CountPlayerUnits(const GameStateW& gameState, PlayerId pid) {
int count = 0;
const auto* units = gameState->units();
for (int i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(i);
for (size_t i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(static_cast<unsigned int>(i));
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->player_id() == pid) {
count++;
@@ -588,9 +584,9 @@ int AICommandFilter::CountPlayerUnits(const GameState* gameState, PlayerId pid)
}
bool AICommandFilter::WouldAbandonCriticalCastle(
const ShardokCommand& cmd,
PlayerId pid,
const GameState* gameState) {
const ShardokCommand& /*cmd*/,
PlayerId /*pid*/,
const GameStateW& /*gameState*/) {
// Simplified implementation - return false for now
// TODO: Implement proper castle abandonment detection when API is available
return false;
@@ -40,20 +40,20 @@ public:
const CommandListSPtr& commands,
PlayerId pid,
bool isDefender,
const GameState* gameState,
const GameStateW& gameState,
const SettingsGetter& settings,
const APDCache& apdCache);
private:
// Helper to build enemy locations once for efficiency
static CoordsSet BuildEnemyLocations(const GameState* gameState, PlayerId pid);
static CoordsSet BuildEnemyLocations(const GameStateW& gameState, PlayerId pid);
// Spell preparation filters
static bool IsWastefulAction(
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameState* gameState,
const GameStateW& gameState,
const SettingsGetter& settings,
const APDCache& apdCache,
const CoordsSet& enemyLocations,
@@ -65,7 +65,7 @@ private:
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameState* gameState,
const GameStateW& gameState,
const SettingsGetter& settings,
const APDCache& apdCache,
const CoordsSet& enemyLocations,
@@ -76,27 +76,29 @@ private:
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameState* gameState,
const GameStateW& gameState,
const SettingsGetter& settings,
double minDistToEnemies);
// Helper functions for distance and position analysis
static double MinDistanceToEnemyUnits(
const GameState* gameState,
const GameStateW& gameState,
PlayerId pid,
const CoordsSet& enemyLocations);
static double MinDistanceToCastles(
const GameState* gameState,
const GameStateW& gameState,
PlayerId pid,
const CoordsSet& castleLocations);
static bool IsPlayerOutnumbered(const GameState* gameState, PlayerId pid, double threshold);
static bool IsPlayerOutnumbered(const GameStateW& gameState, PlayerId pid, double threshold);
static int CountPlayerUnits(const GameState* gameState, PlayerId pid);
static int CountPlayerUnits(const GameStateW& gameState, PlayerId pid);
static bool
WouldAbandonCriticalCastle(const ShardokCommand& cmd, PlayerId pid, const GameState* gameState);
static bool WouldAbandonCriticalCastle(
const ShardokCommand& cmd,
PlayerId pid,
const GameStateW& gameState);
};
} // namespace shardok
@@ -4,6 +4,9 @@
#include "AIDefenderStrategySelector.hpp"
#include <algorithm>
#include <ranges>
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
@@ -14,7 +17,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 {
@@ -57,7 +60,9 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
++attackerNonUndeadUnitCount;
if (!common::Contains(attackerUnitIdsRequiringWaterCrossing, unit->unit_id())) {
if (!std::ranges::contains(
attackerUnitIdsRequiringWaterCrossing,
unit->unit_id())) {
++attackerNonUndeadUnitNotRequiringWaterCrossingCount;
}
}
@@ -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 */
File diff suppressed because it is too large Load Diff
@@ -5,8 +5,11 @@
#ifndef EAGLE0_AISCORECALCULATOR_HPP
#define EAGLE0_AISCORECALCULATOR_HPP
#include <chrono>
#include <future>
#include "src/main/cpp/net/eagle0/common/TaskResult.hpp"
#include "src/main/cpp/net/eagle0/common/ThreadPool.hpp"
#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/ShardokCTypes.h"
@@ -30,125 +33,23 @@ using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
class AIScoreCalculator {
public:
struct IndexAndScore {
size_t index;
CommandType type;
ScoreValue lookaheadScore;
ScoreValue immediateScore;
};
// Start a new metrics collection session
static void BeginMetricsSession();
private:
[[nodiscard]] static auto DefenderScatterStrategyScoreForState(
const GameState *gameState,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto DefenderHoldCastlesStrategyScoreForState(
const GameState *gameState,
const CoordsSet &castleCoords,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto FleeStrategyScoreForState(
const GameState *gameState,
PlayerId playerId) -> ScoreValue;
[[nodiscard]] static auto DefenderScoreForState(
const GameState *gameState,
const AIStrategy &defenderStrategy,
const CoordsSet &castleCoords,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto AttackerScoreForState(
const GameState *gameState,
const AIStrategy &attackerStrategy,
const CoordsSet &castleCoords,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
struct ImmediateAndLookaheadScore {
ScoreValue immediateScore;
future<ScoreValue> lookaheadScore;
};
static auto BasicLookaheadCalculator(
PlayerId pid,
bool isDefender,
int remainingLookahead,
int maxRepeatCount,
const shared_ptr<ShardokEngine> &innerEngine,
ScoreValue currentUtility,
const AIStrategy &attackerStrategy,
const SettingsGetter &settingsGetter,
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache) -> ScoreValue;
static auto CalcOne(
PlayerId pid,
bool isDefender,
uint32_t commandIndex,
int remainingLookahead,
int maxRepeatCount,
const std::shared_ptr<RandomGenerator> &randomGenerator,
const ShardokEngine &guessedEngine,
const AIStrategy &attackerStrategy,
const SettingsGetter &settingsGetter,
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache) -> ImmediateAndLookaheadScore;
struct CommandEvaluationResult {
ScoreValue immediateScore;
ScoreValue lookaheadScore;
};
static auto EvaluateCommand(
PlayerId pid,
bool isDefender,
uint32_t commandIndex,
int remainingLookahead,
int maxRepeatCount,
const ShardokEngine &guessedEngine,
const AIStrategy &attackerStrategy,
ScoreValue currentUtility,
const SettingsGetter &settingsGetter,
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache) -> CommandEvaluationResult;
public:
// End the current session and return metrics
static eagle0::common::ThreadPoolMetrics EndMetricsSession();
// Evaluate the score of a guessed game state based on the current AI strategy. DOES NOT perform
// or evaluate any commands.
[[nodiscard]] static auto GuessedStateScore(
bool isDefender,
const GameState *state,
const GameStateW &state,
const AIStrategy &aiStrategy,
const CoordsSet &allCastleCoords,
const SettingsGetter &settingsGetter,
const APDCache &apdCache,
const ALCache &alCache) -> ScoreValue;
[[nodiscard]] static auto BestCommandIndex(
PlayerId pid,
bool isDefender,
int remainingLookahead,
int maxRepeatCount,
const ShardokEngine &guessedEngine,
const AIStrategy &attackerStrategy,
ScoreValue currentUtility,
const SettingsGetter &settingsGetter,
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache) -> IndexAndScore;
// Evaluates the score for a particular command index for the given player, using lookahead.
[[nodiscard]] static auto CommandScore(
PlayerId pid,
bool isDefender,
@@ -161,7 +62,9 @@ public:
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache,
size_t commandIndex) -> ScoreValue;
size_t commandIndex,
std::chrono::steady_clock::time_point deadline)
-> std::future<eagle0::common::TaskResult<ScoreValue>>;
};
} // namespace shardok
@@ -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
@@ -32,8 +32,8 @@ auto CalculateTimeBudget(
bool isClose = false;
const auto *units = state->units();
for (int i = 0; i < units->size() && !isClose; ++i) {
const auto *myUnit = units->Get(i);
for (size_t i = 0; i < units->size() && !isClose; ++i) {
const auto *myUnit = units->Get(static_cast<unsigned int>(i));
if (myUnit->player_id() != playerId) continue;
const auto &myCoords = myUnit->location();
@@ -43,8 +43,8 @@ auto CalculateTimeBudget(
const Cube myCube = OffsetToCube(myCoords);
// Check distance to enemy units
for (int j = 0; j < units->size(); ++j) {
const auto *enemyUnit = units->Get(j);
for (size_t j = 0; j < units->size(); ++j) {
const auto *enemyUnit = units->Get(static_cast<unsigned int>(j));
if (enemyUnit->player_id() == playerId) continue;
const auto &enemyCoords = enemyUnit->location();
@@ -80,7 +80,7 @@ auto CalculateTimeBudget(
const auto remainingBudget = std::chrono::duration_cast<std::chrono::milliseconds>(budget);
// Get minimum depth requirement
const int minDepth = settingsGetter.Backing().min_lookahead_turns();
const size_t minDepth = settingsGetter.Backing().min_lookahead_turns();
return AITimeBudget{
.remainingBudget = remainingBudget,
@@ -9,15 +9,13 @@
#include <chrono>
#include <memory>
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.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"
namespace shardok {
// Forward declarations
class GameSettings;
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
using GameSettingsSPtr = std::shared_ptr<GameSettings>;
// RAII counter for tracking concurrent AI command evaluations
@@ -33,7 +31,7 @@ public:
// Configuration structure for iterative deepening time budget
struct AITimeBudget {
std::chrono::milliseconds remainingBudget; // Time budget remaining (decremented as used)
int minDepthRequired; // Minimum depth from minLookaheadTurns
size_t minDepthRequired; // Minimum depth from minLookaheadTurns
bool isCloseToEnemy; // Proximity flag for budget selection
};
@@ -98,7 +98,7 @@ auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue {
return battalionValue + heroValue;
}
auto archeryValue(const Unit *unit) -> double {
auto archeryValue(const Unit * /*unit*/) -> double {
// TODO: make this depend on the value of the targets
return kArcheryPossibleValue;
}
@@ -113,7 +113,7 @@ auto reduceValue(const Unit *unit, const Terrain *unitTerrain) -> double {
return 0.0;
}
auto fearValue(const Unit *unit) -> double {
auto fearValue(const Unit * /*unit*/) -> double {
// TODO: make this depend on the value of the targets
return kFearPossibleValue;
}
@@ -4,9 +4,11 @@
#include "AIVictoryConditionScoreCalculator.hpp"
#include <algorithm>
#include <ranges>
#include "AIAttackLocations.hpp"
#include "AIDistanceDebuf.hpp"
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/victory_condition.hpp"
@@ -122,12 +124,12 @@ auto AttackerDebufForDefenderOccupiedCriticalTile(
}
auto DefenderHoldsCriticalTilesVictoryScore(
const net::eagle0::shardok::storage::fb::GameState* gameState,
const GameStateW& gameState,
const CoordsSet& criticalTileLocations,
const PlayerInfo* player,
const APDCache& apdCache,
const ALCache& alCache,
const SettingsGetter& settings) -> ScoreValue {
const APDCache& /*apdCache*/,
const ALCache& /*alCache*/,
const SettingsGetter& /*settings*/) -> ScoreValue {
ScoreValue total = 0.0;
const auto rc = gameState->hex_map()->row_count();
@@ -152,7 +154,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,
@@ -246,12 +248,12 @@ auto AttackerHoldsCriticalTilesVictoryScore(
}
auto LastPlayerStandingVictoryScore(
const GameState* gameState,
const GameStateW& gameState,
const PlayerInfo* player,
const APDCache& apdCache,
const ALCache& alCache,
const SettingsGetter& settings) -> ScoreValue {
if (!common::Contains(
if (!std::ranges::contains(
*player->victory_conditions(),
net::eagle0::shardok::storage::fb::
VictoryCondition_VICTORY_CONDITION_LAST_PLAYER_STANDING)) {
@@ -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,9 +74,9 @@ auto UnitIdsRequiringWaterCrossing(
}
auto UnitIdsToCreateWaterCrossing(
const GameState *gameState,
const GameStateW &gameState,
const PlayerId pid,
const APDCache &apdCache,
const APDCache & /*apdCache*/,
const SettingsGetter &settings) -> vector<UnitId> {
vector<UnitId> unitIds{};
@@ -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,
@@ -4,8 +4,10 @@
#include "AIWaterCrossingCommandChooser.hpp"
#include <algorithm>
#include <ranges>
#include "AIMinimumDistanceAndTarget.hpp"
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
namespace shardok {
@@ -17,10 +19,10 @@ 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;
uint32_t castleClaimCount = 0;
for (const auto *unit : *gameState->units()) {
if (unit->player_id() != playerId) continue;
const auto status = unit->status();
@@ -83,7 +85,7 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
// a large penalty
for (const UnitId uid : unitIdsRequiringCrossing) {
// If this unit ID can also create a crossing, we already handled it
if (common::Contains(unitIdsCreatingCrossing, uid)) continue;
if (std::ranges::contains(unitIdsCreatingCrossing, uid)) continue;
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = settingsGetter.GetBattalionType(unit->battalion().type());
@@ -119,11 +121,11 @@ 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());
int castleClaimCount = 0;
uint32_t castleClaimCount = 0;
for (const auto *unit : *gameState->units()) {
if (unit->player_id() != playerId) continue;
const auto status = unit->status();
@@ -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;
};
+42 -1
View File
@@ -6,10 +6,12 @@ cc_library(
hdrs = ["AIAttackerStrategySelector.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
],
deps = [
":ai_attack_locations",
":ai_flee_decision_calculator",
":ai_score_utilities",
":ai_strategy",
":ai_water_crossing_command_chooser",
@@ -26,6 +28,7 @@ cc_library(
hdrs = ["AIAttackGroups.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
],
deps = [
@@ -60,6 +63,7 @@ cc_library(
hdrs = ["AIDefenderStrategySelector.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
],
deps = [
@@ -67,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",
@@ -80,6 +85,7 @@ cc_library(
hdrs = ["AIDistanceDebuf.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
],
deps = [
@@ -112,21 +118,43 @@ cc_library(
hdrs = ["AIScoreUtilities.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//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"],
hdrs = ["AICommandFilter.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
],
deps = [
@@ -146,6 +174,7 @@ cc_library(
hdrs = ["AIScoreCalculator.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
],
deps = [
@@ -154,6 +183,8 @@ cc_library(
":ai_unit_score_calculator",
":ai_victory_condition_score_calculator",
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
"//src/main/cpp/net/eagle0/common:task_result",
"//src/main/cpp/net/eagle0/common:thread_pool",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_guesser",
],
@@ -165,6 +196,7 @@ cc_library(
hdrs = ["AIStrategy.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
],
deps = [
@@ -178,6 +210,7 @@ cc_library(
hdrs = ["AIUnitScoreCalculator.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
],
deps = [
@@ -193,6 +226,7 @@ cc_library(
hdrs = ["AIVictoryConditionScoreCalculator.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
],
deps = [
@@ -200,6 +234,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",
@@ -217,6 +252,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",
@@ -234,6 +270,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",
@@ -246,10 +283,11 @@ cc_library(
hdrs = ["AITimeBudget.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
],
deps = [
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
"//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/util:hex_cube_utils",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
@@ -263,6 +301,7 @@ cc_library(
hdrs = ["IterativeDeepeningAI.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
],
deps = [
@@ -271,6 +310,7 @@ cc_library(
":ai_score_calculator",
":ai_time_budget",
":ai_water_crossing_command_chooser",
"//src/main/cpp/net/eagle0/common:task_result",
"//src/main/cpp/net/eagle0/common:time_utils",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
@@ -287,6 +327,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",
@@ -5,12 +5,14 @@
#include "IterativeDeepeningAI.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
#include <utility>
#include "AIAttackerStrategySelector.hpp"
#include "AIScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/common/TaskResult.hpp"
#include "src/main/cpp/net/eagle0/common/TimeUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
@@ -43,11 +45,26 @@ auto IterativeDeepeningAI::IterativeSearch(
const auto initialBudgetMs = initialBudget.remainingBudget;
SearchResult result;
// Start ThreadPool metrics session
AIScoreCalculator::BeginMetricsSession();
if (commands.empty()) {
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
printf("ID AI: Commands are empty, returning early\n");
#endif
result.searchCompleted = true;
// End session and print metrics (only if session was long enough to be interesting)
auto metrics = AIScoreCalculator::EndMetricsSession();
if (metrics.session_duration.count() >= 100) {
printf("ThreadPool Metrics (empty commands):\n");
printf(" Tasks enqueued: %zu\n", metrics.tasks_enqueued);
printf(" Tasks succeeded: %zu\n", metrics.tasks_succeeded);
printf(" Tasks deadline exceeded: %zu\n", metrics.tasks_deadline_exceeded);
printf(" Average thread load: %.1f%%\n", metrics.average_thread_load * 100.0);
printf(" Session duration: %lldms\n", metrics.session_duration.count());
}
return result;
}
@@ -55,7 +72,7 @@ auto IterativeDeepeningAI::IterativeSearch(
bool isSetupPhase =
(state->status()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP);
int maxDepth = isSetupPhase ? 2 : std::numeric_limits<int>::max();
size_t maxDepth = isSetupPhase ? 2 : std::numeric_limits<int>::max();
// Calculate current utility and create engine once for all command evaluations
const auto& settingsGetter = settings->GetGetter();
@@ -76,29 +93,40 @@ auto IterativeDeepeningAI::IterativeSearch(
highestDepthCompleted.clear();
highestDepthCompleted.resize(commands.size(), 0);
int currentDepth = 1;
size_t currentDepth = 1;
size_t previousBestCommand = 0; // Track best command from previous depth
size_t evaluatedCountAtHighestDepth = 0;
auto completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
// Main iterative deepening loop
while ((currentDepth == 1 || !IsTimeExpired(timeBudget)) && currentDepth <= maxDepth) {
// Track depth timing
auto depthStartTime = std::chrono::steady_clock::now();
auto elapsedSinceStart =
std::chrono::duration_cast<std::chrono::milliseconds>(depthStartTime - startTime);
printf("ID AI: Starting depth %zu at %lldms\n", currentDepth, elapsedSinceStart.count());
// Get command indices sorted by best score from previous depth
std::vector<size_t> sortedIndices = GetCommandsSortedByPreviousDepth(
currentDepth,
scoresByDepth,
highestDepthCompleted);
int evaluatedCount = 0;
size_t evaluatedCount = 0;
bool allEvaluated = true;
bool allEndTurnCommands = true; // Track if all commands are END_TURN
// Try to evaluate all commands at this depth, within budget constraints
// Start all command evaluations for this depth
std::vector<std::pair<size_t, std::future<SearchResult>>> futures;
futures.reserve(sortedIndices.size());
for (size_t cmdIndex : sortedIndices) {
if (currentDepth > 1 && IsTimeExpired(timeBudget)) {
allEvaluated = false;
break;
}
auto cmdResult = SearchCommandAtDepthWithEngine(
auto future = SearchCommandAtDepthWithEngine(
guessedEngine,
settingsGetter,
maxRepeatCount,
@@ -108,29 +136,47 @@ auto IterativeDeepeningAI::IterativeSearch(
currentUtility,
timeBudget);
// Ensure scoresByDepth[cmdIndex] has enough space
if (scoresByDepth[cmdIndex].size() <= currentDepth) {
scoresByDepth[cmdIndex].resize(currentDepth + 1);
}
scoresByDepth[cmdIndex][currentDepth] = cmdResult.bestScore;
highestDepthCompleted[cmdIndex] = currentDepth;
evaluatedCount++;
// Check if this command is not END_TURN_COMMAND
if (commands[cmdIndex].type() != net::eagle0::shardok::common::END_TURN_COMMAND) {
allEndTurnCommands = false;
}
futures.emplace_back(cmdIndex, std::move(future));
}
if (evaluatedCount < commands.size()) {
printf("ID AI: Depth %d - evaluated %d/%zu commands\n",
currentDepth,
evaluatedCount,
commands.size());
auto afterTaskSubmission = std::chrono::steady_clock::now();
auto submissionTime = std::chrono::duration_cast<std::chrono::milliseconds>(
afterTaskSubmission - depthStartTime);
printf("ID AI: Submitted %zu tasks for depth %zu (took %lldms)\n",
futures.size(),
currentDepth,
submissionTime.count());
// Now wait for all futures and collect results
printf("ID AI: Waiting for %zu futures at depth %zu\n", futures.size(), currentDepth);
auto waitStartTime = std::chrono::steady_clock::now();
for (auto& [cmdIndex, future] : futures) {
auto cmdResult = future.get();
// Only record results for successfully completed evaluations
if (cmdResult.searchCompleted) {
// Ensure scoresByDepth[cmdIndex] has enough space
if (scoresByDepth[cmdIndex].size() <= currentDepth) {
scoresByDepth[cmdIndex].resize(currentDepth + 1);
}
scoresByDepth[cmdIndex][currentDepth] = cmdResult.bestScore;
highestDepthCompleted[cmdIndex] = currentDepth;
evaluatedCount++;
// Check if this command is not END_TURN_COMMAND
if (commands[cmdIndex].type() != net::eagle0::shardok::common::END_TURN_COMMAND) {
allEndTurnCommands = false;
}
}
// If searchCompleted is false, we don't increment evaluatedCount or update
// highestDepthCompleted This means the iterative deepening logic will correctly handle
// incomplete evaluations
}
// Find the best command at current depth and check if it changed
if (evaluatedCount > 0) {
evaluatedCountAtHighestDepth = evaluatedCount;
size_t currentBestCommand = 0;
ScoreValue currentBestScore = -std::numeric_limits<ScoreValue>::infinity();
@@ -146,13 +192,13 @@ auto IterativeDeepeningAI::IterativeSearch(
// Log if best command changed from previous depth
if (currentDepth > 1 && currentBestCommand != previousBestCommand) {
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
printf("ID AI: Best command changed at depth %d:\n", currentDepth);
printf(" Depth %d best: command %zu (score %.2f) - %s\n",
printf("ID AI: Best command changed at depth %lu:\n", currentDepth);
printf(" Depth %lu best: command %zu (score %.2f) - %s\n",
currentDepth - 1,
previousBestCommand,
scoresByDepth[previousBestCommand][currentDepth - 1],
commands[previousBestCommand].DebugString().c_str());
printf(" Depth %d best: command %zu (score %.2f) - %s\n",
printf(" Depth %lu best: command %zu (score %.2f) - %s\n",
currentDepth,
currentBestCommand,
currentBestScore,
@@ -163,22 +209,46 @@ auto IterativeDeepeningAI::IterativeSearch(
previousBestCommand = currentBestCommand;
}
// Log depth completion timing
auto depthEndTime = std::chrono::steady_clock::now();
auto depthDuration = std::chrono::duration_cast<std::chrono::milliseconds>(
depthEndTime - depthStartTime);
auto waitDuration =
std::chrono::duration_cast<std::chrono::milliseconds>(depthEndTime - waitStartTime);
auto totalElapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(depthEndTime - startTime);
printf("ID AI: Completed depth %zu at %lldms (depth took %lldms, wait took %lldms, "
"evaluated %zu/%zu)\n",
currentDepth,
totalElapsed.count(),
depthDuration.count(),
waitDuration.count(),
evaluatedCount,
sortedIndices.size());
// Only proceed to next depth if we completed all commands at current depth
if (!allEvaluated) { break; }
if (!allEvaluated) {
completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
break;
}
// Stop if all evaluated commands were END_TURN at the root - no point going deeper
if (allEndTurnCommands && evaluatedCount > 0) { break; }
if (allEndTurnCommands && evaluatedCount > 0) {
completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
break;
}
// Also check if scores haven't changed from previous depth
// This indicates we've hit END_TURN in the lookahead
if (currentDepth > 1 && evaluatedCount > 0) {
bool scoresUnchanged = true;
int unchangedCount = 0;
size_t unchangedCount = 0;
for (size_t i = 0; i < sortedIndices.size() && i < evaluatedCount; ++i) {
size_t cmdIndex = sortedIndices[i];
// This command was evaluated at both current and previous depth
if (scoresByDepth[cmdIndex].size() > currentDepth &&
if (size_t cmdIndex = sortedIndices[i];
scoresByDepth[cmdIndex].size() > currentDepth &&
scoresByDepth[cmdIndex].size() > currentDepth - 1) {
// Check if score changed between depth N-1 and depth N
if (std::abs(
@@ -193,86 +263,76 @@ auto IterativeDeepeningAI::IterativeSearch(
}
// If all evaluated commands had unchanged scores, we've hit END_TURN in lookahead
if (scoresUnchanged && unchangedCount == evaluatedCount) { break; }
if (scoresUnchanged && unchangedCount == evaluatedCount) {
completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
break;
}
}
// Check if we've used more than 50% of total budget
auto totalElapsed = std::chrono::steady_clock::now() - startTime;
auto totalElapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(totalElapsed);
double budgetUsedPercent = (double)totalElapsedMs.count() / initialBudgetMs.count();
auto totalElapsedCheck = std::chrono::steady_clock::now() - startTime;
auto totalElapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(totalElapsedCheck);
double budgetUsedPercent = static_cast<double>(totalElapsedMs.count()) /
static_cast<double>(initialBudgetMs.count());
if (budgetUsedPercent > 0.5) {
printf("ID AI: Stopping after depth %d - used %.1f%% of time budget\n",
printf("ID AI: Stopping after depth %lu - used %.1f%% of time budget\n",
currentDepth,
budgetUsedPercent * 100);
completionReason = EvaluationCompletionReason::NOT_ENOUGH_TIME_TO_CONTINUE;
break;
}
currentDepth++;
}
// If we completed the loop without any breaks, we successfully exhausted meaningful search
if (completionReason == EvaluationCompletionReason::RAN_OUT_OF_TIME &&
currentDepth > maxDepth) {
// We hit the depth limit rather than running out of time
completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
}
// Select best result from highest depth achieved for each command
result = SelectBestResult(scoresByDepth, highestDepthCompleted);
result.minimumDepthCompleted = result.depthAchieved >= timeBudget.minDepthRequired;
result.searchCompleted = result.minimumDepthCompleted;
result.timeUsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - startTime);
result.availableCommandCount = commands.size();
result.commandCountEvaluated = evaluatedCountAtHighestDepth;
result.completionReason = completionReason;
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
printf("ID AI: Search complete - achieved depth %d for best command %zu (score %.2f)\n",
result.depthAchieved,
result.bestCommandIndex,
result.bestScore);
#endif
return result;
}
auto IterativeDeepeningAI::SearchAtDepth(
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
const int depth) const -> SearchResult {
SearchResult result;
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
printf("SearchAtDepth: depth=%d, commands=%zu\n", depth, commands.size());
#endif
if (commands.empty()) {
result.searchCompleted = true;
return result;
// Validation: if completion reason is RAN_OUT_OF_COMMANDS, evaluation should be 100%
if (completionReason == EvaluationCompletionReason::RAN_OUT_OF_COMMANDS &&
result.commandCountEvaluated < result.availableCommandCount) {
printf("ERROR: Completion reason RAN_OUT_OF_COMMANDS but evaluation %lu/%zu < 100%%\n",
result.commandCountEvaluated,
result.availableCommandCount);
}
const auto& settingsGetter = settings->GetGetter();
const auto guessedEngine = ShardokEngine(settings, state);
const auto maxRepeatCount = settingsGetter.Backing().ai_utility_repeat_count();
const ScoreValue currentUtility = AIScoreCalculator::GuessedStateScore(
isDefender,
state,
strategy,
castleCoords,
settingsGetter,
apdCache,
alCache);
// Perform search at specified depth
const auto indexAndScore = AIScoreCalculator::BestCommandIndex(
playerId,
isDefender,
depth, // Use the specified depth for lookahead
maxRepeatCount,
guessedEngine,
strategy,
currentUtility,
settingsGetter,
castleCoords,
apdCache,
alCache);
result.bestCommandIndex = indexAndScore.index;
result.bestScore = indexAndScore.lookaheadScore;
result.searchCompleted = true;
// End session and print ThreadPool metrics (only for sessions >= 100ms)
auto metrics = AIScoreCalculator::EndMetricsSession();
if (metrics.session_duration.count() >= 100) {
printf("ThreadPool Metrics (depth %zu, %s):\n",
result.depthAchieved,
completionReason == EvaluationCompletionReason::RAN_OUT_OF_TIME ? "timeout"
: completionReason == EvaluationCompletionReason::RAN_OUT_OF_COMMANDS ? "complete"
: completionReason == EvaluationCompletionReason::NOT_ENOUGH_TIME_TO_CONTINUE
? "no_time"
: "unknown");
printf(" Tasks enqueued: %zu\n", metrics.tasks_enqueued);
printf(" Tasks succeeded: %zu\n", metrics.tasks_succeeded);
printf(" Tasks deadline exceeded: %zu\n", metrics.tasks_deadline_exceeded);
printf(" Tasks cancelled: %zu\n", metrics.tasks_cancelled);
printf(" Average thread load: %.1f%%\n", metrics.average_thread_load * 100.0);
printf(" Session duration: %lldms\n", metrics.session_duration.count());
printf(" Tasks per ms: %.2f\n",
metrics.session_duration.count() > 0 ? static_cast<double>(metrics.tasks_enqueued) /
metrics.session_duration.count()
: 0.0);
}
return result;
}
@@ -281,38 +341,6 @@ bool IterativeDeepeningAI::IsTimeExpired(const AITimeBudget& budget) {
return budget.remainingBudget <= std::chrono::milliseconds(0);
}
auto IterativeDeepeningAI::SearchAllCommandsAtDepth(
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
const int depth) const -> std::vector<SearchResult> {
// Use SearchAtDepth to get the best overall result
const auto bestResult = SearchAtDepth(settings, state, commands, depth);
std::vector<SearchResult> results;
results.reserve(commands.size());
for (size_t i = 0; i < commands.size(); ++i) {
SearchResult result;
result.bestCommandIndex = i;
result.depthAchieved = depth;
result.searchCompleted = true;
result.minimumDepthCompleted = true;
// For the best command, use the actual score
// For others, use a slightly lower score (this is a simplification for Phase 2)
if (i == bestResult.bestCommandIndex) {
result.bestScore = bestResult.bestScore;
} else {
result.bestScore = bestResult.bestScore * 0.95; // Slightly lower but reasonable
}
results.push_back(result);
}
return results;
}
auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
const ShardokEngine& guessedEngine,
const GameSettings::Getter& settingsGetter,
@@ -321,16 +349,20 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
const size_t commandIndex,
const int depth,
const ScoreValue currentUtility,
AITimeBudget& timeBudget) const -> SearchResult {
AITimeBudget& timeBudget) const -> std::future<SearchResult> {
SearchResult result;
result.bestCommandIndex = commandIndex;
result.depthAchieved = depth;
result.searchCompleted = true;
result.minimumDepthCompleted = true;
result.availableCommandCount = commands.size();
result.commandCountEvaluated = 1; // We're evaluating just this command
if (commandIndex >= commands.size()) {
result.bestScore = 0.0;
return result;
std::promise<SearchResult> p;
p.set_value(result);
return p.get_future();
}
try {
@@ -338,8 +370,11 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
AIEvaluationCounter counter;
const auto startTime = std::chrono::steady_clock::now();
// Use CommandScore to evaluate the specific command at the given depth
const auto commandScore = AIScoreCalculator::CommandScore(
// Calculate deadline from remaining time budget
const auto deadline = startTime + timeBudget.remainingBudget;
// Get the future from CommandScore - don't wait yet
auto commandScoreFuture = AIScoreCalculator::CommandScore(
playerId,
isDefender,
depth,
@@ -351,11 +386,15 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
castleCoords,
apdCache,
alCache,
commandIndex);
commandIndex,
deadline);
// Calculate time and adjust budget before waiting
// This is needed because we need to update timeBudget synchronously
const auto commandResult = commandScoreFuture.get();
// Calculate time used and adjust based on concurrent evaluations
const auto elapsed = std::chrono::steady_clock::now() - startTime;
const int concurrentCount = counter.GetCurrentCount();
const int concurrentCount = AIEvaluationCounter::GetCurrentCount();
const auto adjustedElapsed = elapsed / std::max(1, concurrentCount);
const auto adjustedElapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(adjustedElapsed);
@@ -363,7 +402,15 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
// Deduct adjusted time from remaining budget
timeBudget.remainingBudget -= adjustedElapsedMs;
result.bestScore = commandScore;
// Check if we got a valid result or if evaluation failed/timed out
if (!commandResult.succeeded()) {
// Command evaluation failed or timed out - mark as incomplete
result.bestScore = 0.0;
result.searchCompleted = false;
result.minimumDepthCompleted = false;
} else {
result.bestScore = commandResult.value;
}
} catch (const std::exception& e) {
// If evaluation fails, return a neutral score rather than crashing
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
@@ -372,13 +419,15 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
result.bestScore = 0.0;
}
return result;
std::promise<SearchResult> p;
p.set_value(result);
return p.get_future();
}
auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
int currentDepth,
const size_t currentDepth,
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<int>& highestDepthCompleted) const -> std::vector<size_t> {
const std::vector<size_t>& highestDepthCompleted) -> std::vector<size_t> {
std::vector<size_t> indices(scoresByDepth.size());
std::iota(indices.begin(), indices.end(), 0);
@@ -388,8 +437,8 @@ auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
}
// Sort by score at previous depth
int prevDepth = currentDepth - 1;
std::sort(indices.begin(), indices.end(), [&](size_t a, size_t b) {
const size_t prevDepth = currentDepth - 1;
std::ranges::sort(indices, [&](const size_t a, const size_t b) {
// Only consider commands that were evaluated at previous depth
if (highestDepthCompleted[a] >= prevDepth && highestDepthCompleted[b] >= prevDepth) {
return scoresByDepth[a][prevDepth] > scoresByDepth[b][prevDepth];
@@ -403,7 +452,7 @@ auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
auto IterativeDeepeningAI::SelectBestResult(
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<int>& highestDepthCompleted) const -> SearchResult {
const std::vector<size_t>& highestDepthCompleted) -> SearchResult {
SearchResult result;
result.bestScore = -std::numeric_limits<ScoreValue>::infinity();
result.searchCompleted = false;
@@ -411,9 +460,8 @@ auto IterativeDeepeningAI::SelectBestResult(
// Find the command with best score at its highest evaluated depth
for (size_t i = 0; i < scoresByDepth.size(); ++i) {
if (highestDepthCompleted[i] > 0) {
int depth = highestDepthCompleted[i];
ScoreValue score = scoresByDepth[i][depth];
if (score > result.bestScore) {
const size_t depth = highestDepthCompleted[i];
if (ScoreValue score = scoresByDepth[i][depth]; score > result.bestScore) {
result.bestScore = score;
result.bestCommandIndex = i;
result.depthAchieved = depth;
@@ -6,6 +6,7 @@
#define EAGLE0_ITERATIVEDEEPENINGAI_HPP
#include <chrono>
#include <future>
#include <vector>
#include "AIStrategy.hpp"
@@ -23,15 +24,25 @@ class ShardokEngine;
using ScoreValue = double;
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
/// Reason why AI evaluation completed at the achieved depth.
enum class EvaluationCompletionReason {
RAN_OUT_OF_COMMANDS, ///< All remaining commands were trivial (e.g., END_TURN)
RAN_OUT_OF_TIME, ///< Time budget was exhausted with meaningful commands remaining
NOT_ENOUGH_TIME_TO_CONTINUE ///< Insufficient time budget to start next depth iteration
};
class IterativeDeepeningAI {
public:
struct SearchResult {
size_t bestCommandIndex;
ScoreValue bestScore;
int depthAchieved;
size_t depthAchieved;
std::chrono::milliseconds timeUsed;
bool minimumDepthCompleted;
bool searchCompleted;
size_t availableCommandCount;
size_t commandCountEvaluated;
EvaluationCompletionReason completionReason;
SearchResult()
: bestCommandIndex(0),
@@ -39,7 +50,10 @@ public:
depthAchieved(0),
timeUsed(0),
minimumDepthCompleted(false),
searchCompleted(false) {}
searchCompleted(false),
availableCommandCount(0),
commandCountEvaluated(0),
completionReason(EvaluationCompletionReason::RAN_OUT_OF_TIME) {}
};
IterativeDeepeningAI(
@@ -54,7 +68,7 @@ public:
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
const AITimeBudget& timeBudget) const;
const AITimeBudget& initialBudget) const;
private:
PlayerId playerId;
@@ -66,24 +80,12 @@ private:
// Reusable vectors to reduce memory allocations
mutable std::vector<std::vector<ScoreValue>> scoresByDepth;
mutable std::vector<int> highestDepthCompleted;
mutable std::vector<size_t> highestDepthCompleted;
mutable std::vector<size_t> reusableSortedIndices;
[[nodiscard]] SearchResult SearchAtDepth(
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
int depth) const;
[[nodiscard]] static bool IsTimeExpired(const AITimeBudget& budget);
[[nodiscard]] std::vector<SearchResult> SearchAllCommandsAtDepth(
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
int depth) const;
[[nodiscard]] SearchResult SearchCommandAtDepthWithEngine(
[[nodiscard]] std::future<SearchResult> SearchCommandAtDepthWithEngine(
const ShardokEngine& guessedEngine,
const GameSettings::Getter& settingsGetter,
int maxRepeatCount,
@@ -93,14 +95,14 @@ private:
ScoreValue currentUtility,
AITimeBudget& timeBudget) const;
[[nodiscard]] std::vector<size_t> GetCommandsSortedByPreviousDepth(
int currentDepth,
[[nodiscard]] static std::vector<size_t> GetCommandsSortedByPreviousDepth(
size_t currentDepth,
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<int>& highestDepthCompleted) const;
const std::vector<size_t>& highestDepthCompleted);
[[nodiscard]] SearchResult SelectBestResult(
[[nodiscard]] static SearchResult SelectBestResult(
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<int>& highestDepthCompleted) const;
const std::vector<size_t>& highestDepthCompleted);
};
} // namespace shardok
@@ -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"
@@ -21,12 +25,12 @@
namespace shardok {
static constexpr bool kDebugTimings = true;
using net::eagle0::shardok::api::ActionResultView;
using net::eagle0::shardok::api::GameStateView;
void ApplyUpdate(GameStateView &currentView, const ActionResultView &update) {}
static constexpr bool kPerformanceLogging = true;
void ApplyUpdate(GameStateView & /*currentView*/, const ActionResultView & /*update*/) {}
auto RoundsRemaining(const GameSettingsSPtr &settings, const GameStateView &gsv) -> int {
const int maxRounds = settings->GetGetter().Backing().max_rounds();
@@ -42,7 +46,29 @@ ShardokAIClient::ShardokAIClient(
: playerId(playerId),
isDefender(isDefender),
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
waterCrossingCommandChooser(playerId, apdCache) {}
waterCrossingCommandChooser(playerId, apdCache) {
// Pre-generate the most common cache entries for better performance
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
// Pre-fetch for all battalion types, both with and without brave water
using BattalionTypeId = net::eagle0::shardok::storage::fb::BattalionTypeId;
for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
typeId <= BattalionTypeId::BattalionTypeId_MAX;
typeId++) {
const auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
const auto battalionType = settings.GetBattalionType(battalionTypeId);
// Pre-fetch without brave water (braveWaterActionPointCost = -1)
apdCache->GetRaw(hexMap, mapId, battalionType, false, -1);
// Pre-fetch with brave water (includeBravingWater = true, braveWaterActionPointCost = 0)
apdCache->GetRaw(hexMap, mapId, battalionType, true, 0);
}
// Consolidate all the pre-fetched entries into the persistent cache
apdCache->ConsolidateThreadLocalCache_Racy();
}
void CheckCommand(const CommandProto &realDescriptor, const CommandProto &guessedDescriptor) {
string diff;
@@ -63,7 +89,7 @@ void CheckCommand(const CommandProto &realDescriptor, const CommandProto &guesse
auto ShardokAIClient::StandardChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateW &guessedState,
const vector<CommandProto> &realAvailableCommands) const -> size_t {
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
const auto settingsGetter = settings->GetGetter();
const auto guessedEngine = ShardokEngine(settings, guessedState);
@@ -74,7 +100,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
const auto commandCount = guessedCommands.size();
assert(commandCount == realAvailableCommands.size());
for (int i = 0; i < commandCount; i++) {
for (size_t i = 0; i < commandCount; i++) {
CheckCommand(realAvailableCommands[i], guessedCommands[i]);
}
@@ -101,13 +127,34 @@ auto ShardokAIClient::StandardChooseCommandIndex(
auto search_result =
iterativeAI.IterativeSearch(settings, guessedState, realAvailableCommands, timeBudget);
return search_result.bestCommandIndex;
CommandChoiceResults result{};
result.chosenIndex = search_result.bestCommandIndex;
result.availableCommandCount = search_result.availableCommandCount;
result.depthAchieved = search_result.depthAchieved;
result.commandCountEvaluated = search_result.commandCountEvaluated;
result.completionReason = search_result.completionReason;
if constexpr (kPerformanceLogging) {
if (result.commandCountEvaluated < result.availableCommandCount) {
printf("ID AI: Depth %d - evaluated %lu/%zu commands\n",
result.depthAchieved,
result.commandCountEvaluated,
result.availableCommandCount);
}
printf("ID AI: Search complete - achieved depth %d for best command %zu\n",
result.depthAchieved,
result.chosenIndex);
fflush(stdout);
}
return result;
}
auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateW &guessedState,
const vector<CommandProto> &realAvailableCommands) const -> size_t {
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
if (const auto dismissCommand = std::ranges::find_if(
realAvailableCommands,
[](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
@@ -116,49 +163,83 @@ auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
dismissCommand == realAvailableCommands.end()) {
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
} else {
return static_cast<size_t>(std::distance(realAvailableCommands.begin(), dismissCommand));
CommandChoiceResults results{};
results.chosenIndex =
static_cast<size_t>(std::distance(realAvailableCommands.begin(), dismissCommand));
results.availableCommandCount = realAvailableCommands.size();
results.depthAchieved = 1; // Simple heuristic choice
results.commandCountEvaluated = 1; // Only evaluated one command type
results.completionReason =
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS; // Heuristic choice
return results;
}
}
auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateW &guessedState,
const vector<CommandProto> &realAvailableCommands) const -> size_t {
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 vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
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);
}
// 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 = fleeDecision.commandIndex;
results.availableCommandCount = realAvailableCommands.size();
results.depthAchieved = 1; // Heuristic choice
results.commandCountEvaluated = 1; // Only evaluated one command type
results.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
return results;
} else {
return static_cast<size_t>(std::distance(realAvailableCommands.begin(), fleeCommand));
// Fight instead of flee
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
}
}
auto ShardokAIClient::ChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateView &gsv,
const vector<CommandProto> &realAvailableCommands) const -> size_t {
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
static int typeChosenCount[net::eagle0::shardok::common::CommandType_MAX + 1];
static int totalChoices = 0;
size_t chosenIndex;
CommandChoiceResults results{};
const auto guessedState = GameStateGuesser::GuessedState(playerId, settings->GetGetter(), gsv);
if (const int roundsRemaining = RoundsRemaining(settings, gsv);
!isDefender && roundsRemaining <= 1) {
chosenIndex =
results =
FinalRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
} else if (!isDefender && roundsRemaining <= 3) {
chosenIndex =
results =
LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
} else {
chosenIndex = StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
results = StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
}
const auto chosenType = realAvailableCommands[chosenIndex].type();
const auto chosenType = realAvailableCommands[results.chosenIndex].type();
typeChosenCount[static_cast<int>(chosenType)]++;
totalChoices++;
@@ -179,12 +260,11 @@ auto ShardokAIClient::ChooseCommandIndex(
printf("\n\n");
}
return chosenIndex;
return results;
}
auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const -> size_t {
const auto startTimeMicros = CurrentTimeMicros();
auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const
-> CommandChoiceResults {
if (const auto &availableCommands = engine.GetAvailableCommandProtos(playerId, false);
availableCommands.empty()) {
printf("no commands for player %d\n", playerId);
@@ -194,15 +274,9 @@ auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const -> s
const auto &settings = engine.GetGameSettings();
const auto &gsv = engine.GetGameStateView(GetPlayerId());
const size_t chosenIndex = ChooseCommandIndex(settings, gsv, availableCommands);
const auto elapsedMicros = CurrentTimeMicros() - startTimeMicros;
if (kDebugTimings) {
std::cerr << "Milliseconds to choose command index: " << elapsedMicros / 1000
<< std::endl;
}
return chosenIndex;
const auto results = ChooseCommandIndex(settings, gsv, availableCommands);
apdCache->ConsolidateThreadLocalCache_Racy();
return results;
}
}
@@ -15,12 +15,22 @@
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AITimeBudget.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCommandChooser.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/IterativeDeepeningAI.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
namespace shardok {
using VictoryCondition = net::eagle0::shardok::storage::fb::VictoryCondition;
/// Results from AI command selection, including performance metrics.
struct CommandChoiceResults {
size_t chosenIndex; ///< Index of the chosen command in the available commands list
size_t availableCommandCount; ///< Total number of commands that were available to choose from
int depthAchieved; ///< Maximum search depth reached for the best command
size_t commandCountEvaluated; ///< Number of commands evaluated at the highest achieved depth
EvaluationCompletionReason completionReason; ///< Why evaluation stopped at this depth
};
//
// A ShardokGameClient representing an AI player.
//
@@ -37,19 +47,20 @@ private:
[[nodiscard]] auto StandardChooseCommandIndex(
const GameSettingsSPtr& settings,
const GameStateW& guessedState,
const vector<CommandProto>& realAvailableCommands) const -> size_t;
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
[[nodiscard]] auto LateRoundAttackerChooseCommandIndex(
const GameSettingsSPtr& settings,
const GameStateW& guessedState,
const vector<CommandProto>& realAvailableCommands) const -> size_t;
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
[[nodiscard]] auto FinalRoundAttackerChooseCommandIndex(
const GameSettingsSPtr& settings,
const GameStateW& guessedState,
const vector<CommandProto>& realAvailableCommands) const -> size_t;
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
[[nodiscard]] auto ChooseCommandIndex(
const GameSettingsSPtr& settings,
const net::eagle0::shardok::api::GameStateView& gsv,
const vector<CommandProto>& realAvailableCommands) const -> size_t;
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
public:
explicit ShardokAIClient(
@@ -61,7 +72,8 @@ public:
[[nodiscard]] auto GetPlayerId() const -> PlayerId { return playerId; }
[[nodiscard]] auto ChooseCommandIndex(const ShardokEngine& engine) const -> size_t;
[[nodiscard]] auto ChooseCommandIndex(const ShardokEngine& engine) const
-> CommandChoiceResults;
};
} // namespace shardok
@@ -0,0 +1,74 @@
# Shardok Performance Optimization Plan
## Current Status
PostActionUnchecked reduced from 45.4% to 39.3% of total runtime after shared_ptr optimizations.
## ✅ Completed Optimizations
### 1. APDCache Thread-Local Caching
- **Problem**: PreCachedAPDs constructor taking 18.5% of processing time
- **Solution**: Moved thread-local caching into APDCache API using existing FullCacheKey infrastructure
- **Implementation**: Hybrid API with both shared_ptr and raw pointer access, migrated 21+ call sites
- **Result**: Successfully eliminated shared_ptr overhead in AI calculations
### 2. SharedPtr Reference Counting Fix
- **Problem**: Atomic reference counting overhead in ShardokAction::Execute (28.4% of total runtime)
- **Solution**: Changed RandomGenerator parameter from `std::shared_ptr<RandomGenerator>` to `const std::shared_ptr<RandomGenerator>&`
- **Implementation**: Updated 49+ override sites across all command and action classes
- **Result**: Reduced PostActionUnchecked from 45.4% to 39.3% of runtime
## ❌ Failed Attempts
### 1. ToByteString() Caching
- **Problem**: Suspected expensive game state serialization calls
- **Solution**: Added hash-based caching to avoid repeated ToByteString() calls
- **Result**: No measurable performance improvement (discarded)
## 📋 Next Steps (Priority Order)
### 1. Optimize Occupant() with Array-based Indexing (HIGH PRIORITY)
- **Problem**: `Occupant()` function iterates through ALL units (O(n)) to find unit at specific coordinates
- **Solution**: Replace with O(1) array lookup indexed by `row * columnCount + column`
- **Implementation**:
- Simple array storing UnitId (or INVALID_UNIT_ID) at each map position
- Update index when units move/spawn/die
- Use in GameStateW wrapper with lazy initialization
- **Rationale**: Clear algorithmic improvement, frequently called function
- **Expected Impact**: Unknown but potentially significant
### 2. Profile Next Bottleneck (HIGH PRIORITY)
- **Goal**: After Occupant() optimization, re-profile to identify next hotspot
- **Focus**: PostActionUnchecked still 39.3% - drill deeper into remaining time consumption
- **Approach**: Look for unexpected bottlenecks like the shared_ptr reference counting we discovered
- **Rationale**: Profiling has revealed surprising performance issues
### 3. Defer UpdateGameStatusAction (MEDIUM PRIORITY)
- **Problem**: Victory conditions checked after every action
- **Solution**: Batch victory condition checks to end of turn or specific triggers
- **Expected Impact**: Reduce redundant computation overhead
### 4. Object Pooling (MEDIUM PRIORITY)
- **Problem**: Frequent allocation/deallocation of ActionResult and other objects
- **Solution**: Implement object pools for frequently created objects
- **Focus**: ActionResult objects, other high-frequency allocations
- **Expected Impact**: Reduce memory allocation overhead
### 5. Lazy Modifier Hash Calculation (LOW PRIORITY)
- **Problem**: Hash calculations performed unnecessarily
- **Solution**: Compute hashes only when needed, cache between modifications
- **Expected Impact**: Minor optimization for specific scenarios
## Key Insights
1. **Profiling Reveals Surprises**: Both major optimizations (APDCache and shared_ptr) were discovered through profiling rather than intuition
2. **Atomic Operations Are Expensive**: Shared_ptr reference counting showed up as significant assembly-level overhead
3. **Algorithmic Improvements Matter**: O(n) → O(1) optimizations like the proposed Occupant() fix are worth pursuing
4. **Measurement is Critical**: ToByteString() caching seemed logical but provided no benefit
5. **Incremental Progress**: Each optimization reveals the next bottleneck, requiring continuous profiling
## Implementation Notes
- Always profile before and after changes to measure actual impact
- Be prepared to discard optimizations that don't provide measurable benefit
- Focus on algorithmic improvements and unexpected bottlenecks revealed by profiling
- Continue systematic analysis of PostActionUnchecked hotspots
@@ -0,0 +1,306 @@
//
// Created by Dan Crosby on 2025-01-15.
//
#include "AIPerformanceRunner.hpp"
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <string>
#include "PerformanceTestGameStateBuilder.hpp"
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
using namespace shardok;
namespace {
/**
* Convert completion reason to human-readable string.
*/
auto CompletionReasonToString(EvaluationCompletionReason reason) -> std::string {
switch (reason) {
case EvaluationCompletionReason::RAN_OUT_OF_COMMANDS:
return "completed all meaningful commands";
case EvaluationCompletionReason::RAN_OUT_OF_TIME: return "time budget exhausted";
case EvaluationCompletionReason::NOT_ENOUGH_TIME_TO_CONTINUE:
return "insufficient time for next depth";
default: return "unknown";
}
}
/**
* Parse command line arguments into a configuration struct.
*/
auto ParseCommandLineArgs(int argc, char* argv[]) -> PerformanceTestConfig {
PerformanceTestConfig config;
for (int i = 1; i < argc; ++i) {
std::string arg(argv[i]);
if (arg == "--help" || arg == "-h") {
std::cout << "Shardok AI Performance Runner\n"
<< "Usage: " << argv[0] << " [options]\n"
<< "\n"
<< "Options:\n"
<< " --map=NAME Map name (default: Alah)\n"
<< " --turns=N Number of turns to test (default: 5)\n"
<< " --defender=BOOL AI is defender (default: false)\n"
<< " --verbose Enable verbose output\n"
<< " --help, -h Show this help message\n";
std::exit(0);
} else if (arg.starts_with("--map=")) {
config.mapName = arg.substr(6);
} else if (arg.starts_with("--turns=")) {
config.numTurns = std::stoi(arg.substr(8));
} else if (arg.starts_with("--defender=")) {
std::string value = arg.substr(11);
config.defenderToggle = (value == "true" || value == "1");
} else if (arg == "--verbose") {
config.verbose = true;
} else {
std::cerr << "Unknown argument: " << arg << "\n";
std::cerr << "Use --help for usage information.\n";
std::exit(1);
}
}
return config;
}
} // namespace
int main(int argc, char* argv[]) {
std::cout << "Starting AI Performance Runner..." << std::endl;
// Set exec path so FilesystemUtils can find resource files
FilesystemUtils::SetExecPath(argv[0]);
// Set cache directory for ActionPointDistances
FixedActionPointDistances::SetCacheDirectory(
FilesystemUtils::CacheFilesDirectory() + "apdCache/");
try {
std::cout << "Shardok AI Performance Runner\n";
std::cout << "==============================\n";
// Parse command line arguments
auto config = ParseCommandLineArgs(argc, argv);
if (config.verbose) {
std::cout << "Configuration:\n";
std::cout << " Map: " << config.mapName << "\n";
std::cout << " Turns: " << config.numTurns << "\n";
std::cout << " AI is defender: " << (config.defenderToggle ? "Yes" : "No") << "\n";
}
// Initialize game settings
auto settings = PerformanceTestGameStateBuilder::InitializeGameSettings();
// Create test game state
auto gameState = PerformanceTestGameStateBuilder::CreatePerfTestGameState(
settings,
config.defenderToggle);
// Create engine
ShardokEngine engine(settings, gameState);
// Test basic functionality
auto currentState = engine.GetCurrentGameState();
// Create AI client for testing
const PlayerId aiPlayerId = 0;
const bool isDefender = config.defenderToggle;
const auto* hexMap = currentState->hex_map();
const auto settingsGetter = settings->GetGetter();
ShardokAIClient aiClient(aiPlayerId, isDefender, hexMap, settingsGetter);
// Create a second AI client for the human player during setup
// This ensures consistent state handling during setup phase
const PlayerId humanPlayerId = 1;
ShardokAIClient humanSetupAI(humanPlayerId, !isDefender, hexMap, settingsGetter);
// Complete setup phase - AI makes intelligent placement decisions
if (currentState->status()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
while (currentState->status()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
PlayerId currentPlayer = currentState->current_player();
auto availableCommands = engine.GetAvailableCommandProtos(currentPlayer, false);
if (availableCommands.empty()) {
std::cout << "No commands available for player "
<< static_cast<int>(currentPlayer) << "\n";
break;
}
if (currentPlayer == aiPlayerId) {
// Let AI make intelligent placement decisions
auto choiceResults = aiClient.ChooseCommandIndex(engine);
engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
} else {
// Human player: use AI for setup to ensure consistent state handling
auto choiceResults = humanSetupAI.ChooseCommandIndex(engine);
engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
}
currentState = engine.GetCurrentGameState();
}
}
// Test AI performance for configured number of turns
std::cout << "Running AI performance test for " << config.numTurns << " turns...\n";
std::vector<AIPerformanceMetrics> metrics;
for (int turn = 0; turn < config.numTurns; ++turn) {
// Check if AI can make a move
const auto availableCommands = engine.GetAvailableCommandProtos(aiPlayerId, false);
if (availableCommands.empty()) {
std::cout << " No commands available for AI player. Ending test.\n";
break;
}
// Get AI decision with performance metrics
auto choiceResults = aiClient.ChooseCommandIndex(engine);
std::cout << " AI chose command index: " << choiceResults.chosenIndex << "\n";
std::cout << " Depth achieved: " << choiceResults.depthAchieved << "\n";
std::cout << " Commands evaluated: " << choiceResults.commandCountEvaluated << "/"
<< choiceResults.availableCommandCount << "\n";
// Create metrics for this turn
AIPerformanceMetrics turnMetrics;
turnMetrics.commandNumber = turn + 1;
turnMetrics.totalCommands = static_cast<int>(choiceResults.availableCommandCount);
turnMetrics.depthAchieved = choiceResults.depthAchieved;
turnMetrics.commandsEvaluated = static_cast<int>(choiceResults.commandCountEvaluated);
turnMetrics.selectedCommandType = net::eagle0::shardok::common::CommandType_Name(
availableCommands[choiceResults.chosenIndex].type());
turnMetrics.completionReason = choiceResults.completionReason;
metrics.push_back(turnMetrics);
if (config.verbose) {
std::cout << " Command: " << turnMetrics.selectedCommandType << "\n";
std::cout << " Search depth: " << turnMetrics.depthAchieved << "\n";
std::cout << " Commands evaluated: " << turnMetrics.commandsEvaluated << "\n";
std::cout << " Applying command...\n";
}
// Apply the chosen command
engine.PostCommand(aiPlayerId, choiceResults.chosenIndex);
// Check if game is over
if (engine.GameIsOver()) {
std::cout << " Game over after " << (turn + 1) << " turns.\n";
break;
}
}
// Print summary
std::cout << "\nAI Search Performance Summary:\n";
std::cout << "==============================\n";
std::cout << "Total turns: " << metrics.size() << "\n";
if (!metrics.empty()) {
// Calculate summary statistics
double avgDepth = 0.0;
int totalEvaluated = 0;
int totalAvailable = 0;
for (const auto& metric : metrics) {
avgDepth += metric.depthAchieved;
totalEvaluated += metric.commandsEvaluated;
totalAvailable += metric.totalCommands;
}
avgDepth /= metrics.size();
std::cout << "Average search depth: " << std::fixed << std::setprecision(1) << avgDepth
<< "\n";
std::cout << "Total commands evaluated: " << totalEvaluated << "/" << totalAvailable
<< "\n";
// Calculate evaluation rate by depth
// Find max depth achieved across all turns
int maxDepth = 0;
for (const auto& metric : metrics) {
maxDepth = std::max(maxDepth, metric.depthAchieved);
}
if (maxDepth >= 2) {
std::cout << "\nCommands evaluated by depth:\n";
for (int depth = 2; depth <= maxDepth; ++depth) {
int turnsAtThisDepth = 0;
int totalCommandsAtDepth = 0;
int totalCommandsAvailableAtDepth = 0;
for (const auto& metric : metrics) {
bool reachedThisDepth = metric.depthAchieved >= depth;
bool completedAtLowerDepth =
(metric.depthAchieved < depth &&
metric.completionReason ==
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS);
if (reachedThisDepth || completedAtLowerDepth) {
turnsAtThisDepth++;
totalCommandsAvailableAtDepth += metric.totalCommands;
if (metric.depthAchieved > depth || completedAtLowerDepth) {
// If achieved higher depth OR completed all commands at lower
// depth, we evaluated ALL commands at this depth
totalCommandsAtDepth += metric.totalCommands;
} else if (metric.depthAchieved == depth) {
// If stopped at this depth, we evaluated commandsEvaluated commands
if (metric.completionReason ==
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS) {
// If ran out of commands, we evaluated all of them
totalCommandsAtDepth += metric.totalCommands;
} else {
// Otherwise we evaluated the reported number
totalCommandsAtDepth += metric.commandsEvaluated;
}
}
}
// If didn't reach this depth, contributes 0 commands (implicit)
}
double evalRate =
totalCommandsAvailableAtDepth > 0
? (100.0 * totalCommandsAtDepth / totalCommandsAvailableAtDepth)
: 0.0;
std::cout << " Depth " << depth << ": " << totalCommandsAtDepth << "/"
<< totalCommandsAvailableAtDepth << " commands (" << std::fixed
<< std::setprecision(1) << evalRate << "%, " << turnsAtThisDepth
<< "/" << metrics.size() << " turns reached)\n";
}
}
std::cout << "\nTurn-by-turn details:\n";
for (const auto& metric : metrics) {
std::string depthStr = std::to_string(metric.depthAchieved);
if (metric.completionReason == EvaluationCompletionReason::RAN_OUT_OF_COMMANDS) {
depthStr += "*";
}
std::cout << "Turn " << metric.commandNumber << ": depth " << depthStr
<< ", evaluated " << metric.commandsEvaluated << "/"
<< metric.totalCommands << ", chose " << metric.selectedCommandType
<< " (" << CompletionReasonToString(metric.completionReason) << ")\n";
}
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
return 0;
}
@@ -0,0 +1,55 @@
//
// Created by Dan Crosby on 2025-01-15.
//
#ifndef EAGLE0_AIPERFORMANCERUNNER_HPP
#define EAGLE0_AIPERFORMANCERUNNER_HPP
#include <chrono>
#include <map>
#include <string>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/ai/IterativeDeepeningAI.hpp"
namespace shardok {
/**
* Metrics captured for each AI command evaluation during performance testing.
*/
struct AIPerformanceMetrics {
int commandNumber;
int depthAchieved;
int commandsEvaluated;
int totalCommands;
std::string selectedCommandType;
EvaluationCompletionReason completionReason;
};
/**
* Overall results from a performance test run.
*/
struct PerformanceTestResults {
std::string mapName;
int totalTurns;
std::vector<AIPerformanceMetrics> commandMetrics;
double averageDepth;
double completionRate;
std::chrono::milliseconds totalTime;
};
/**
* Configuration options for performance testing.
*/
struct PerformanceTestConfig {
std::string mapName = "Alah";
int numTurns = 5;
bool defenderToggle = false;
bool verbose = false;
int aiUnitCount = 6;
int humanUnitCount = 6;
};
} // namespace shardok
#endif // EAGLE0_AIPERFORMANCERUNNER_HPP
@@ -0,0 +1,51 @@
load("//tools:copts.bzl", "COPTS")
cc_binary(
name = "ai_performance_runner",
srcs = [
"AIPerformanceRunner.cpp",
"AIPerformanceRunner.hpp",
],
copts = COPTS,
data = [
"//src/main/resources/net/eagle0/shardok:battalion_types",
"//src/main/resources/net/eagle0/shardok:settings",
"//src/main/resources/net/eagle0/shardok/maps",
],
deps = [
":performance_test_game_state_builder",
"//src/main/cpp/net/eagle0/common:filesystem_utils",
"//src/main/cpp/net/eagle0/common:time_utils",
"//src/main/cpp/net/eagle0/shardok/ai:ai_attacker_strategy_selector",
"//src/main/cpp/net/eagle0/shardok/ai:ai_defender_strategy_selector",
"//src/main/cpp/net/eagle0/shardok/ai:ai_iterative_deepening",
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator",
"//src/main/cpp/net/eagle0/shardok/ai:ai_time_budget",
"//src/main/cpp/net/eagle0/shardok/ai:ai_water_crossing_command_chooser",
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//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/util:battalion_type_registrar",
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
],
)
cc_library(
name = "performance_test_game_state_builder",
srcs = ["PerformanceTestGameStateBuilder.cpp"],
hdrs = [
"PerformanceTestGameStateBuilder.hpp",
],
copts = COPTS,
deps = [
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:game_state_helpers",
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
"//src/main/cpp/net/eagle0/shardok/util:battalion_type_registrar",
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
"//src/main/protobuf/net/eagle0/shardok/common:player_info_cc_proto",
],
)
@@ -0,0 +1,205 @@
# AI Performance Runner Implementation Plan
## Overview
This document outlines the implementation plan for an automated AI performance testing tool for Shardok. The tool will replicate the manual performance testing currently done through the Unity client's "Custom Battle" interface, providing reproducible and automated performance measurements.
## Goals
1. **Automate Performance Testing**: Eliminate the need for manual Unity client interaction
2. **Reproducible Results**: Ensure consistent test conditions across runs
3. **Detailed Metrics**: Capture the same metrics currently observed manually (commands evaluated at each depth)
4. **Clean Architecture**: Maintain proper dependency boundaries (no src/test dependencies in src/main)
## Directory Structure
```
src/main/cpp/net/eagle0/shardok/ai_performance_runner/
├── AIPerformanceRunner.cpp # Main binary entry point
├── AIPerformanceRunner.hpp # Performance metrics structs and helpers
├── PerformanceTestGameStateBuilder.cpp # Game state setup utilities
├── PerformanceTestGameStateBuilder.hpp # Game state builder interface
├── BUILD.bazel # Build configuration
└── README.md # Usage documentation
```
## Implementation Details
### 1. Performance Metrics Structure
```cpp
struct AIPerformanceMetrics {
int commandNumber;
int depthAchieved;
std::map<int, int> commandsEvaluatedAtDepth; // depth -> count
std::chrono::milliseconds timeUsed;
bool minimumDepthCompleted;
bool searchCompleted;
std::string selectedCommandType;
};
struct PerformanceTestResults {
std::string mapName;
int totalTurns;
std::vector<AIPerformanceMetrics> commandMetrics;
double averageDepth;
double completionRate;
std::chrono::milliseconds totalTime;
};
```
### 2. Test Configuration
The default configuration replicates the Unity client's "Perf" button:
- **Map**: "Alah"
- **AI Player**: 6 units with professions 1-6, all battalion type 4 (Heavy Infantry)
- **Human Player**: 6 units (no specific configuration needed since AI will control)
- **Defender Toggle**: Configurable (affects starting positions)
### 3. Key Components
#### AIPerformanceRunner.cpp
- Main entry point with command-line argument parsing
- Test execution loop
- Results formatting and output
- Integration with ShardokEngine and IterativeDeepeningAI
#### PerformanceTestGameStateBuilder.cpp
- Game state creation utilities (migrated from test code)
- Map loading helpers
- Unit placement logic
- Player setup functions
### 4. Build Configuration
```python
load("//tools:copts.bzl", "COPTS")
cc_binary(
name = "ai_performance_runner",
srcs = ["AIPerformanceRunner.cpp"],
copts = COPTS,
data = [
"//src/main/resources/net/eagle0/shardok:battalion_types",
"//src/main/resources/net/eagle0/shardok:settings",
"//src/main/resources/net/eagle0/shardok/maps",
],
deps = [
":performance_test_game_state_builder",
"//src/main/cpp/net/eagle0/common:time_utils",
"//src/main/cpp/net/eagle0/shardok/ai:ai_iterative_deepening",
"//src/main/cpp/net/eagle0/shardok/ai:ai_attacker_strategy_selector",
"//src/main/cpp/net/eagle0/shardok/ai:ai_defender_strategy_selector",
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator",
"//src/main/cpp/net/eagle0/shardok/ai:ai_time_budget",
"//src/main/cpp/net/eagle0/shardok/ai:ai_water_crossing_command_chooser",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//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/util:battalion_type_registrar",
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
],
)
cc_library(
name = "performance_test_game_state_builder",
srcs = ["PerformanceTestGameStateBuilder.cpp"],
hdrs = [
"AIPerformanceRunner.hpp",
"PerformanceTestGameStateBuilder.hpp",
],
copts = COPTS,
deps = [
"//src/main/cpp/net/eagle0/common:filesystem_utils",
"//src/main/cpp/net/eagle0/common:tsv_parser",
"//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/util:map_loader",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/flatbuffer/net/eagle0/shardok/storage:player_info_cc_fbs",
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
],
)
```
### 5. Command-Line Interface
```bash
# Run default performance test (Alah map, 6v6 units)
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner
# Run with specific number of turns
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --turns=10
# Run with defender configuration
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --defender=true
# Run with verbose output
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --verbose
# Run with specific map
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --map=Chipingia
```
### 6. Expected Output Format
```
Shardok AI Performance Test
===========================
Map: Alah
Configuration: 6v6 units (AI as attacker)
Time Budget: Dynamic (proximity-based)
Turn 1:
Command 1: Depth 2, evaluated 140/280 commands, time: 1250ms [MoveCommand]
Command 2: Depth 2, evaluated ALL commands, time: 1180ms [MeleeCommand]
Command 3: Depth 3, evaluated 21/156 commands, time: 1300ms [ArcheryCommand]
Command 4: Depth 3, evaluated 78/312 commands, time: 1290ms [MoveCommand]
Turn Summary: Avg depth 2.5, Total time: 5020ms
Overall Results:
Total Turns: 5
Average Depth Achieved: 2.4
Commands Completed at Target Depth: 85%
Total Time: 25.1s
Average Time per Command: 1255ms
```
### 7. Implementation Phases
#### Phase 1: Basic Infrastructure
1. Create directory structure and BUILD.bazel
2. Implement PerformanceTestGameStateBuilder with minimal game state creation
3. Create basic AIPerformanceRunner that can load a map and create players
#### Phase 2: AI Integration
1. Integrate IterativeDeepeningAI
2. Implement performance metric collection
3. Add basic output formatting
#### Phase 3: Full Feature Set
1. Add command-line argument parsing
2. Implement multiple test configurations (Perf, Rivers, Custom)
3. Add detailed performance metrics and analysis
#### Phase 4: Polish and Documentation
1. Create comprehensive README.md
2. Add error handling and validation
3. Implement baseline comparison features
## Success Criteria
1. **Functional**: Tool successfully runs AI turns and captures performance metrics
2. **Accurate**: Results match manually observed performance within reasonable variance
3. **Reproducible**: Multiple runs produce consistent results
4. **Maintainable**: Clean code structure with no dependencies on src/test
5. **Usable**: Clear command-line interface and helpful output
## Future Enhancements
- JSON output format for automated analysis
- Performance regression detection
- Integration with CI/CD pipeline
- Configurable test scenarios beyond "Perf" and "Rivers"
- Multi-threaded performance testing
@@ -0,0 +1,253 @@
//
// Created by Dan Crosby on 2025-01-15.
//
#include "PerformanceTestGameStateBuilder.hpp"
#include <filesystem>
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/common/TsvParser.hpp"
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/BattalionTypeRegistrar.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
namespace shardok {
namespace {
// Profession enum values
constexpr int NO_PROFESSION = 0;
// Player IDs
constexpr PlayerId AI_PLAYER_ID = 0;
constexpr PlayerId HUMAN_PLAYER_ID = 1;
} // namespace
auto PerformanceTestGameStateBuilder::InitializeGameSettings() -> GameSettingsSPtr {
auto settings = std::make_shared<GameSettings>();
auto setter = settings->GetSetter();
// Load battalion types
BattalionTypeRegistrar::RegisterBattalionTypes(setter);
// Load complete settings from settings.tsv file
TsvParser parser;
const string settingsPath = FilesystemUtils::StaticShardokFilesDirectory() + "settings.tsv";
const string settingsTsv = string(byte_vector::FromPath(settingsPath));
const auto valuesAndTypes = parser.ParseColumnEntryTsv(settingsTsv);
setter.SetFromTypesAndValues(valuesAndTypes[1], valuesAndTypes[0]);
return settings;
}
auto PerformanceTestGameStateBuilder::CreatePerfTestGameState(
const GameSettingsSPtr& settings,
bool defenderToggle) -> GameStateW {
return CreateCustomTestGameState(
settings,
"Alah",
6, // 6 AI units (full test configuration)
6, // 6 human units (full test configuration)
defenderToggle);
}
auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
const GameSettingsSPtr& settings,
const std::string& mapName,
int aiUnitCount,
int humanUnitCount,
bool defenderToggle) -> GameStateW {
// Load the map using existing utilities
auto hexMapProto = LoadMap(mapName);
// Create player info protos
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos;
// AI player
net::eagle0::shardok::common::PlayerInfo aiPlayerInfo;
aiPlayerInfo.set_player_id(AI_PLAYER_ID);
aiPlayerInfo.set_is_defender(defenderToggle);
aiPlayerInfo.set_starting_food(1000);
aiPlayerInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
if (defenderToggle) {
aiPlayerInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
} else {
aiPlayerInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
}
playerInfoProtos.push_back(aiPlayerInfo);
// Human player
net::eagle0::shardok::common::PlayerInfo humanPlayerInfo;
humanPlayerInfo.set_player_id(HUMAN_PLAYER_ID);
humanPlayerInfo.set_is_defender(!defenderToggle);
humanPlayerInfo.set_starting_food(1000);
humanPlayerInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
if (!defenderToggle) {
humanPlayerInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
} else {
humanPlayerInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
}
playerInfoProtos.push_back(humanPlayerInfo);
// Create units
std::vector<net::eagle0::shardok::storage::fb::Unit> units;
// Create AI units in reserve (location -1, -1)
for (int i = 0; i < aiUnitCount && i < 6; ++i) {
units.push_back(AddGenericUnit(
AI_PLAYER_ID,
i, // Unit ID
net::eagle0::shardok::storage::fb::Coords(-1, -1), // Reserve location
i + 1, // Profession: 1-6 (Mage through Strategist)
HEAVY_INFANTRY_BATTALION_TYPE,
defenderToggle ? -1 : 0)); // Defender: -1, Attacker: 0
}
// Create human units in reserve (location -1, -1)
for (int i = 0; i < humanUnitCount && i < 6; ++i) {
units.push_back(AddGenericUnit(
HUMAN_PLAYER_ID,
aiUnitCount + i, // Unit ID starting aiUnitCount
net::eagle0::shardok::storage::fb::Coords(-1, -1), // Reserve location
NO_PROFESSION,
HEAVY_INFANTRY_BATTALION_TYPE,
defenderToggle ? 0 : -1)); // Defender: -1, Attacker: 0
}
// Use the proper SetupInitialGameState helper (setup phase will be handled by AI)
return shardok::fb::SetupInitialGameState(
"performance_test_game", // gameId
hexMapProto,
playerInfoProtos,
units,
4, // month
false, // isWinter
settings->GetGetter());
}
auto PerformanceTestGameStateBuilder::AddPlayerInfo(
flatbuffers::FlatBufferBuilder& fbb,
int playerId,
bool isDefender,
int food) -> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo> {
std::vector<int8_t> victoryConditions{
net::eagle0::shardok::storage::fb::
VictoryCondition_VICTORY_CONDITION_LAST_PLAYER_STANDING};
if (isDefender) {
victoryConditions.push_back(
net::eagle0::shardok::storage::fb::
VictoryCondition_VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
} else {
victoryConditions.push_back(
net::eagle0::shardok::storage::fb::
VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
}
auto victoryConditionsOffset = fbb.CreateVector(victoryConditions);
net::eagle0::shardok::storage::fb::PlayerInfoBuilder pib(fbb);
pib.add_player_id(playerId);
pib.add_starting_food(food);
pib.add_is_defender(isDefender);
pib.add_victory_conditions(victoryConditionsOffset);
return pib.Finish();
}
auto PerformanceTestGameStateBuilder::AddGenericUnit(
PlayerId playerId,
UnitId unitId,
const net::eagle0::shardok::storage::fb::Coords& location,
int profession,
int battalionType,
int startingPositionIndex) -> net::eagle0::shardok::storage::fb::Unit {
net::eagle0::shardok::storage::fb::Unit unit{}; // Initialize to zero
// Basic unit properties (following UnitConversions.cpp pattern)
unit.mutate_player_id(playerId);
unit.mutate_unit_id(unitId);
unit.mutate_eagle_player_id(playerId); // Set eagle player ID
unit.mutable_location() = location;
unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
unit.mutate_remaining_action_points(12);
unit.mutate_hidden(false);
unit.mutate_fortified(false);
unit.mutate_can_flee(true);
unit.mutate_can_start_fire(false);
unit.mutate_can_archery(false);
unit.mutate_stun_rounds_remaining(0);
unit.mutate_commanding_unit_id(-1);
unit.mutate_targeted_unit(-1);
unit.mutate_starting_position_index(startingPositionIndex);
unit.mutate_has_moved_in_zoc(false);
unit.mutate_volleys_remaining(0);
unit.mutate_food_remaining(1000.0f); // Set food remaining
// Battalion
net::eagle0::shardok::storage::fb::Battalion battalion;
battalion.mutate_type(
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(battalionType));
battalion.mutate_size(1000.0);
battalion.mutate_armament(100.0f);
battalion.mutate_training(100.0f);
battalion.mutate_morale(50.0f);
unit.mutable_battalion() = battalion;
// Hero (if profession is specified)
if (profession != NO_PROFESSION) {
unit.mutate_has_attached_hero(true);
net::eagle0::shardok::storage::fb::Hero hero;
hero.mutate_strength(50);
hero.mutate_strength_xp(0);
hero.mutate_agility(50);
hero.mutate_agility_xp(0);
hero.mutate_wisdom(50);
hero.mutate_wisdom_xp(0);
hero.mutate_charisma(50);
hero.mutate_charisma_xp(0);
hero.mutate_constitution(80);
hero.mutate_constitution_xp(0);
hero.mutate_vigor(50);
hero.mutate_starting_vigor(50);
hero.mutate_spent_vigor(0);
hero.mutate_bravery(50);
hero.mutate_integrity(50);
hero.mutate_ambition(50);
hero.mutate_eagle_hero_id(unitId + 1);
hero.mutate_is_vip(false);
hero.mutable_profession_info().mutate_profession(
static_cast<net::eagle0::shardok::storage::fb::Profession>(profession));
hero.mutable_profession_info().mutate_meteor_cast_state(
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
hero.mutable_control_info().mutate_controlled_unit_id(-1);
hero.mutable_control_info().mutate_controlled_this_round(false);
unit.mutable_attached_hero() = hero;
} else {
unit.mutate_has_attached_hero(false);
}
// Initialize opponent knowledge for both players (player IDs 0 and 1)
unit.mutable_opponent_knowledge()->Mutate(0, 0); // Player 0 knowledge
unit.mutable_opponent_knowledge()->Mutate(1, 0); // Player 1 knowledge
return unit;
}
} // namespace shardok
@@ -0,0 +1,88 @@
//
// Created by Dan Crosby on 2025-01-15.
//
#ifndef EAGLE0_PERFORMANCETESTGAMESTATEBUILDER_HPP
#define EAGLE0_PERFORMANCETESTGAMESTATEBUILDER_HPP
#include <flatbuffers/flatbuffers.h>
#include <memory>
#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/player_info.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
namespace shardok {
// Forward declarations
class GameSettings;
using GameSettingsSPtr = std::shared_ptr<GameSettings>;
/**
* Builder class for creating game states used in performance testing.
* Provides utilities to set up specific test scenarios matching the Unity client's
* "Perf" button configuration.
*/
class PerformanceTestGameStateBuilder {
public:
/**
* Initialize game settings from the default configuration files.
* Must be called before creating game states.
*/
static auto InitializeGameSettings() -> GameSettingsSPtr;
/**
* Create the standard "Perf" test configuration:
* - Map: Alah
* - 6 AI units with professions 1-6, all Heavy Infantry
* - 6 Human units (minimal configuration)
*
* @param settings The game settings to use
* @param defenderToggle If true, AI is defender; if false, AI is attacker
* @return A GameStateW with the configured battle
*/
static auto CreatePerfTestGameState(
const GameSettingsSPtr& settings,
bool defenderToggle = false) -> GameStateW;
/**
* Create a custom test configuration with specified parameters.
*
* @param settings The game settings to use
* @param mapName Name of the map to load
* @param aiUnitCount Number of AI units to create
* @param humanUnitCount Number of human units to create
* @param defenderToggle If true, AI is defender; if false, AI is attacker
* @return A GameStateW with the configured battle
*/
static auto CreateCustomTestGameState(
const GameSettingsSPtr& settings,
const std::string& mapName,
int aiUnitCount,
int humanUnitCount,
bool defenderToggle) -> GameStateW;
private:
// Helper functions for building game state components
static auto
AddPlayerInfo(flatbuffers::FlatBufferBuilder& fbb, int playerId, bool isDefender, int food)
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
static auto AddGenericUnit(
PlayerId playerId,
UnitId unitId,
const net::eagle0::shardok::storage::fb::Coords& location,
int profession,
int battalionType,
int startingPositionIndex = -1) -> net::eagle0::shardok::storage::fb::Unit;
// Battalion type constants (matching Unity client)
static constexpr int HEAVY_INFANTRY_BATTALION_TYPE = 4;
};
} // namespace shardok
#endif // EAGLE0_PERFORMANCETESTGAMESTATEBUILDER_HPP
@@ -5,7 +5,10 @@
#ifndef EAGLE0_GAMEUPDATERECEIVER_HPP
#define EAGLE0_GAMEUPDATERECEIVER_HPP
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
#include "src/main/protobuf/net/eagle0/shardok/api/action_result_view.pb.h"
#pragma GCC diagnostic pop
namespace shardok {
using std::vector;
@@ -8,9 +8,11 @@
#include "ShardokGameController.hpp"
#include <algorithm>
#include <iterator>
#include <ranges>
#include <thread>
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
@@ -85,10 +87,10 @@ void ShardokGameController::LockedNotifyClients() const { updateCondition.notify
auto ShardokGameController::LockedAIClientForPid(PlayerId pid) const
-> shared_ptr<ShardokAIClient> {
return common::FindIf(
aiClients,
[pid](const auto &client) { return client->GetPlayerId() == pid; })
.value_or(nullptr);
const auto it = std::ranges::find_if(aiClients, [pid](const auto &client) {
return client->GetPlayerId() == pid;
});
return (it != aiClients.end()) ? *it : nullptr;
}
void ShardokGameController::DoAIThread() {
@@ -129,7 +131,7 @@ auto ShardokGameController::LockedCheckOneAICommand() -> bool {
const PlayerId currentPid = engine->GetCurrentPlayerId();
if (const shared_ptr<ShardokAIClient> currentPlayerClient = LockedAIClientForPid(currentPid)) {
const int index = currentPlayerClient->ChooseCommandIndex(*engine);
const int index = currentPlayerClient->ChooseCommandIndex(*engine).chosenIndex;
engine->PostCommand(currentPid, index);
LockedNotifyClients();
@@ -165,7 +167,7 @@ void ShardokGameController::PostCommand(
CheckFactionId(engine, shardokPlayerId, eagleFactionId);
const auto expectedToken = engine->GetUnfilteredHistoryCount();
const auto expectedToken = static_cast<int64_t>(engine->GetUnfilteredHistoryCount());
if (token < expectedToken) {
printf("Double token in postCommand\n");
// The client is missing some updates; probably it's a double-submit
@@ -193,7 +195,7 @@ void ShardokGameController::PostPlacementCommands(
CheckFactionId(engine, shardokPlayerId, eagleFactionId);
const auto expectedToken = engine->GetUnfilteredHistoryCount();
const auto expectedToken = static_cast<int64_t>(engine->GetUnfilteredHistoryCount());
if (token < expectedToken) {
printf("Double token in postPlacementCommands\n");
// The client is missing some updates; probably it's a double-submit
@@ -240,9 +242,11 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
incomingRegistrations--;
}
updates.mainResults = common::Map(awrs, [](const ShardokActionWithResultingState &a) {
return a.action_result();
});
updates.mainResults.reserve(awrs.size());
std::ranges::transform(
awrs,
std::back_inserter(updates.mainResults),
[](const ShardokActionWithResultingState &a) { return a.action_result(); });
const auto playerInfos = engine->GetPlayerInfos();
updates.filteredResults.reserve(playerInfos.size() + 1);
@@ -8,6 +8,9 @@
#include "AvailableCommandsFactory.hpp"
#include <algorithm>
#include <ranges>
#include "src/main/cpp/net/eagle0/shardok/library/FireUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/PlayerSetupCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"
@@ -132,30 +135,30 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
}
if (battType->adjustsMorale &&
unit->battalion().morale() < settings.Backing().minimum_morale_to_act()) {
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->CanDoWithLowMorale();
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->CanDoWithLowMorale();
});
}
if (unit->stun_rounds_remaining() > 0) {
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->CanDoWhileStunned();
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->CanDoWhileStunned();
});
}
if (hasHero && unit->attached_hero().vigor() < settings.Backing().minimum_vigor_to_act()) {
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->CanDoWithLowVigor();
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->CanDoWithLowVigor();
});
}
if (unitMovedIntoZoc) {
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->CanDoAfterMovingIntoZoc();
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->CanDoAfterMovingIntoZoc();
});
}
if (common::ContainsWhere(oneUnitCommands, [](const CommandSPtr &cmd) {
if (std::ranges::any_of(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->IsRequiredToEndTurn();
})) {
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->IsRequiredToEndTurn();
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->IsRequiredToEndTurn();
});
}
@@ -183,7 +186,7 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
/* onlyFollowUps=*/false);
}
if (!common::ContainsWhere(commands, [](const CommandSPtr &command) {
if (!std::ranges::any_of(commands, [](const CommandSPtr &command) {
return command->IsRequiredToEndTurn();
})) {
commands.push_back(std::make_shared<EndTurnCommand>(playerId, gameState, settings));
@@ -9,17 +9,15 @@
#ifndef AvailableCommandsFactory_hpp
#define AvailableCommandsFactory_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/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
namespace shardok {
using std::optional;
using std::unique_ptr;
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
using UnitIdOptional = optional<UnitId>;
class AvailableCommandsFactory {
@@ -1,5 +1,19 @@
load("//tools:copts.bzl", "COPTS")
cc_library(
name = "game_state_w",
srcs = ["GameStateW.cpp"],
hdrs = ["GameStateW.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
deps = [
":shardok_c_types",
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
],
)
cc_library(
name = "engine",
srcs = ["ShardokEngine.cpp"],
@@ -7,6 +21,7 @@ cc_library(
copts = COPTS,
visibility = ["//visibility:public"],
deps = [
":game_state_w",
":unit_placement_info",
"//src/main/cpp/net/eagle0/shardok/library/actions:perform_undead_commands_action",
"//src/main/cpp/net/eagle0/shardok/library/actions:update_game_status_action",
@@ -15,7 +30,6 @@ cc_library(
"//src/main/cpp/net/eagle0/shardok/library/util:game_state_validator",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:action_result_filter",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_filter",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/protobuf/net/eagle0/shardok/storage:action_with_resulting_state_cc_proto",
],
)
@@ -117,10 +131,9 @@ cc_library(
copts = COPTS,
visibility = ["//src/main/cpp/net/eagle0/shardok/library:__subpackages__"],
deps = [
":game_state_w",
":shardok_exception",
"//src/main/cpp/net/eagle0/common:random_generator",
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
],
)
@@ -0,0 +1,135 @@
//
// Created by Dan Crosby on 2025-01-21.
//
#include "GameStateW.hpp"
#include <algorithm>
#include <ranges>
namespace shardok {
auto GameStateW::GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
-> const Unit* {
const auto* state = Get();
if (!state || !state->hex_map()) { return nullptr; }
const int16_t rowCount = state->hex_map()->row_count();
const int16_t columnCount = state->hex_map()->column_count();
// 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 (state->occupied_tiles() && !state->occupied_tiles()->empty()) {
const size_t tileIndex =
static_cast<size_t>(coords.row()) * static_cast<size_t>(columnCount) +
static_cast<size_t>(coords.column());
const size_t expectedBitfieldSize =
(static_cast<size_t>(rowCount) * static_cast<size_t>(columnCount) + 7) /
8; // Ceiling division
if (state->occupied_tiles()->size() == expectedBitfieldSize) {
const size_t byteIndex = tileIndex / 8;
const size_t bitOffset = tileIndex % 8;
const uint8_t byte = state->occupied_tiles()->Get(static_cast<unsigned int>(byteIndex));
const bool isOccupied = (byte & (1 << bitOffset)) != 0;
if (!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 (!state->units()) { return nullptr; }
for (size_t i = 0; i < state->units()->size(); ++i) {
const auto* unit = state->units()->Get(static_cast<unsigned int>(i));
if (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 GameStateW::GetKnownEnemyOccupant(
PlayerId playerId,
const std::vector<PlayerId>& allyPids,
const net::eagle0::shardok::storage::fb::Coords& coords) const -> const Unit* {
const auto* occupant = GetOccupant(coords);
if (occupant) {
if (!occupant->hidden() && occupant->player_id() != playerId &&
!std::ranges::contains(allyPids, occupant->player_id())) {
return occupant;
}
}
return nullptr;
}
void GameStateW::UpdateOccupiedTile(
const net::eagle0::shardok::storage::fb::Coords& oldCoords,
const net::eagle0::shardok::storage::fb::Coords& newCoords) {
const auto* state = Get();
auto* mutableOccupiedTiles = (*this)->mutable_occupied_tiles();
if (!state || !state->hex_map() || !mutableOccupiedTiles) { return; }
const int16_t rowCount = state->hex_map()->row_count();
const int16_t columnCount = state->hex_map()->column_count();
// Clear old position in bitfield
if (oldCoords.row() >= 0 && oldCoords.row() < rowCount && oldCoords.column() >= 0 &&
oldCoords.column() < columnCount) {
const size_t tileIndex =
static_cast<size_t>(oldCoords.row()) * static_cast<size_t>(columnCount) +
static_cast<size_t>(oldCoords.column());
const size_t byteIndex = tileIndex / 8;
const size_t bitOffset = tileIndex % 8;
if (byteIndex < mutableOccupiedTiles->size()) {
uint8_t byte = mutableOccupiedTiles->Get(static_cast<unsigned int>(byteIndex));
byte &= ~(1 << bitOffset); // Clear the bit
mutableOccupiedTiles->Mutate(static_cast<unsigned int>(byteIndex), byte);
}
}
// Set new position in bitfield
if (newCoords.row() >= 0 && newCoords.row() < rowCount && newCoords.column() >= 0 &&
newCoords.column() < columnCount) {
const size_t tileIndex =
static_cast<size_t>(newCoords.row()) * static_cast<size_t>(columnCount) +
static_cast<size_t>(newCoords.column());
const size_t byteIndex = tileIndex / 8;
const size_t bitOffset = tileIndex % 8;
if (byteIndex < mutableOccupiedTiles->size()) {
uint8_t byte = mutableOccupiedTiles->Get(static_cast<unsigned int>(byteIndex));
byte |= (1 << bitOffset); // Set the bit
mutableOccupiedTiles->Mutate(static_cast<unsigned int>(byteIndex), byte);
}
}
}
auto GameStateW::GetOccupiedTilesBitfield() const -> const flatbuffers::Vector<uint8_t>* {
const auto* state = Get();
if (!state || !state->hex_map()) { return nullptr; }
if (!state->occupied_tiles() || state->occupied_tiles()->empty()) { return nullptr; }
// Verify the bitfield size matches expected map size
const int16_t rowCount = state->hex_map()->row_count();
const int16_t columnCount = state->hex_map()->column_count();
const size_t expectedBitfieldSize =
(static_cast<size_t>(rowCount) * static_cast<size_t>(columnCount) + 7) / 8;
if (state->occupied_tiles()->size() != expectedBitfieldSize) { return nullptr; }
return state->occupied_tiles();
}
} // namespace shardok
@@ -0,0 +1,111 @@
//
// Created by Dan Crosby on 2025-01-15.
//
#ifndef EAGLE0_GAMESTATEW_HPP
#define EAGLE0_GAMESTATEW_HPP
#include <cstdint>
#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"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
namespace shardok {
/**
* @class GameStateW
* @brief A wrapper class for the FlatBuffer-generated GameState type.
*
* GameStateW extends the Wrapper class to provide additional functionality
* for working with the net::eagle0::shardok::storage::fb::GameState type.
* It inherits all constructors and assignment operators from the base Wrapper
* class, enabling seamless integration with the underlying FlatBuffer type.
*
* This class is part of the shardok namespace and is designed to simplify
* interactions with the GameState FlatBuffer type while maintaining the
* flexibility and functionality of the Wrapper base class.
*/
class GameStateW : public Wrapper<net::eagle0::shardok::storage::fb::GameState> {
public:
using BaseType = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
using Unit = net::eagle0::shardok::storage::fb::Unit;
// Inherit all constructors from Wrapper
using BaseType::BaseType;
// Default constructor
GameStateW() : BaseType() {}
// Copy constructor
GameStateW(const GameStateW& other) : BaseType(other) {}
// Move constructor
GameStateW(GameStateW&& other) noexcept : BaseType(std::move(other)) {}
// Copy assignment
GameStateW& operator=(const GameStateW& other) {
BaseType::operator=(other);
return *this;
}
// Move assignment
GameStateW& operator=(GameStateW&& other) noexcept {
BaseType::operator=(std::move(other));
return *this;
}
// Constructor from base type
GameStateW(const BaseType& base) : BaseType(base) {}
GameStateW(BaseType&& base) : BaseType(std::move(base)) {}
/**
* @brief Get the unit occupying the specified coordinates using occupied tiles bitfield.
* @param coords The coordinates to check.
* @return Pointer to the unit at the coordinates, or nullptr if none.
*
* Fast path: O(1) bitfield check for empty tiles (~90% of cases).
* Slow path: O(n) unit search only when bitfield indicates occupation (~10% of cases).
*/
[[nodiscard]] auto GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
-> const Unit*;
/**
* @brief Get the known enemy unit occupying the specified coordinates using occupied tiles
* bitfield.
* @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.
*
* Uses the bitfield-optimized GetOccupant() internally.
*/
[[nodiscard]] auto GetKnownEnemyOccupant(
PlayerId playerId,
const std::vector<PlayerId>& allyPids,
const net::eagle0::shardok::storage::fb::Coords& coords) const -> const Unit*;
/**
* @brief Update the occupied tiles bitfield when a unit changes position.
* @param oldCoords The previous coordinates (use {-1, -1} if unit was off-map).
* @param newCoords The new coordinates (use {-1, -1} if unit is now off-map).
*/
void UpdateOccupiedTile(
const net::eagle0::shardok::storage::fb::Coords& oldCoords,
const net::eagle0::shardok::storage::fb::Coords& newCoords);
/**
* @brief Get the occupied tiles bitfield for efficient tile occupancy checking.
* @return Pointer to the bitfield data, or nullptr if not available.
*
* Returns the raw bitfield where bit at index (row*column_count + col) indicates
* if that tile is occupied. Useful for caching the bitfield to avoid repeated
* GameStateW lookups in performance-critical code like MoveCommand.
*/
[[nodiscard]] auto GetOccupiedTilesBitfield() const -> const flatbuffers::Vector<uint8_t>*;
};
} // namespace shardok
#endif // EAGLE0_GAMESTATEW_HPP
@@ -12,7 +12,10 @@
#include <string>
#include <vector>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
#include "src/main/protobuf/net/eagle0/shardok/storage/odds.pb.h"
#pragma GCC diagnostic pop
namespace shardok {
typedef net::eagle0::shardok::storage::Odds PercentileRollOdds;
@@ -14,7 +14,7 @@ using std::vector;
auto ShardokAction::Execute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
vector<ActionResult> results = InternalExecute(currentState, generator);
return results;
@@ -22,7 +22,7 @@ auto ShardokAction::Execute(
auto ShardokAction::ExecuteWithRoll(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator,
const std::shared_ptr<RandomGenerator>& generator,
const std::optional<int32_t> roll) const -> vector<ActionResult> {
vector<ActionResult> results = InternalExecuteWithRoll(currentState, generator, roll);
@@ -13,14 +13,15 @@
#include "ShardokException.hpp"
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
#pragma GCC diagnostic pop
namespace shardok {
using net::eagle0::shardok::storage::ActionResult;
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
using std::shared_ptr;
using std::vector;
using PercentileRollOdds = net::eagle0::shardok::storage::Odds;
@@ -39,14 +40,14 @@ private:
// override that one and get the default behavior here.
[[nodiscard]] virtual auto InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator> generator) const -> std::vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& generator) const -> std::vector<ActionResult> {
return InternalExecuteWithRoll(currentState, generator, std::optional<int32_t>());
}
[[nodiscard]] virtual auto InternalExecuteWithRoll(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator,
std::optional<int32_t> roll) const -> std::vector<ActionResult> {
const GameStateW& /*currentState*/,
const std::shared_ptr<RandomGenerator>& /*generator*/,
std::optional<int32_t> /*roll*/) const -> std::vector<ActionResult> {
throw ShardokClientErrorException("Roll not supported");
}
@@ -58,11 +59,11 @@ public:
[[nodiscard]] auto Execute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> std::vector<ActionResult>;
const std::shared_ptr<RandomGenerator>& generator) const -> std::vector<ActionResult>;
[[nodiscard]] auto ExecuteWithRoll(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator,
const std::shared_ptr<RandomGenerator>& generator,
std::optional<int32_t> roll) const -> std::vector<ActionResult>;
};
@@ -46,7 +46,7 @@ public:
[[nodiscard]] virtual auto HasOdds() const -> bool { return false; }
[[nodiscard]] virtual auto GetOddsPercentile() const -> int32_t { return 0; }
virtual void AddFollowUpCommandTypes(const std::unordered_set<CommandType>& newTypes) {
virtual void AddFollowUpCommandTypes(const std::unordered_set<CommandType>& /*newTypes*/) {
throw ShardokInternalErrorException("Can't add follow up commands to this type");
}
};
@@ -9,6 +9,8 @@
#include "ShardokEngine.hpp"
#include <algorithm>
#include <optional>
#include <ranges>
#include <utility>
#include <vector>
@@ -37,11 +39,6 @@ using net::eagle0::shardok::storage::ShardokActionWithResultingState;
using GameStatusProto = net::eagle0::shardok::common::GameStatus;
using TileModifierProto = net::eagle0::shardok::common::TileModifier;
[[nodiscard]] auto ShardokEngine::GetCurrentGameState() const
-> net::eagle0::shardok::storage::fb::GameState const * {
return gameState.Get();
}
[[nodiscard]] auto ShardokEngine::GetCurrentGameStateBytes() const -> byte_vector {
return gameState.ToByteVector();
}
@@ -97,7 +94,7 @@ void ShardokEngine::ApplyAndAddActionResults(const vector<ActionResultProto> &re
}
void ShardokEngine::ApplyAndAddActionResult(const ActionResultProto &result) {
MutatingApplyResult(gameState, result, settingsGetter);
gameState = ApplyResult(std::move(gameState), result, settingsGetter);
if (trackHistory) {
actionHistory.emplace_back();
@@ -114,7 +111,7 @@ ShardokEngine::ShardokEngine(
settingsGetter(settings->GetGetter()),
availableCommandsFactory(
AvailableCommandsFactory::MakeAvailableCommandsFactory(settingsGetter)),
gameState(fb::GameStateW::FromByteString(history.back().state_after_fb())),
gameState(GameStateW::FromByteString(history.back().state_after_fb())),
trackHistory(trackHistory),
actionHistory(history),
criticalTileCoords(gameState->hex_map()) {}
@@ -184,7 +181,7 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
const ShardokActionWithResultingState &awrs : newHistory) {
GameStateView viewAfter = GameStateFilteredForPlayer(
settingsGetter,
fb::GameStateW::FromByteString(awrs.state_after_fb()),
GameStateW::FromByteString(awrs.state_after_fb()),
askingPlayer);
if (auto filteredResult = ActionResultFilteredForPlayer(
@@ -197,7 +194,7 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
filteredResult.has_value()) {
filteredHistory.push_back(*filteredResult);
}
previousState = fb::GameStateW::FromByteString(awrs.state_after_fb());
previousState = GameStateW::FromByteString(awrs.state_after_fb());
previousStatePtr = previousState.Get();
previousView = viewAfter;
}
@@ -219,7 +216,7 @@ auto ShardokEngine::GetUnitById(const PlayerId askingPlayer, const UnitId unitId
}
void ShardokEngine::PostWhileCurrentPlayerHasOnlyOneOption(
const std::shared_ptr<RandomGenerator> &randomGenerator) {
std::shared_ptr<RandomGenerator> randomGenerator) {
while (GetGameStatus()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING &&
GetCurrentPlayerId() != UNCONTROLLED_PLAYER_ID) {
@@ -315,21 +312,19 @@ void ShardokEngine::PostPlacementCommands(
availableCommandsFactory->GetPlayerSetupCommands(gameState, player);
// first make sure they're all valid and there are no duplicates
for (int i = 0; i < placementInfos.size(); i++) {
for (size_t i = 0; i < placementInfos.size(); i++) {
const UnitPlacementInfo &pi = placementInfos[i];
if (auto command = common::FindIf(
*placementCommands,
[pi](const CommandSPtr &cmd) {
return cmd->GetCommandProto().actor().value() == pi.unitId &&
cmd->GetCommandProto().target() == pi.location;
});
!command.has_value()) {
const auto it = std::ranges::find_if(*placementCommands, [pi](const CommandSPtr &cmd) {
return cmd->GetCommandProto().actor().value() == pi.unitId &&
cmd->GetCommandProto().target() == pi.location;
});
if (it == placementCommands->end()) {
throw ShardokClientErrorException("No such placement info found");
}
// check that we're not double-filling any location or double-placing any unit
for (int j = i + 1; j < placementInfos.size(); j++) {
for (size_t j = i + 1; j < placementInfos.size(); j++) {
const UnitPlacementInfo &other = placementInfos[j];
if (pi.unitId == other.unitId)
@@ -344,12 +339,11 @@ void ShardokEngine::PostPlacementCommands(
// now execute
for (const auto &pi : placementInfos) {
auto command = common::FindIf(*placementCommands, [pi](const CommandSPtr &cmd) {
const auto it = std::ranges::find_if(*placementCommands, [pi](const CommandSPtr &cmd) {
return cmd->GetCommandProto().actor().value() == pi.unitId &&
cmd->GetCommandProto().target() == pi.location;
});
for (vector<ActionResult> onePlacementResults =
(*command)->Execute(gameState, randomGenerator);
for (vector<ActionResult> onePlacementResults = (*it)->Execute(gameState, randomGenerator);
const ActionResultProto &oneResult : onePlacementResults) {
HandleActionResult(oneResult, randomGenerator);
}
@@ -381,17 +375,17 @@ void ShardokEngine::PostFinishedPlacementCommand(
const auto placementCommands =
availableCommandsFactory->GetPlayerSetupCommands(gameState, player);
const auto command = common::FindIf(*placementCommands, [](const CommandSPtr &cmd) {
const auto it = std::ranges::find_if(*placementCommands, [](const CommandSPtr &cmd) {
return cmd->GetCommandProto().type() ==
net::eagle0::shardok::common::END_PLAYER_SETUP_COMMAND;
});
if (!command.has_value()) {
if (it == placementCommands->end()) {
throw ShardokClientErrorException("No finish placement command found");
}
cachedAvailableCommands = nullptr;
PostActionUnchecked(command.value(), randomGenerator, std::nullopt);
PostActionUnchecked(*it, randomGenerator, std::nullopt);
}
void ShardokEngine::PostCommand(
@@ -460,7 +454,7 @@ void ShardokEngine::HandleActionResult(
const Coords modifiedCoords = FromCoordsProto(modifierWithCoords.coords());
const TileModifierProto &modifier = modifierWithCoords.modifiers();
const Unit *occupant = Occupant(GetCurrentGameState()->units(), modifiedCoords);
const Unit *occupant = gameState.GetOccupant(modifiedCoords);
// Check for swept away hero
if (const Terrain *terrain = GetTerrain(GetCurrentGameState()->hex_map(), modifiedCoords);
occupant && IsWater(terrain->type()) && !IsTraversible(modifier) &&
@@ -587,6 +581,7 @@ void AddUnits(vector<net::eagle0::shardok::storage::ResolvedUnit> &to, const Uni
break;
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
ru.set_status(
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT);
break;
@@ -604,7 +599,7 @@ auto ShardokEngine::EndGameUnits() const -> vector<net::eagle0::shardok::storage
"Trying to get the end game units before the game is over");
}
const auto *gs = GetCurrentGameState();
const auto &gs = GetCurrentGameState();
vector<net::eagle0::shardok::storage::ResolvedUnit> endgameUnits;
AddUnits(endgameUnits, *gs->units());
@@ -20,7 +20,6 @@
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/action_result_view.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"
@@ -35,7 +34,6 @@ using std::vector;
using net::eagle0::shardok::api::UnitView;
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
using net::eagle0::shardok::storage::ShardokActionWithResultingState;
using HexMapProto = net::eagle0::shardok::common::HexMap;
@@ -62,15 +60,14 @@ private:
[[nodiscard]] auto HandleUnitFallingIntoWater(
const Terrain *terrain,
const net::eagle0::shardok::storage::fb::Unit *unit,
const fb::Unit *unit,
std::shared_ptr<RandomGenerator> randomGenerator) const -> vector<ActionResult>;
void HandleActionResult(
const ActionResult &actionResult,
const std::shared_ptr<RandomGenerator> &randomGenerator);
[[nodiscard]] auto GetUnit(const UnitId uid) const
-> const net::eagle0::shardok::storage::fb::Unit * {
[[nodiscard]] auto GetUnit(const UnitId uid) const -> const fb::Unit * {
return GetCurrentGameState()->units()->Get(uid);
}
@@ -114,8 +111,7 @@ public:
[[nodiscard]] auto GetGameStateAtStartOfAction(ActionId startingActionId) const -> GameStateW;
[[nodiscard]] auto GetCurrentGameState() const
-> net::eagle0::shardok::storage::fb::GameState const *;
[[nodiscard]] auto GetCurrentGameState() const -> const GameStateW & { return gameState; }
[[nodiscard]] auto GetCurrentGameStateBytes() const -> byte_vector;
@@ -129,7 +125,7 @@ public:
// Controller API
[[nodiscard]] auto GetGameHistory(ActionId lastUpdatedActionId) const
-> vector<net::eagle0::shardok::storage::ShardokActionWithResultingState>;
-> vector<ShardokActionWithResultingState>;
[[nodiscard]] auto GetUnfilteredHistoryCount() const -> size_t {
return actionHistory.size() + startingHistoryCount;
@@ -145,8 +141,7 @@ public:
[[nodiscard]] auto GetFilteredGameHistory(PlayerId askingPlayer) const
-> vector<net::eagle0::shardok::api::ActionResultView>;
[[nodiscard]] auto GetUnitById(PlayerId askingPlayer, UnitId unitId) const
-> net::eagle0::shardok::api::UnitView;
[[nodiscard]] auto GetUnitById(PlayerId askingPlayer, UnitId unitId) const -> UnitView;
void PostPlacementCommands(
PlayerId player,
@@ -161,8 +156,7 @@ public:
std::shared_ptr<RandomGenerator> randomGenerator = nullptr,
std::optional<int32_t> roll = std::nullopt);
void PostWhileCurrentPlayerHasOnlyOneOption(
const std::shared_ptr<RandomGenerator> &randomGenerator);
void PostWhileCurrentPlayerHasOnlyOneOption(std::shared_ptr<RandomGenerator> randomGenerator);
auto PostWhilePlayerHasOnlyOneOption(
PlayerId playerId,
std::shared_ptr<RandomGenerator> randomGenerator) -> bool;
@@ -180,7 +174,7 @@ public:
[[nodiscard]] auto GetMonth() const -> int { return GetCurrentGameState()->month(); }
[[nodiscard]] auto GetPlayerInfos() const -> vector<PlayerInfoProto> {
const auto *currentGameState = GetCurrentGameState();
const auto &currentGameState = GetCurrentGameState();
vector<PlayerInfoProto> protos{};
for (const auto *const piFB : *currentGameState->player_infos()) {
protos.push_back(fb::ToPlayerInfoProto(piFB));
@@ -188,18 +182,18 @@ public:
return protos;
}
auto GetGameStatus() const -> const net::eagle0::shardok::storage::fb::GameStatus * {
[[nodiscard]] auto GetGameStatus() const
-> const net::eagle0::shardok::storage::fb::GameStatus * {
return GetCurrentGameState()->status();
}
auto GetGameSettings() const -> GameSettingsSPtr { return gameSettings; }
[[nodiscard]] auto GetGameSettings() const -> GameSettingsSPtr { return gameSettings; }
static inline auto GameIsOver(const net::eagle0::shardok::storage::fb::GameStatus *status)
-> bool {
static inline auto GameIsOver(const fb::GameStatus *status) -> bool {
return (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY);
}
inline auto GameIsOver() const -> bool { return GameIsOver(GetGameStatus()); }
[[nodiscard]] inline auto GameIsOver() const -> bool { return GameIsOver(GetGameStatus()); }
};
} // namespace shardok
@@ -10,14 +10,10 @@
#define MeteorCastActionFactory_hpp
#include "src/main/cpp/net/eagle0/shardok/library/ShardokAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
namespace shardok {
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
class MeteorCastActionFactory {
private:
const SettingsGetter settings;
@@ -28,7 +28,7 @@ auto PlayerSetupCommandFactory::AddAvailablePlaceAndHideUnitCommandsForOneUnit(
CoordsSet unusedStartingPositions(gameState->hex_map());
for (const Coords *possiblePosition : *thisUnitStartingPositions) {
if (!Occupant(gameState->units(), *possiblePosition)) {
if (!gameState.GetOccupant(*possiblePosition)) {
unusedStartingPositions.Add(*possiblePosition);
}
}
@@ -37,7 +37,7 @@ auto PlayerSetupCommandFactory::AddAvailablePlaceAndHideUnitCommandsForOneUnit(
CoordsSet unusedHidingPositions(gameState->hex_map());
for (const Coords &possibleHidingPosition : GetAllCoords(gameState->hex_map())) {
if (!Occupant(gameState->units(), possibleHidingPosition)) {
if (!gameState.GetOccupant(possibleHidingPosition)) {
const Terrain *terrain = GetTerrain(gameState->hex_map(), possibleHidingPosition);
if (AllowsHiding(terrain)) { unusedHidingPositions.Add(possibleHidingPosition); }
}
@@ -87,11 +87,11 @@ auto PlayerSetupCommandFactory::AddAvailablePlayerSetupCommands(
if (placedUnits.size() >= 10) return;
if (unplacedUnits.empty()) return;
for (const auto &kv : unplacedUnits) {
for (const auto &[unitId, unit] : unplacedUnits) {
AddAvailablePlaceAndHideUnitCommandsForOneUnit(
existingCommands,
isDefender,
kv.second,
unit,
gameState);
}
}
@@ -5,14 +5,12 @@
#ifndef EAGLE0_PLAYERSETUPCOMMANDFACTORY_HPP
#define EAGLE0_PLAYERSETUPCOMMANDFACTORY_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/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
namespace shardok {
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
using Unit = net::eagle0::shardok::storage::fb::Unit;
class PlayerSetupCommandFactory {
@@ -5,13 +5,11 @@
#ifndef EAGLE0_UNDEADCHANGEACTIONFACTORY_HPP
#define EAGLE0_UNDEADCHANGEACTIONFACTORY_HPP
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
namespace shardok {
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
class UndeadChangeActionFactory {
private:
@@ -81,11 +81,6 @@ private:
vector<shared_future<vector<int16_t>>> distances;
static void fill(
vector<std::unordered_map<size_t, std::shared_ptr<ActionPointDistances>>> &vec) {
for (int i = 0; i < 6; i++) { vec.emplace_back(); }
}
public:
explicit OnDemandActionPointDistances(
const HexMap *map,
@@ -4,12 +4,16 @@
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include <algorithm>
#include <chrono>
#include <unordered_map>
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#define CACHE_STATS_LOGGING_ false
#define CACHE_STATS_FREQUENCY_SECONDS_ 1
namespace shardok {
@@ -19,32 +23,86 @@ thread_local ActionPointDistancesCache::TLSCache ActionPointDistancesCache::tlsC
#if CACHE_STATS_LOGGING_
// Thread-local statistics for performance monitoring
thread_local struct {
int persistentHits = 0;
int persistentMisses = 0;
int localHits = 0;
int localMisses = 0;
int sharedAccesses = 0;
int evictionEvents = 0;
int apdLoadedFromFile = 0;
int apdGeneratedFresh = 0;
std::chrono::steady_clock::time_point lastReportTime = std::chrono::steady_clock::now();
} cacheStats;
// Helper function to print stats periodically
static void MaybePrintCacheStats() {
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - cacheStats.lastReportTime).count() >=
CACHE_STATS_FREQUENCY_SECONDS_) {
printf("Thread cache stats: %d persistent hits, %d persistent misses, %d local hits, "
"%d local misses, %d shared accesses, %d eviction events, "
"%d APD loaded from file, %d APD generated fresh\n",
cacheStats.persistentHits,
cacheStats.persistentMisses,
cacheStats.localHits,
cacheStats.localMisses,
cacheStats.sharedAccesses,
cacheStats.evictionEvents,
cacheStats.apdLoadedFromFile,
cacheStats.apdGeneratedFresh);
cacheStats.lastReportTime = now;
}
}
#endif
class BadHashException : public std::exception {
class BadHashException final : public std::exception {
public:
BadHashException() = default;
[[nodiscard]] auto what() const noexcept -> const char* override { return "Bad map hash!"; };
};
constexpr int kBattalionTypeCount = 6;
// Helper function to check if any ice is present on the map
static auto HasIceOnMap(const HexMap* map) -> bool {
return std::ranges::any_of(*map->terrain(), [](const auto* terrain) {
return terrain->modifier().ice().present();
});
}
ActionPointDistancesCache::ActionPointDistancesCache() {
bravingDistances.resize(kBattalionTypeCount);
noBravingDistances.resize(kBattalionTypeCount);
// Helper function to create a copy of the map with all ice removed
// This ensures AI pathfinding treats ice as impassable water
// This should only be called if ice is present on the map
static auto CreateIceClearedMap(const HexMap* map) -> fb::HexMapW {
using namespace flatbuffers;
using namespace net::eagle0::shardok::storage::fb;
// First, create a full copy using the efficient memcpy approach
auto mapCopy = fb::CopyHexMap(map);
// Now modify the ice on the mutable copy
auto* mutableMap = mapCopy.Get();
const auto* terrainVec = mutableMap->mutable_terrain();
for (size_t i = 0; i < terrainVec->size(); i++) {
// Only process tiles with ice
if (auto* terrain = terrainVec->GetMutableObject(i); terrain->modifier().ice().present()) {
terrain->mutable_modifier().mutable_ice().mutate_present(false);
terrain->mutable_modifier().mutable_ice().mutate_integrity(0.0f);
}
}
// Recompute the modifier hash using the canonical function
// This ensures consistency with the standard hash computation
mutableMap->mutate_modifier_hash(GetModifierHash(mutableMap));
return mapCopy;
}
auto ActionPointDistancesCache::MakeCacheKey(
const MapId& mapId,
const BattalionTypeSPtr& battalionType,
bool includeBravingWater,
int braveWaterActionPointCost) -> FullCacheKey {
const bool includeBravingWater,
const int braveWaterActionPointCost) -> FullCacheKey {
return FullCacheKey{
mapId,
static_cast<int>(battalionType->typeId),
@@ -58,6 +116,13 @@ auto ActionPointDistancesCache::GetMapId(const HexMap* map) -> MapId {
return MapId{.terrainTypesId = map->base_hash(), .modifierId = modifierId};
}
void ActionPointDistancesCache::ConsolidateThreadLocalCache_Racy() {
persistentCache.insert(std::begin(sharedDistances), std::end(sharedDistances));
sharedDistances.clear();
// Clear the current thread's cache since persistent cache now has everything
tlsCache.clear();
}
auto ActionPointDistancesCache::GetRaw(
const HexMap* map,
@@ -65,40 +130,96 @@ auto ActionPointDistancesCache::GetRaw(
const BattalionTypeSPtr& battalionType,
const bool includeBravingWater,
const int braveWaterActionPointCost) -> const ActionPointDistances* {
// Create cache key using helper method
// Create cache key first - check cache before expensive ice-clearing operation
auto cacheKey =
MakeCacheKey(mapId, battalionType, includeBravingWater, braveWaterActionPointCost);
// Check the persistent map first
if (persistentCache.contains(cacheKey)) {
#if CACHE_STATS_LOGGING_
cacheStats.persistentHits++;
MaybePrintCacheStats();
#endif
// Return directly from persistent cache without TLS insertion
// This avoids the overhead of thread-local storage operations on hot path
return persistentCache.at(cacheKey).rawPtr;
}
#if CACHE_STATS_LOGGING_
cacheStats.persistentMisses++;
#endif
// Check thread-local cache first (no locks needed!)
auto localIt = tlsCache.find(cacheKey);
if (localIt != tlsCache.end()) {
if (tlsCache.contains(cacheKey)) {
#if CACHE_STATS_LOGGING_
cacheStats.localHits++;
// Print stats every 100 requests to monitor effectiveness
if ((cacheStats.localHits + cacheStats.localMisses) % 100 == 0) {
printf("Thread cache stats: %d local hits, %d misses, %d shared accesses, %d eviction "
"events\n",
cacheStats.localHits,
cacheStats.localMisses,
cacheStats.sharedAccesses,
cacheStats.evictionEvents);
}
MaybePrintCacheStats();
#endif
return localIt->second.rawPtr; // Raw pointer - zero overhead access!
return tlsCache.at(cacheKey).rawPtr; // Raw pointer - zero overhead access!
}
#if CACHE_STATS_LOGGING_
cacheStats.localMisses++;
#endif
// Thread-local cache miss - access shared cache
auto result = GetFromSharedCache(
map,
mapId,
// Check shared cache before expensive ice-clearing operation
shared_ptr<ActionPointDistances> sharedResult;
if (sharedDistances.if_contains(cacheKey, [&sharedResult](const auto& kv) {
sharedResult = kv.second;
})) {
#if CACHE_STATS_LOGGING_
cacheStats.sharedAccesses++;
#endif
// Cache hit in shared cache - store in thread-local cache and return
tlsCache.emplace(cacheKey, CacheEntry(sharedResult));
#if CACHE_STATS_LOGGING_
MaybePrintCacheStats();
#endif
return sharedResult.get();
}
// Cache miss in both caches - need to create ice-cleared map for pathfinding computation
const bool hasIce = HasIceOnMap(map);
// 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
iceClearedMap = CreateIceClearedMap(map);
mapToUse = iceClearedMap.Get();
}
// Create new pathfinding result using factory method
auto creationResult = FixedActionPointDistances::Create(
mapToUse,
mapId.terrainTypesId,
mapId.modifierId,
battalionType,
includeBravingWater,
braveWaterActionPointCost);
#if CACHE_STATS_LOGGING_
// Track whether this was loaded from file or generated fresh
if (creationResult.loadedFromFile) {
cacheStats.apdLoadedFromFile++;
} else {
cacheStats.apdGeneratedFresh++;
}
#endif
auto result = creationResult.apd;
// Store in shared cache
sharedDistances.lazy_emplace_l(
cacheKey,
[](const auto& /*kv*/) { /* already checked above */ },
[=](const auto& ctor) { ctor(cacheKey, result); });
// Cache result locally for future lookups by this thread
// Store both shared_ptr and raw pointer for hybrid access
tlsCache.emplace(cacheKey, CacheEntry(result));
@@ -118,45 +239,6 @@ auto ActionPointDistancesCache::GetRaw(
return result.get();
}
auto ActionPointDistancesCache::GetFromSharedCache(
const HexMap* map,
const MapId& mapId,
const BattalionTypeSPtr& battalionType,
const bool includeBravingWater,
const int braveWaterActionPointCost) -> std::shared_ptr<ActionPointDistances> {
#if CACHE_STATS_LOGGING_
cacheStats.sharedAccesses++;
#endif
auto& vec = includeBravingWater ? bravingDistances : noBravingDistances;
auto& distancesMap = vec[battalionType->typeId];
shared_ptr<ActionPointDistances> toReturn;
// Try shared read lock first (multiple threads can read simultaneously)
if (distancesMap.if_contains(mapId, [&toReturn](const auto& kv) { toReturn = kv.second; })) {
return toReturn;
}
// Cache miss - need to create new entry with exclusive access
distancesMap.lazy_emplace_l(
mapId,
[&toReturn](const auto& kv) { toReturn = kv.second; },
[=, &toReturn](const auto& ctor) {
auto newDistances = std::make_shared<FixedActionPointDistances>(
map,
mapId.terrainTypesId,
mapId.modifierId,
battalionType,
includeBravingWater,
braveWaterActionPointCost);
ctor(mapId, newDistances);
toReturn = newDistances;
});
return toReturn;
}
void ActionPointDistancesCache::ClearThreadLocalCache() { tlsCache.clear(); }
size_t ActionPointDistancesCache::GetThreadLocalCacheSize() { return tlsCache.size(); }
@@ -20,21 +20,15 @@ namespace shardok {
using std::shared_ptr;
struct MapId {
int64_t terrainTypesId;
int64_t modifierId;
friend size_t hash_value(const MapId& id) {
return gtl::HashState::combine(0, id.terrainTypesId, id.modifierId);
}
uint64_t terrainTypesId;
uint64_t modifierId;
auto operator==(const MapId& other) const -> bool {
return terrainTypesId == other.terrainTypesId && modifierId == other.modifierId;
}
};
using APDKey = MapId;
// Extended key for thread-local cache that includes battalion type
// Unified cache key for both thread-safe and thread-local caches
struct FullCacheKey {
MapId mapId;
int battalionTypeId;
@@ -51,41 +45,48 @@ struct FullCacheKey {
// Hash function for FullCacheKey
struct FullCacheKeyHash {
size_t operator()(const FullCacheKey& key) const {
return gtl::HashState::combine(
hash_value(key.mapId),
key.battalionTypeId,
key.includeBravingWater,
key.braveWaterCost);
// Pack small fields into a single 64-bit value
uint64_t packed = (static_cast<uint64_t>(key.battalionTypeId) << 32) |
(static_cast<uint64_t>(key.braveWaterCost) << 1) |
(key.includeBravingWater ? 1 : 0);
// Hash MapId fields directly instead of going through hash_value(MapId)
return gtl::HashState::combine(0, key.mapId.terrainTypesId, key.mapId.modifierId, packed);
}
};
class ActionPointDistancesCache {
private:
using APDMap = gtl::parallel_flat_hash_map<
APDKey,
shared_ptr<ActionPointDistances>,
gtl::priv::hash_default_hash<APDKey>,
gtl::priv::hash_default_eq<APDKey>,
std::allocator<std::pair<const APDKey, shared_ptr<ActionPointDistances>>>,
6,
std::mutex>;
vector<APDMap> noBravingDistances;
vector<APDMap> bravingDistances;
// Thread-local cache storing both shared_ptr and raw pointer for hybrid access
// Lifetime guaranteed by shared cache ownership
struct CacheEntry {
shared_ptr<ActionPointDistances> sharedPtr;
const ActionPointDistances* rawPtr;
CacheEntry(shared_ptr<ActionPointDistances> ptr)
explicit CacheEntry(shared_ptr<ActionPointDistances> ptr)
: sharedPtr(std::move(ptr)),
rawPtr(sharedPtr.get()) {}
};
using TLSCache = std::unordered_map<FullCacheKey, CacheEntry, FullCacheKeyHash>;
// Tier 1: persistent map. This is NOT safe to write to while reads may be happening.
using PersistentMap = gtl::flat_hash_map<FullCacheKey, CacheEntry, FullCacheKeyHash>;
PersistentMap persistentCache;
using APDMap = gtl::parallel_flat_hash_map<
FullCacheKey,
shared_ptr<ActionPointDistances>,
FullCacheKeyHash,
std::equal_to<FullCacheKey>,
std::allocator<std::pair<const FullCacheKey, shared_ptr<ActionPointDistances>>>,
6,
std::mutex>;
APDMap sharedDistances;
using TLSCache = gtl::flat_hash_map<FullCacheKey, CacheEntry, FullCacheKeyHash>;
static thread_local TLSCache tlsCache;
// Epoch system removed - TLS cache uses size-based eviction instead
// Helper to build cache key
static auto MakeCacheKey(
const MapId& mapId,
@@ -93,16 +94,12 @@ private:
bool includeBravingWater,
int braveWaterActionPointCost) -> FullCacheKey;
// Private method for accessing shared cache with improved locking
auto GetFromSharedCache(
const HexMap* map,
const MapId& mapId,
const BattalionTypeSPtr& battalionType,
bool includeBravingWater,
int braveWaterActionPointCost) -> shared_ptr<ActionPointDistances>;
public:
explicit ActionPointDistancesCache();
explicit ActionPointDistancesCache() {
// Pre-size persistent cache to reduce hash collisions
// Estimate: ~12 entries from pre-fetching + ~50-100 entries during gameplay
persistentCache.reserve(128);
}
// Returns raw pointer for zero overhead access
// Lifetime guaranteed by shared cache ownership
@@ -115,6 +112,11 @@ public:
static auto GetMapId(const HexMap* map) -> MapId;
// Consolidate the thread-safe cache into the persistent cache and clear
// the current thread's local cache. This is only safe if we know reads
// are not happening from other threads.
void ConsolidateThreadLocalCache_Racy();
// Cache management methods
static void ClearThreadLocalCache();
static size_t GetThreadLocalCacheSize();
@@ -26,14 +26,24 @@ void FixedActionPointDistances::SetCacheDirectory(const string& newDir) {
static thread_local byte_vector _scratch;
FixedActionPointDistances::FixedActionPointDistances(
FixedActionPointDistances::FixedActionPointDistances(const HexMap* /*map*/, int columnCount)
: ActionPointDistances(columnCount) {}
auto FixedActionPointDistances::Create(
const HexMap* map,
int64_t terrainTypesHash,
int64_t modifierHash,
const BattalionTypeSPtr& battalionType,
bool includeBravingWater,
int braveWaterActionPointCost)
: ActionPointDistances(map->column_count()) {
int braveWaterActionPointCost) -> CreationResult {
// Create the object using private constructor
auto apd = std::shared_ptr<FixedActionPointDistances>(
new FixedActionPointDistances(map, map->column_count()));
CreationResult result;
result.apd = apd;
result.loadedFromFile = false;
string path = "";
if (!cacheDirectory.empty()) {
@@ -55,22 +65,26 @@ FixedActionPointDistances::FixedActionPointDistances(
const int indexCount = map->row_count() * map->column_count();
if (!path.empty() && FilesystemUtils::FileExistsAtPath(path)) {
distances.resize(indexCount);
apd->distances.resize(indexCount);
// load from file
const auto& bytes = _scratch.ReplaceWithPath(path);
const auto* ptr = reinterpret_cast<const DIST_T*>(bytes.data());
for (int fromIndex = 0; fromIndex < indexCount; fromIndex++) {
distances[fromIndex].insert(distances[fromIndex].end(), &(ptr[0]), &(ptr[indexCount]));
apd->distances[fromIndex].insert(
apd->distances[fromIndex].end(),
&(ptr[0]),
&(ptr[indexCount]));
ptr += indexCount;
}
result.loadedFromFile = true;
} else {
_scratch.reserve(indexCount * indexCount * sizeof(DIST_T));
vector<std::future<vector<vector<DIST_T>>>> futures(indexCount);
auto braveWaterPossibleCoords =
includeBravingWater ? BraveWaterPossibleCoords(map) : nullptr;
includeBravingWater ? apd->BraveWaterPossibleCoords(map) : nullptr;
int chunkSize = (indexCount + ASYNC_COUNT - 1) / ASYNC_COUNT;
// Break into chunks for async
@@ -83,7 +97,7 @@ FixedActionPointDistances::FixedActionPointDistances(
for (int i = 0; i < chunkSize; i++) {
const auto fromIndex = chunkStartIndex + i;
if (fromIndex >= indexCount) { continue; }
chunkVec.push_back(GenerateDistances(
chunkVec.push_back(ActionPointDistances::GenerateDistances(
fromIndex,
map,
includeBravingWater,
@@ -95,18 +109,20 @@ FixedActionPointDistances::FixedActionPointDistances(
});
}
distances.reserve(indexCount);
apd->distances.reserve(indexCount);
_scratch.clear();
_scratch.reserve(indexCount * indexCount * sizeof(DIST_T));
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
auto resultsVec = futures[chunkIdx].get();
distances.insert(distances.end(), resultsVec.begin(), resultsVec.end());
apd->distances.insert(apd->distances.end(), resultsVec.begin(), resultsVec.end());
for (const auto& r : resultsVec) { _scratch.append(r); }
}
if (!path.empty()) { FilesystemUtils::AtomicallySaveToPath(path, _scratch); }
}
return result;
}
} // namespace shardok
} // namespace shardok
@@ -17,31 +17,43 @@ using std::vector;
using BattalionTypeSPtr = std::shared_ptr<const BattalionType>;
class FixedActionPointDistances final : public ActionPointDistances {
public:
struct CreationResult {
std::shared_ptr<FixedActionPointDistances> apd;
bool loadedFromFile;
};
private:
vector<vector<DIST_T>> distances;
inline static string cacheDirectory = "";
// Private constructor - use Create factory method instead
explicit FixedActionPointDistances(const HexMap *map, int columnCount);
public:
static void SetCacheDirectory(const string &newDir);
explicit FixedActionPointDistances(
// Factory method to create FixedActionPointDistances with metadata
static auto Create(
const HexMap *map,
int64_t terrainTypesHash,
int64_t modifierHash,
const BattalionTypeSPtr &battalionType,
bool includeBravingWater,
int braveWaterActionPointCost = -1);
int braveWaterActionPointCost = -1) -> CreationResult;
~FixedActionPointDistances() override = default;
auto Distance(const int fromIndex, const int toIndex) const -> DIST_T override {
[[nodiscard]] auto Distance(const int fromIndex, const int toIndex) const -> DIST_T override {
return distances[fromIndex][toIndex];
}
auto Distance(const Coords &from, const Coords &to) const -> DIST_T override {
[[nodiscard]] auto Distance(const Coords &from, const Coords &to) const -> DIST_T override {
return Distance(ToIndex(from), ToIndex(to));
}
friend struct CreationResult;
};
} // namespace shardok
@@ -130,8 +130,8 @@ void ApplyResolvedUnit(
}
}
common::FilterInPlace(inoutState.units, [unitId](const auto &unit) {
return unit.unit_id() != unitId;
std::erase_if(inoutState.units, [unitId](const auto &unit) {
return unit.unit_id() == unitId;
});
inoutState.units[unitId] = *((Unit *)resolvedUnit.unit_bytes().data());
inoutState.units[unitId].mutate_status(
@@ -153,28 +153,60 @@ auto ApplyResults(
void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result) {
UnitId maxChangedUnitId = 0;
bool needsVectorExpansion = false;
bool needsReservedSlotConversion = false;
// First pass: check what kind of modifications we need
for (const auto &unitBytes : result.changed_units_fb()) {
const auto *unit = (Unit *)unitBytes.data();
maxChangedUnitId = std::max(maxChangedUnitId, unit->unit_id());
if (static_cast<unsigned int>(unit->unit_id()) >= mutatingState->units()->size()) {
// Unit ID beyond vector size - must expand
needsVectorExpansion = true;
break; // No point checking further
} else if (
mutatingState->units()->Get(unit->unit_id())->status() ==
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
// Unit wants to use a reserved slot
needsReservedSlotConversion = true;
}
}
if (maxChangedUnitId < mutatingState->units()->size()) {
// Early return if no modifications needed
if (!needsVectorExpansion && !needsReservedSlotConversion) { return; }
// If we need to expand the vector, go straight to slow path
if (needsVectorExpansion) {
int unitsNeeded = 1 + maxChangedUnitId - mutatingState->units()->size();
mutatingState = CopyWithExtraUnits(mutatingState, unitsNeeded);
return;
} else {
mutatingState = CopyWithExtraUnits(
mutatingState,
1 + maxChangedUnitId - mutatingState->units()->size());
}
// Otherwise, we just need to convert reserved slots (fast path)
if (needsReservedSlotConversion) {
// Convert reserved slots to real units in place
// We only need to process the units that are being changed
for (const auto &unitBytes : result.changed_units_fb()) {
const auto *unit = (Unit *)unitBytes.data();
auto *mutableUnit = mutatingState->mutable_units()->GetMutableObject(unit->unit_id());
if (mutableUnit->status() ==
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
// Convert this reserved slot to a real unit
mutableUnit->mutate_status(
net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
// The calling code will set the specific values it needs
}
}
}
}
auto ApplyResult(
const GameStateW &startingState,
GameStateW startingState,
const ActionResultProto &result,
const SettingsGetter &settings) -> GameStateW {
auto endGS = startingState;
MutatingApplyResult(endGS, result, settings);
return endGS;
MutatingApplyResult(startingState, result, settings);
return startingState;
}
void MutatingApplyResult(
@@ -306,10 +338,18 @@ void MutatingApplyResult(
settings);
}
fb::ApplyUnit(
mutatingGameState->units()->GetMutableObject(changedUnit->unit_id()),
changedUnit,
status);
// Capture old position before applying changes
auto *mutableUnit = mutatingGameState->units()->GetMutableObject(changedUnit->unit_id());
const auto oldLocation = mutableUnit->location();
fb::ApplyUnit(mutableUnit, changedUnit, status);
// Update occupied tiles bitfield if position changed
const auto &newLocation = changedUnit->location();
if (oldLocation.row() != newLocation.row() ||
oldLocation.column() != newLocation.column()) {
mutatingGameState.UpdateOccupiedTile(oldLocation, newLocation);
}
if (battalionSizeBefore != battalionSizeAfter) {
if (changedUnit->battalion().type() ==
@@ -11,13 +11,11 @@
#include <flatbuffers/flatbuffers.h>
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
namespace shardok {
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
using ActionResultProto = net::eagle0::shardok::storage::ActionResult;
// flatbuffers
@@ -26,7 +24,7 @@ void MutatingApplyResult(
const ActionResultProto& actionResult,
const SettingsGetter& settings);
auto ApplyResult(
const GameStateW& startingState,
GameStateW startingState,
const ActionResultProto& actionResult,
const SettingsGetter& settings) -> GameStateW;
auto ApplyResults(
@@ -12,11 +12,10 @@ cc_library(
deps = [
":game_state_copier",
":unit_helpers",
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/map:hex_map_hasher",
"//src/main/cpp/net/eagle0/shardok/library/unit",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
],
)
@@ -27,8 +26,7 @@ cc_library(
hdrs = ["GameStateCopier.hpp"],
copts = COPTS,
deps = [
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
],
)
@@ -4,6 +4,8 @@
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/GameStateCopier.hpp"
#include <cstring>
namespace shardok {
using Unit = net::eagle0::shardok::storage::fb::Unit;
@@ -14,12 +16,59 @@ auto CopyWithExtraUnits(const GameStateW& original, int additionalCount) -> Game
net::eagle0::shardok::storage::fb::GameStateT endGST;
startGS->UnPackTo(&endGST);
for (int i = 0; i < additionalCount; i++) {
// Add the requested units plus some extra slack for future use
int extraSlack = std::max(5, additionalCount * 2);
for (int i = 0; i < additionalCount + extraSlack; i++) {
Unit unit;
unit.mutate_unit_id(endGST.units.size());
unit.mutate_unit_id(static_cast<int16_t>(endGST.units.size()));
if (i < additionalCount) {
unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
} else {
unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT);
// Set safe defaults for reserved slots
unit.mutate_player_id(-1);
unit.mutate_eagle_player_id(-1);
unit.mutable_location().mutate_row(-1);
unit.mutable_location().mutate_column(-1);
}
endGST.units.push_back(unit);
}
// Copy occupied tiles bitfield from original GameState (much faster than O(n) rebuild)
if (startGS->occupied_tiles() && startGS->hex_map()) {
const size_t originalBitfieldSize = startGS->occupied_tiles()->size();
endGST.occupied_tiles.resize(originalBitfieldSize);
// Fast O(bitfield_bytes) copy instead of O(units) rebuild
std::memcpy(
endGST.occupied_tiles.data(),
startGS->occupied_tiles()->data(),
originalBitfieldSize);
} else if (endGST.hex_map) {
// Fallback: create new bitfield only if original doesn't have one
const int16_t rowCount = endGST.hex_map->row_count;
const int16_t columnCount = endGST.hex_map->column_count;
const size_t mapSize = static_cast<size_t>(rowCount) * static_cast<size_t>(columnCount);
const size_t bitfieldSize = (mapSize + 7) / 8; // Ceiling division
endGST.occupied_tiles.resize(bitfieldSize, 0); // Initialize all bits to 0 (empty)
// Populate bitfield based on unit positions (O(n) fallback)
for (const auto& unit : endGST.units) {
if (unit.status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
const auto& location = unit.location();
if (location.row() >= 0 && location.row() < rowCount && location.column() >= 0 &&
location.column() < columnCount) {
const size_t tileIndex =
static_cast<size_t>(location.row()) * static_cast<size_t>(columnCount) +
static_cast<size_t>(location.column());
const size_t byteIndex = tileIndex / 8;
const size_t bitOffset = tileIndex % 8;
endGST.occupied_tiles[byteIndex] |= (1 << bitOffset); // Set the bit
}
}
}
}
flatbuffers::FlatBufferBuilder newFbb;
newFbb.ForceDefaults(true);
newFbb.Finish(net::eagle0::shardok::storage::fb::GameState::Pack(newFbb, &endGST));
@@ -5,13 +5,10 @@
#ifndef EAGLE0_GAMESTATECOPIER_HPP
#define EAGLE0_GAMESTATECOPIER_HPP
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
namespace shardok {
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
auto CopyWithExtraUnits(const GameStateW& original, int additionalCount) -> GameStateW;
} // namespace shardok
@@ -11,7 +11,7 @@
namespace shardok {
auto DefensiveAmbushAction::InternalExecute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
const auto results = CombatUtils::InternalPerformMelee(
ActionCost(ActionCost::standard, 0),
currentState->units()->Get(ambusherId),
@@ -17,8 +17,10 @@ private:
const SettingsGetter settings;
protected:
auto InternalExecute(const GameStateW& currentState, std::shared_ptr<RandomGenerator> generator)
const -> vector<ActionResult> override;
auto InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
public:
DefensiveAmbushAction(
@@ -15,7 +15,7 @@ using net::eagle0::shardok::common::GameStatus;
auto EndPlayerSetupCommand::InternalExecute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResultProto> {
auto startingGameState = gameState;
ActionResultProto endResult{};
@@ -8,15 +8,12 @@
#include <optional>
#include <utility>
#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/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
namespace shardok {
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
class EndPlayerSetupCommand : public ShardokCommand {
private:
const PlayerId nextPid;
@@ -26,7 +23,8 @@ private:
protected:
[[nodiscard]] auto InternalExecute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
public:
explicit EndPlayerSetupCommand(
@@ -101,7 +101,7 @@ FallIntoWaterAction::FallIntoWaterAction(
auto FallIntoWaterAction::InternalExecute(
const GameStateW &currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
vector<ActionResult> results{};
auto fallerTerrain =
@@ -150,8 +150,7 @@ auto FallIntoWaterAction::InternalExecute(
Terrain bestTerrain{};
for (const auto &adjWithTerrain : adjacentCoordsAndTerrain) {
const auto *possibleOccupant =
Occupant(currentState->units(), adjWithTerrain.adjacentCoords);
const auto *possibleOccupant = currentState.GetOccupant(adjWithTerrain.adjacentCoords);
if (possibleOccupant && (possibleOccupant->player_id() == fallerAfter.player_id() ||
!possibleOccupant->hidden())) {
continue;
@@ -172,7 +171,7 @@ auto FallIntoWaterAction::InternalExecute(
}
}
if (found && !Occupant(currentState->units(), bestCoords)) {
if (found && !currentState.GetOccupant(bestCoords)) {
PercentileRollOdds odds = EscapeChance(
baseEscapeOdds,
bestTerrain,
@@ -40,7 +40,8 @@ private:
[[nodiscard]] auto InternalExecute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const UnitId fallerId;
const SettingsGetter settings;
@@ -23,8 +23,8 @@ FireOutAction::FireOutAction(
fireOutOdds(std::move(odds)) {}
auto FireOutAction::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
const GameStateW& /*currentState*/,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const auto fireOutRoll = generator->Percentile();
ActionResult resultProto;
@@ -17,8 +17,10 @@ namespace shardok {
class FireOutAction : public ShardokAction {
private:
auto InternalExecute(const GameStateW& currentState, std::shared_ptr<RandomGenerator> generator)
const -> vector<ActionResult> override;
auto InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const Coords coords;
const net::eagle0::shardok::storage::fb::TileModifier existingModifier;
@@ -23,8 +23,8 @@ FireSpreadAction::FireSpreadAction(
fireSpreadOdds(std::move(odds)) {}
auto FireSpreadAction::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
const GameStateW& /*currentState*/,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const auto fireSpreadRoll = generator->Percentile();
if (PercentileRollSucceeds(fireSpreadOdds, fireSpreadRoll)) {
@@ -17,8 +17,10 @@ namespace shardok {
class FireSpreadAction : public ShardokAction {
private:
auto InternalExecute(const GameStateW& currentState, std::shared_ptr<RandomGenerator> generator)
const -> vector<ActionResult> override;
auto InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const Coords coords;
const net::eagle0::shardok::storage::fb::TileModifier existingModifier;
@@ -41,7 +41,7 @@ auto effectiveIce(const TerrainProto& terr) -> double {
auto IceAdjustmentAction::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
vector<ActionResult> results{};
TerrainProto newTerrain{};
@@ -19,7 +19,8 @@ class IceAdjustmentAction : public ShardokAction {
private:
[[nodiscard]] auto InternalExecute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const Coords coords;
const Terrain terrain;
@@ -33,6 +33,7 @@ auto IsResolved(const net::eagle0::shardok::storage::fb::UnitStatus status) -> b
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT: return false;
}
@@ -43,7 +44,8 @@ class MeteorUnitDamageAction : public ShardokAction {
private:
[[nodiscard]] auto InternalExecute(
const GameStateW &currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> override;
const std::shared_ptr<RandomGenerator> &generator) const
-> vector<ActionResultProto> override;
const SettingsGetter settings;
const double attackerIntelligence;
@@ -69,8 +71,8 @@ public:
};
auto MeteorUnitDamageAction::InternalExecute(
const GameStateW &currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
const GameStateW & /*currentState*/,
const std::shared_ptr<RandomGenerator> & /*generator*/) const -> vector<ActionResultProto> {
CombatDamage attackerDamage =
CombatDamage::Builder()
.SetFire(attackerIntelligence * baseDamage * damageMultiplier)
@@ -97,8 +99,10 @@ auto MeteorUnitDamageAction::InternalExecute(
class MeteorTileDamageAction : public ShardokAction {
private:
auto InternalExecute(const GameStateW &currentState, std::shared_ptr<RandomGenerator> generator)
const -> vector<ActionResultProto> override;
auto InternalExecute(
const GameStateW &currentState,
const std::shared_ptr<RandomGenerator> &generator) const
-> vector<ActionResultProto> override;
const Terrain *terrain;
const Coords coords;
@@ -127,8 +131,8 @@ public:
};
auto MeteorTileDamageAction::InternalExecute(
const GameStateW &currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
const GameStateW & /*currentState*/,
const std::shared_ptr<RandomGenerator> & /*generator*/) const -> vector<ActionResultProto> {
auto tm = fb::ToTileModifierProto(terrain->modifier());
MutatingAdjustBridgeIntegrity(&tm, integrityAdjustment);
@@ -150,8 +154,8 @@ auto MeteorTileDamageAction::InternalExecute(
}
auto MeteorCastAction::InternalExecute(
const GameStateW &currentState,
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
const GameStateW & /*currentState*/,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
vector<ActionResultProto> allResults{};
auto runningGameState = startingGameState;
for (const UnitId &actorId : actorIds) {
@@ -167,7 +171,7 @@ auto MeteorCastAction::PerformOneActorCast(
vector<ActionResultProto> &results,
const Unit *actorBefore,
GameStateW &runningGameState,
std::shared_ptr<RandomGenerator> generator) const -> GameStateW {
const std::shared_ptr<RandomGenerator> &generator) const -> GameStateW {
const double actorIntelligence = actorBefore->attached_hero().wisdom();
ActionResultProto mainResult{};
@@ -179,7 +183,7 @@ auto MeteorCastAction::PerformOneActorCast(
results.push_back(mainResult);
const Coords target = actorBefore->attached_hero().profession_info().cast_target();
const Unit *possibleOccupant = Occupant(runningGameState->units(), target);
const Unit *possibleOccupant = runningGameState.GetOccupant(target);
const Terrain *targetTerrain = GetTerrain(startingGameState->hex_map(), target);
if (possibleOccupant) {
// Direct damage action
@@ -244,7 +248,7 @@ auto MeteorCastAction::PerformOneActorCast(
for (const Coords &splashCoords : adjacentCoords) {
const auto &splashTerrain = GetTerrain(runningGameState->hex_map(), splashCoords);
const Unit *splashOccupant = Occupant(runningGameState->units(), splashCoords);
const Unit *splashOccupant = runningGameState.GetOccupant(splashCoords);
if (splashOccupant) {
MeteorUnitDamageAction splashUnitDamageAction(
settings,
@@ -297,7 +301,7 @@ auto MeteorCastAction::PerformOneActorCast(
// Check for fallen heroes
for (const Coords &coords : destroyedBridgeOrIceTiles) {
const auto *maybeOccupant = Occupant(runningGameState->units(), coords);
const auto *maybeOccupant = runningGameState.GetOccupant(coords);
if (maybeOccupant) {
const BattalionTypeSPtr &battalionType =
settings.GetBattalionType(maybeOccupant->battalion().type());
@@ -12,15 +12,12 @@
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/ShardokAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
namespace shardok {
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
using Unit = net::eagle0::shardok::storage::fb::Unit;
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
class MeteorCastAction : public ShardokAction {
private:
@@ -29,11 +26,12 @@ private:
vector<ActionResult>& results,
const Unit* actorBefore,
GameStateW& runningGameState,
std::shared_ptr<RandomGenerator> generator) const -> GameStateW;
const std::shared_ptr<RandomGenerator>& generator) const -> GameStateW;
[[nodiscard]] auto InternalExecute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const vector<UnitId> actorIds;
const GameStateW startingGameState;
@@ -97,7 +97,7 @@ auto BurnStructuresResult(const GameStateW &gameState, const SettingsGetter &set
auto NewWeather(
const net::eagle0::shardok::storage::fb::MonthlyWeather &monthlyWeather,
const WeatherFb &oldWeather,
const SettingsGetter &settings,
const SettingsGetter & /*settings*/,
const std::shared_ptr<RandomGenerator> &randomGenerator) -> Weather {
const Weather::Conditions newConditions =
ConditionsByMonth(monthlyWeather, randomGenerator->Percentile());
@@ -135,7 +135,7 @@ auto NewWeather(
auto NewRoundAction::InternalExecute(
const GameStateW &currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
vector<ActionResultProto> results{};
GameStateW runningGameState = startingGameState;
@@ -11,25 +11,24 @@
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/FireOutActionFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/FireSpreadActionFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/IceAndSnowAdjustmentActionFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/UndeadChangeActionFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map.hpp"
namespace shardok {
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
class NewRoundAction : public ShardokAction {
private:
[[nodiscard]] auto InternalExecute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const GameStateW startingGameState;
const SettingsGetter settings;
@@ -21,8 +21,8 @@ auto ChooseUndeadCommand(
const std::shared_ptr<RandomGenerator> &randomGenerator) -> CommandSPtr;
auto PerformUndeadCommandsAction::InternalExecute(
const GameStateW &currentState,
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
const GameStateW & /*currentState*/,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
GameStateW runningGameState = startingGameState;
vector<ActionResultProto> allResults{};
@@ -13,13 +13,12 @@
namespace shardok {
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
class PerformUndeadCommandsAction : public ShardokAction {
private:
[[nodiscard]] auto InternalExecute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const GameStateW& startingGameState;
const SettingsGetter settings;
@@ -14,7 +14,7 @@ using net::eagle0::shardok::common::GameStatus;
[[nodiscard]] auto PlaceHiddenUnitCommand::InternalExecute(
const GameStateW& currentState,
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
auto actorAfter = *currentState->units()->Get(actorId);
actorAfter.mutable_location() = target;
actorAfter.mutate_hidden(true);

Some files were not shown because too many files have changed in this diff Show More