Compare commits

..
54 Commits
Author SHA1 Message Date
adminandClaude dda962679f Add transposition pruning optimization
This optimization detects duplicate game states in the current search path
and prunes redundant branches, preventing infinite loops and reducing
redundant computation while maintaining correctness.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 22:10:02 -07:00
adminandGitHub 5e265c4845 fix a crasher if the SuppressBeasts succeeds but the battalion is destroyed (#4354) 2025-08-22 21:45:43 -07:00
adminandGitHub e2720911c9 cache GameState scores (#4344)
* cache GameState scores

* fix

* more infinite recursion checks

* fix the bug and improve logging

* small fixes

* rename

* null checks etc

* fix the build

* no change

* remove the null checks

* fix the build

* fix from comment
2025-08-22 17:45:54 -07:00
adminandGitHub 1b731c2080 oops (#4352) 2025-08-22 17:45:43 -07:00
54c7ae4a10 Add deadline parameter to AIScoreCalculator::CommandScore (#4351)
Pipes deadline through all AI scoring functions to enable timeout handling:
- Add deadline parameter to CommandScore, CalcOne, BestCommandIndex, EvaluateCommand, BasicLookaheadCalculator
- Add deadline checking in CalcOne to return early if timeout exceeded
- Update IterativeDeepeningAI to compute deadline from time budget
- No ThreadPool changes - uses original async/deferred approach

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-22 17:18:44 -07:00
adminandGitHub ca5c67158d Revert "Pipe deadline to AIScoreCalculator and use the thread pool (#4340)" (#4350)
This reverts commit 3b25ba3f97.
2025-08-22 16:57:33 -07:00
adminandGitHub 06835671a6 Revert "don't use a sentinel value (#4341)" (#4349)
This reverts commit a542361ae5.
2025-08-22 16:55:55 -07:00
adminandGitHub 563fd07036 Revert "add some metrics to the threadpool and use thread pools for lower dep…" (#4348)
This reverts commit f896d2d517.
2025-08-22 16:54:37 -07:00
adminandGitHub 427e284ac8 Revert "just use a queue (#4343)" (#4347)
This reverts commit c59aecf0b8.
2025-08-22 16:52:52 -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
232 changed files with 5214 additions and 2417 deletions
+10
View File
@@ -84,6 +84,16 @@ find . -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i
find . -name "*.cs" | xargs clang-format -i
```
### Static Analysis
```bash
# Run clang-tidy static analysis on C++ files
# Note: This may show some header include errors but will still analyze the main file
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' <file_path> -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++20
# Example for AI files:
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' /Users/dancrosby/CodingProjects/github/eagle0/src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.cpp -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++20
```
## Language-Specific Patterns
**Scala (Strategic Layer):**
+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": {},
+177
View File
@@ -0,0 +1,177 @@
# Transposition Table Pruning Optimization
## Overview
This document describes an optimization to prune duplicate game states during AI search, preventing redundant exploration of positions we've already seen in the current search path.
## The Problem
Currently, when the AI encounters the same game state through different move sequences (a transposition), it may explore the same subtree multiple times. This wastes computational resources.
## The Solution
Implement **transposition pruning** - when we encounter a game state that's already in our current search path, we immediately return without further exploration.
## Implementation Plan
### 1. Add Path Tracking
We need to track which game states are currently being explored in the search tree:
```cpp
// Add to AIScoreCalculator.cpp
thread_local std::unordered_set<uint64_t> t_currentSearchPath;
// RAII helper to manage path tracking
class SearchPathGuard {
uint64_t hash;
bool added;
public:
SearchPathGuard(uint64_t h) : hash(h), added(false) {
auto [_, inserted] = t_currentSearchPath.insert(hash);
added = inserted;
}
~SearchPathGuard() {
if (added) {
t_currentSearchPath.erase(hash);
}
}
bool wasAlreadyInPath() const { return !added; }
};
```
### 2. Modify BasicLookaheadCalculator
Add duplicate detection at the start of the function:
```cpp
auto BasicLookaheadCalculator(...) {
// Get hash of current state
uint64_t stateHash = hashGameState(innerEngine->GetCurrentGameState());
// Check if we're already exploring this state
SearchPathGuard pathGuard(stateHash);
if (pathGuard.wasAlreadyInPath()) {
// State is already being explored - return neutral value
std::promise<ScoreValue> p;
p.set_value(0.0); // Or return current evaluation
return p.get_future();
}
// Continue with existing transposition table check...
auto cachedScore = g_transpositionTable.probe(...);
// ... rest of function
}
```
### 3. Modify CalcOne
Similar check when creating new engine states:
```cpp
auto CalcOne(...) {
auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
// Check for duplicate state after applying move
uint64_t stateHash = hashGameState(innerEngine->GetCurrentGameState());
if (t_currentSearchPath.count(stateHash) > 0) {
// This move leads to a state we're already exploring
std::promise<ScoreValue> p;
p.set_value(AIScoreCalculator::GuessedStateScore(...)); // Return static eval
returnValue.lookaheadScore = p.get_future();
return returnValue;
}
// Continue with normal evaluation...
}
```
### 4. Add Metrics
Track how often pruning occurs:
```cpp
struct PruningStats {
std::atomic<uint64_t> duplicatesDetected{0};
std::atomic<uint64_t> branchesPruned{0};
std::atomic<uint64_t> nodesExplored{0};
void print() const {
printf("Pruning Stats: %lu duplicates, %lu pruned, %.2f%% pruning rate\n",
duplicatesDetected.load(), branchesPruned.load(),
100.0 * branchesPruned.load() / nodesExplored.load());
}
};
static PruningStats g_pruningStats;
```
## Benefits
1. **Reduced Computation**: Avoid exploring identical positions multiple times
2. **Better Depth**: Can search deeper with the same time budget
3. **Cache Efficiency**: Better use of transposition table space
4. **Deterministic Results**: More consistent evaluations
## Potential Issues
1. **Hash Collisions**: Need robust hashing to avoid false positives
2. **Thread Safety**: Path tracking must be thread-local
3. **Memory Usage**: Set of visited states grows with search depth
4. **Evaluation Consistency**: Need to handle different depths appropriately
## Alternative Approaches
### Option 1: Store "In Progress" Flag in Transposition Table
Instead of a separate set, mark entries in the transposition table as "currently being explored":
```cpp
struct TTEntry {
// ... existing fields ...
std::atomic<bool> in_progress; // Flag for current exploration
std::atomic<std::thread::id> exploring_thread; // Which thread is exploring
};
```
### Option 2: Depth-Limited Path Tracking
Only track states from the last N moves to limit memory usage:
```cpp
thread_local std::deque<uint64_t> t_recentStates;
constexpr size_t MAX_PATH_HISTORY = 10;
```
### Option 3: Bloom Filter for Approximate Detection
Use a Bloom filter for memory-efficient approximate duplicate detection:
```cpp
class BloomFilter {
std::bitset<65536> filter;
// Multiple hash functions for low false positive rate
};
```
## Testing Strategy
1. **Correctness Tests**:
- Verify same evaluation with and without pruning
- Test with known transposition-heavy positions
- Check thread safety with concurrent searches
2. **Performance Tests**:
- Measure nodes explored with/without pruning
- Time to depth comparisons
- Memory usage monitoring
3. **Regression Tests**:
- Ensure no degradation in playing strength
- Verify deterministic behavior
## Implementation Priority
1. **Phase 1**: Basic path tracking with thread-local set
2. **Phase 2**: Add metrics and logging
3. **Phase 3**: Optimize memory usage if needed
4. **Phase 4**: Consider more sophisticated approaches if beneficial
## Expected Impact
Based on typical game tree structures, we expect:
- 10-30% reduction in nodes explored
- 15-25% increase in achievable search depth
- Minimal memory overhead (< 1MB per thread)
- More consistent move selection in transposition-heavy positions
+90
View File
@@ -0,0 +1,90 @@
# Testing the Transposition Pruning Optimization
## What We've Implemented
The transposition pruning optimization adds the following features to AIScoreCalculator:
1. **Thread-local path tracking** (`t_currentSearchPath`) - tracks game state hashes currently being explored
2. **SearchPathGuard** - RAII class to automatically manage path insertion/removal
3. **Duplicate detection** in both `BasicLookaheadCalculator` and `CalcOne`
4. **Pruning statistics** - tracks how often duplicates are detected and branches are pruned
5. **Public API functions**:
- `resetSearchPath()` - clear path at start of search
- `printPruningStats()` - display statistics
- `resetPruningStats()` - reset counters
## How It Works
### In BasicLookaheadCalculator:
```cpp
// Check if we're already exploring this state
SearchPathGuard pathGuard(stateHash);
if (pathGuard.wasAlreadyInPath()) {
// Return current utility, increment pruning stats
return future_with_current_utility;
}
// Continue with normal transposition table check and search...
```
### In CalcOne:
```cpp
// After applying a move, check if resulting state is already being explored
if (t_currentSearchPath.count(newStateHash) > 0) {
// Return static evaluation, don't search further
return static_evaluation;
}
// Continue with normal lookahead search...
```
## Benefits
1. **Prevents infinite loops** - cycles in the game tree are detected and cut short
2. **Reduces redundant computation** - same positions aren't explored multiple times
3. **Enables deeper search** - saved time can be used for exploring new positions
4. **Maintains correctness** - returns reasonable evaluations (current/static eval) for pruned branches
## Performance Monitoring
The optimization includes comprehensive statistics:
- `duplicatesDetected` - how many times we found a duplicate state
- `branchesPruned` - how many subtrees were cut short
- `nodesExplored` - total nodes considered (for pruning rate calculation)
## Thread Safety
- Uses `thread_local` storage for path tracking, so each thread has its own search path
- No synchronization needed between threads
- Statistics use atomic counters for safe concurrent updates
## Usage
To use the optimization:
```cpp
// At start of AI search
AIScoreCalculator::resetSearchPath();
AIScoreCalculator::resetPruningStats();
// Run normal AI calculations...
auto score = AIScoreCalculator::CommandScore(...);
// At end of search
AIScoreCalculator::printPruningStats();
```
## Expected Results
In game positions with transpositions (same position reachable via different move sequences), we should see:
- Non-zero `duplicatesDetected` count
- Significant pruning rate (5-20% in complex positions)
- No change in final move selection quality
- Potentially faster search times or deeper achievable search depths
## Testing Strategy
1. **Correctness**: Run existing tests to ensure no regressions
2. **Functionality**: Create positions known to have transpositions
3. **Performance**: Measure search time and depth with/without optimization
4. **Statistics**: Verify counters increment appropriately
The optimization is conservative - it only prunes when absolutely safe (duplicate state in current path) and returns reasonable fallback evaluations.
+7 -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,13 @@ cc_library(
],
)
cc_library(
name = "thread_pool",
hdrs = ["ThreadPool.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
)
cc_library(
name = "time_utils",
hdrs = ["TimeUtils.hpp"],
+15 -4
View File
@@ -7,12 +7,23 @@
#include <cstdint>
constexpr uint64_t FNV_PRIME = 0x100000001b3;
constexpr uint64_t FNV_OFFSET_BASIS = 0xcbf29ce484222325;
// FNV-1a 64-bit constants
constexpr uint64_t FNV_PRIME = 0x00000100000001B3ULL;
constexpr uint64_t FNV_OFFSET_BASIS = 0xcbf29ce484222325ULL;
// FNV-1a algorithm: XOR first, then multiply
static inline auto MixIn(uint64_t& hash, const uint8_t byte) {
hash = hash * FNV_PRIME;
hash = hash ^ 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,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,200 @@
//
// 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 <functional>
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
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) {}
// NO implicit conversion - this was causing infinite recursion
// Use .value or .get() instead
T get() const { return value; }
bool succeeded() const { return status == TaskStatus::SUCCESS; }
bool deadlineExceeded() const { return status == TaskStatus::DEADLINE_EXCEEDED; }
};
class ThreadPool {
public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
private:
struct Task {
std::function<void()> function;
int priority;
TimePoint deadline;
bool has_deadline;
Task(std::function<void()> f, int p, TimePoint d, bool has_d)
: function(std::move(f)),
priority(p),
deadline(d),
has_deadline(has_d) {}
// Higher priority values and earlier deadlines have higher priority
bool operator<(const Task& other) const {
if (priority != other.priority) {
return priority < other.priority; // Lower priority values have lower priority in
// priority_queue
}
if (has_deadline && other.has_deadline) {
return deadline > other.deadline; // Later deadlines have lower priority
}
if (has_deadline && !other.has_deadline) {
return false; // Tasks with deadlines have higher priority
}
if (!has_deadline && other.has_deadline) {
return true; // Tasks without deadlines have lower priority
}
return false; // Equal priority, no preference
}
};
std::vector<std::thread> workers;
std::priority_queue<Task> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
std::atomic<bool> stop{false};
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, 0, 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(const_cast<Task&>(tasks.top()));
tasks.pop();
} else {
continue;
}
}
// Execute the task (deadline checking is now handled inside the task)
if (task.function) { task.function(); }
}
});
}
}
// Enqueue a task with priority only
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args, int priority = 0)
-> 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()>>(
[actualTask = std::move(actualTask)]() mutable -> result_type {
return result_type(actualTask());
});
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([task]() { (*task)(); }, priority, TimePoint{}, false);
}
condition.notify_one();
return result;
}
// Enqueue a task with priority and deadline
template<class F, class... Args>
auto enqueue_with_deadline(F&& f, Args&&... args, int priority, TimePoint deadline)
-> 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()>>(
[actualTask = std::move(actualTask), deadline]() mutable -> result_type {
if (Clock::now() > deadline) {
return result_type(return_type{}, TaskStatus::DEADLINE_EXCEEDED);
}
return result_type(actualTask());
});
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([task]() { (*task)(); }, priority, deadline, true);
}
condition.notify_one();
return result;
}
// Get current queue size (approximate, for monitoring)
size_t queue_size() const {
std::unique_lock<std::mutex> lock(const_cast<std::mutex&>(queue_mutex));
return tasks.size();
}
// Get detailed queue information for debugging
void debug_queue_state() const {
std::unique_lock<std::mutex> lock(const_cast<std::mutex&>(queue_mutex));
printf("ThreadPool: Queue size: %zu\n", tasks.size());
if (!tasks.empty()) {
// Create a copy to inspect priorities without modifying queue
auto queue_copy = tasks;
std::vector<int> priorities;
while (!queue_copy.empty()) {
priorities.push_back(queue_copy.top().priority);
queue_copy.pop();
}
printf("ThreadPool: Priorities in queue: ");
for (int p : priorities) { printf("%d ", p); }
printf("\n");
}
}
~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;
}
@@ -93,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 {
@@ -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,9 @@
#include "AIAttackGroups.hpp"
#include <cstdlib>
#include <iterator>
#include <ranges>
#include <unordered_map>
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -221,11 +224,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,
@@ -20,8 +20,8 @@ CoordsSet AICommandFilter::BuildEnemyLocations(const GameStateW& gameState, Play
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());
@@ -486,12 +486,12 @@ bool AICommandFilter::IsWastefulMovement(
}
bool AICommandFilter::IsStrategicBlunder(
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameStateW& 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
@@ -506,8 +506,8 @@ double AICommandFilter::MinDistanceToEnemyUnits(
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();
@@ -537,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();
@@ -572,8 +572,8 @@ 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++;
@@ -584,9 +584,9 @@ int AICommandFilter::CountPlayerUnits(const GameStateW& gameState, PlayerId pid)
}
bool AICommandFilter::WouldAbandonCriticalCastle(
const ShardokCommand& cmd,
PlayerId pid,
const GameStateW& 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;
@@ -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 */
@@ -6,8 +6,12 @@
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <future>
#include <unordered_set>
#include "TranspositionTable.hpp"
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
@@ -25,6 +29,117 @@
namespace shardok {
struct IndexAndScore {
size_t index;
CommandType type;
ScoreValue lookaheadScore;
ScoreValue immediateScore;
};
struct ImmediateAndLookaheadScore {
ScoreValue immediateScore;
future<ScoreValue> lookaheadScore;
};
[[nodiscard]] 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,
std::chrono::steady_clock::time_point deadline) -> std::future<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,
std::chrono::steady_clock::time_point deadline) -> std::future<IndexAndScore>;
[[nodiscard]] 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,
std::chrono::steady_clock::time_point deadline) -> ImmediateAndLookaheadScore;
[[nodiscard]] static auto DefenderScatterStrategyScoreForState(
const GameStateW &gameState,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto DefenderHoldCastlesStrategyScoreForState(
const GameStateW &gameState,
const CoordsSet &castleCoords,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto FleeStrategyScoreForState(const GameStateW &gameState, PlayerId playerId)
-> ScoreValue;
[[nodiscard]] static auto DefenderScoreForState(
const GameStateW &gameState,
const AIStrategy &defenderStrategy,
const CoordsSet &castleCoords,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto AttackerScoreForState(
const GameStateW &gameState,
const AIStrategy &attackerStrategy,
const CoordsSet &castleCoords,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
struct CommandEvaluationResult {
ScoreValue immediateScore;
future<ScoreValue> lookaheadScore;
};
[[nodiscard]] 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,
std::chrono::steady_clock::time_point deadline) -> CommandEvaluationResult;
#define LOGGING_ 0
#define PERFORMANCE_LOGGING_ 0
@@ -137,9 +252,54 @@ using Unit = fb::Unit;
static const std::vector _averageSequence = {0.5};
static const auto _averageGenerator = std::make_shared<SequenceRandomGenerator>(_averageSequence);
static auto CommandSorter(
const AIScoreCalculator::IndexAndScore &l,
const AIScoreCalculator::IndexAndScore &r) -> bool {
// Thread-local tracking of game states currently being explored in the search tree
// This prevents infinite loops and redundant exploration of transpositions
thread_local std::unordered_set<uint64_t> t_currentSearchPath;
// RAII helper to manage search path tracking
class SearchPathGuard {
uint64_t hash;
bool added;
public:
SearchPathGuard(uint64_t h) : hash(h), added(false) {
auto [_, inserted] = t_currentSearchPath.insert(hash);
added = inserted;
}
~SearchPathGuard() {
if (added) { t_currentSearchPath.erase(hash); }
}
bool wasAlreadyInPath() const { return !added; }
};
// Statistics for transposition pruning optimization
struct PruningStats {
std::atomic<uint64_t> duplicatesDetected{0};
std::atomic<uint64_t> branchesPruned{0};
std::atomic<uint64_t> nodesExplored{0};
void print() const {
uint64_t explored = nodesExplored.load();
uint64_t pruned = branchesPruned.load();
if (explored > 0) {
printf("Transposition Pruning: %llu duplicates detected, %llu branches pruned (%.2f%% "
"pruning rate)\n",
(unsigned long long)duplicatesDetected.load(),
(unsigned long long)pruned,
100.0 * pruned / explored);
}
}
void reset() {
duplicatesDetected = 0;
branchesPruned = 0;
nodesExplored = 0;
}
};
static PruningStats g_pruningStats;
static auto CommandSorter(const IndexAndScore &l, const IndexAndScore &r) -> bool {
if (l.lookaheadScore < r.lookaheadScore) return true;
if (l.lookaheadScore > r.lookaheadScore) return false;
@@ -261,12 +421,13 @@ auto AttackerUnitsScore(
const APDCache &apdCache,
const MapId &mapId) -> ScoreValue {
// Cache frequently accessed FlatBuffer fields to avoid repeated offset calculations
const auto *cachedGameState = gameState.Get();
const auto *cachedUnits = cachedGameState->units();
const auto *cachedHexMap = cachedGameState->hex_map();
const auto *gameStateRawPtr = gameState.Get();
const auto *cachedUnits = gameStateRawPtr->units();
const auto *cachedHexMap = gameStateRawPtr->hex_map();
const int16_t cachedRowCount = cachedHexMap->row_count();
const int16_t cachedColumnCount = cachedHexMap->column_count();
const int cachedCurrentRound = cachedGameState->current_round();
const int cachedCurrentRound = gameStateRawPtr->current_round();
bool isLateGame = cachedCurrentRound > 18; // Inline IsLateGame for efficiency
@@ -289,7 +450,7 @@ auto AttackerUnitsScore(
auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);
for (const Unit *unit : *cachedUnits) {
const auto *pi = PlayerInfoForPid(cachedGameState, unit->player_id());
const auto *pi = PlayerInfoForPid(gameState, unit->player_id());
if (pi == nullptr) continue;
switch (unit->status()) {
@@ -458,9 +619,7 @@ auto AttackerUnitsScore(
// If the best we can do puts us very close to the enemy, and the unit is almost
// destroyed, return a negative value; better to flee
if (unit->can_flee() && closestDistanceToEnemy < 5 &&
closestDistanceToEnemy != ActionPointDistances::IMPOSSIBLE &&
unit->battalion().size() < 10) {
if (unit->can_flee() && closestDistanceToEnemy < 5 && unit->battalion().size() < 10) {
distanceMultiplier = -1;
} else {
DIST_T closestDistanceToFriendly = 1;
@@ -510,12 +669,13 @@ auto AttackerUnitsScore(
return attackerUnitsValue - defenderUnitsValue;
}
auto AIScoreCalculator::FleeStrategyScoreForState(
const GameStateW &gameState,
const PlayerId playerId) -> ScoreValue {
auto FleeStrategyScoreForState(const GameStateW &gameState, const PlayerId playerId) -> ScoreValue {
ScoreValue scoreValue = 0.0;
for (const auto *unit : *gameState->units()) {
const auto *gameStatePtr = gameState.Get();
const auto *units = gameStatePtr->units();
for (const auto *unit : *units) {
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
if (unit->player_id() == playerId &&
@@ -532,27 +692,31 @@ auto AIScoreCalculator::FleeStrategyScoreForState(
return scoreValue;
}
auto AIScoreCalculator::DefenderScatterStrategyScoreForState(
auto DefenderScatterStrategyScoreForState(
const GameStateW &gameState,
const int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue {
if (gameState->status()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
for (const PlayerId winningPid : *gameState->status()->winning_shardok_ids()) {
const auto *gameStatePtr = gameState.Get();
const auto *status = gameStatePtr->status();
if (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
const auto *winningIds = status->winning_shardok_ids();
const auto *playerInfos = gameStatePtr->player_infos();
for (const PlayerId winningPid : *winningIds) {
if (winningPid < 0) continue;
if (gameState->player_infos()->Get(winningPid)->is_defender()) return INT_MAX;
if (playerInfos->Get(winningPid)->is_defender()) return INT_MAX;
return INT_MIN;
}
return INT_MAX;
}
if (gameState->status()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
return 0;
}
if (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) { return 0; }
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
const auto *hexMap = gameStatePtr->hex_map();
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
const auto unitsTotal = -AttackerUnitsScore(
gameState,
@@ -568,7 +732,7 @@ auto AIScoreCalculator::DefenderScatterStrategyScoreForState(
return unitsTotal;
}
auto AIScoreCalculator::DefenderHoldCastlesStrategyScoreForState(
auto DefenderHoldCastlesStrategyScoreForState(
const GameStateW &gameState,
const CoordsSet &castleCoords,
const int roundsRemaining,
@@ -608,7 +772,7 @@ auto AIScoreCalculator::DefenderHoldCastlesStrategyScoreForState(
return UNITS_BASE_MULTIPLIER * unitsMultiplier * unitsTotal + victoryConditionTotal;
}
auto AIScoreCalculator::DefenderScoreForState(
auto DefenderScoreForState(
const GameStateW &gameState,
const AIStrategy &defenderStrategy,
const CoordsSet &castleCoords,
@@ -664,7 +828,7 @@ auto AIScoreCalculator::DefenderScoreForState(
throw ShardokInternalErrorException("Escaped AIStrategy switch");
}
auto AIScoreCalculator::AttackerScoreForState(
auto AttackerScoreForState(
const GameStateW &gameState,
const AIStrategy &attackerStrategy,
const CoordsSet &castleCoords,
@@ -792,7 +956,7 @@ void PrintCommand(
printf("u%f\n", utility);
}
auto AIScoreCalculator::BasicLookaheadCalculator(
auto BasicLookaheadCalculator(
const PlayerId pid,
const bool isDefender,
const int remainingLookahead,
@@ -803,12 +967,54 @@ auto AIScoreCalculator::BasicLookaheadCalculator(
const SettingsGetter &settingsGetter,
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache) -> ScoreValue {
const ALCache &alCache,
std::chrono::steady_clock::time_point deadline) -> std::future<ScoreValue> {
g_pruningStats.nodesExplored++;
// Get hash of current state for duplicate detection
uint64_t stateHash = innerEngine->GetCurrentGameState().ComputeFNV1aHash();
// Check if we're already exploring this state in the current search path
SearchPathGuard pathGuard(stateHash);
if (pathGuard.wasAlreadyInPath()) {
// This state is already being explored higher up in the search tree
// Return the current utility to avoid infinite loops and redundant work
g_pruningStats.duplicatesDetected++;
g_pruningStats.branchesPruned++;
std::promise<ScoreValue> p;
p.set_value(currentUtility); // Return current evaluation
return p.get_future();
}
// Check transposition table before expensive computation
auto cachedScore =
g_transpositionTable.probe(innerEngine->GetCurrentGameState(), remainingLookahead, pid);
if (cachedScore.has_value()) {
// Return cached result immediately
std::promise<ScoreValue> p;
p.set_value(*cachedScore);
return p.get_future();
}
const auto nextUtility = currentUtility;
// Check if we've reached the depth limit before making recursive calls
if (remainingLookahead <= 0) {
// Store the current utility in the transposition table and return it
// Note: Store with depth 1 since depth 0 indicates an empty entry in the transposition
// table
g_transpositionTable.store(innerEngine->GetCurrentGameState(), 1, pid, nextUtility);
std::promise<ScoreValue> p;
p.set_value(nextUtility);
return p.get_future();
}
if (const CommandListSPtr nextCommands = innerEngine->GetAvailableCommandsForAIPlayer(pid);
nextCommands && !nextCommands->empty()) {
const auto [index, type, lookaheadScore, immediateScore] = BestCommandIndex(
// Get the future from BestCommandIndex without calling .get()
auto bestCommandFuture = BestCommandIndex(
pid,
isDefender,
remainingLookahead - 1,
@@ -819,18 +1025,51 @@ auto AIScoreCalculator::BasicLookaheadCalculator(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
alCache,
deadline);
if (auto &nextCommand = innerEngine->GetAvailableCommandsForAIPlayer(pid)->at(index);
nextCommand->GetCommandType() != net::eagle0::shardok::common::END_TURN_COMMAND) {
return immediateScore;
}
// Return a future that chains the best command evaluation
return std::async(
std::launch::deferred,
[bestCommandFuture = std::move(bestCommandFuture),
innerEngine,
pid,
nextUtility,
remainingLookahead]() mutable -> ScoreValue {
const auto [index, type, lookaheadScore, immediateScore] =
bestCommandFuture.get();
ScoreValue resultScore;
if (auto &nextCommand =
innerEngine->GetAvailableCommandsForAIPlayer(pid)->at(index);
nextCommand->GetCommandType() !=
net::eagle0::shardok::common::END_TURN_COMMAND) {
resultScore = immediateScore;
} else {
resultScore = nextUtility;
}
// Store in transposition table before returning
g_transpositionTable.store(
innerEngine->GetCurrentGameState(),
remainingLookahead,
pid,
resultScore);
return resultScore;
});
}
return nextUtility;
// No commands available, store and return the current utility as a future
g_transpositionTable
.store(innerEngine->GetCurrentGameState(), remainingLookahead, pid, nextUtility);
std::promise<ScoreValue> p;
p.set_value(nextUtility);
return p.get_future();
}
auto AIScoreCalculator::CalcOne(
auto CalcOne(
PlayerId pid,
bool isDefender,
uint32_t commandIndex,
@@ -842,13 +1081,47 @@ auto AIScoreCalculator::CalcOne(
const SettingsGetter &settingsGetter,
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache) -> ImmediateAndLookaheadScore {
const ALCache &alCache,
std::chrono::steady_clock::time_point deadline) -> ImmediateAndLookaheadScore {
ImmediateAndLookaheadScore returnValue{};
// Check if we've exceeded the deadline
if (std::chrono::steady_clock::now() > deadline) {
// Return with a default score and an empty future that resolves immediately
std::promise<ScoreValue> p;
p.set_value(0.0); // Default timeout score
returnValue.immediateScore = 0.0;
returnValue.lookaheadScore = p.get_future();
return returnValue;
}
auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
auto innerUtility = GuessedStateScore(
// Check if this move leads to a state we're already exploring
uint64_t newStateHash = innerEngine->GetCurrentGameState().ComputeFNV1aHash();
if (t_currentSearchPath.count(newStateHash) > 0) {
// This move creates a transposition to a state already in our search path
// Return the static evaluation to avoid redundant exploration
g_pruningStats.duplicatesDetected++;
auto staticEval = AIScoreCalculator::GuessedStateScore(
isDefender,
innerEngine->GetCurrentGameState(),
attackerStrategy,
allCastleCoords,
settingsGetter,
apdCache,
alCache);
returnValue.immediateScore = staticEval;
std::promise<ScoreValue> p;
p.set_value(staticEval);
returnValue.lookaheadScore = p.get_future();
return returnValue;
}
auto innerUtility = AIScoreCalculator::GuessedStateScore(
isDefender,
innerEngine->GetCurrentGameState(),
attackerStrategy,
@@ -874,8 +1147,9 @@ auto AIScoreCalculator::CalcOne(
&settingsGetter,
&allCastleCoords,
&apdCache,
&alCache]() -> ScoreValue {
return BasicLookaheadCalculator(
&alCache,
deadline]() -> ScoreValue {
auto lookaheadFuture = BasicLookaheadCalculator(
pid,
isDefender,
remainingLookahead,
@@ -886,11 +1160,14 @@ auto AIScoreCalculator::CalcOne(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
alCache,
deadline);
return lookaheadFuture.get();
};
#if MULTITHREAD
returnValue.lookaheadScore = std::async(std::launch::async, lookaheadLambda);
auto launchPolicy = remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
#else
std::promise<ScoreValue> p;
returnValue.lookaheadScore = p.get_future();
@@ -902,7 +1179,7 @@ auto AIScoreCalculator::CalcOne(
return returnValue;
}
[[nodiscard]] auto AIScoreCalculator::BestCommandIndex(
[[nodiscard]] auto BestCommandIndex(
const PlayerId pid,
const bool isDefender,
const int remainingLookahead,
@@ -913,7 +1190,8 @@ auto AIScoreCalculator::CalcOne(
const SettingsGetter &settingsGetter,
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache) -> IndexAndScore {
const ALCache &alCache,
std::chrono::steady_clock::time_point deadline) -> std::future<IndexAndScore> {
const CommandListSPtr guessedDescriptors = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
// Filter out obviously bad commands to reduce search space
@@ -930,12 +1208,14 @@ auto AIScoreCalculator::CalcOne(
double minDistToEnemies = std::numeric_limits<double>::max();
const auto *units = gameState->units();
for (int i = 0; i < units->size(); ++i) {
if (const auto *playerUnit = units->Get(i); playerUnit->player_id() == pid) {
for (size_t i = 0; i < units->size(); ++i) {
if (const auto *playerUnit = units->Get(static_cast<unsigned int>(i));
playerUnit->player_id() == pid) {
const auto &playerCoords = playerUnit->location();
for (int j = 0; j < units->size(); ++j) {
if (const auto *enemyUnit = units->Get(j); enemyUnit->player_id() != pid) {
for (size_t j = 0; j < units->size(); ++j) {
if (const auto *enemyUnit = units->Get(static_cast<unsigned int>(j));
enemyUnit->player_id() != pid) {
const auto &enemyCoords = enemyUnit->location();
// Proper hex distance calculation using cube coordinates
@@ -972,24 +1252,29 @@ auto AIScoreCalculator::CalcOne(
const auto commandCount = filteredIndices.size();
vector<IndexAndScore> allIndices(commandCount);
// Structure to hold all command evaluation data
struct CommandEvaluation {
size_t index;
CommandType type;
ScoreValue immediateScore;
std::vector<std::future<ScoreValue>> lookaheadFutures;
};
// Primary index is the command index; vector may contain repeated attempts
vector<vector<future<ScoreValue>>> scoreFutures(commandCount);
std::vector<CommandEvaluation> commandEvaluations(commandCount);
for (uint32_t index = 0; index < commandCount; index++) {
const auto originalIndex = filteredIndices[index];
const auto &guessedDescriptor = guessedDescriptors->at(originalIndex);
const auto guessedCommandType = guessedDescriptor->GetCommandType();
allIndices[index].index = originalIndex;
allIndices[index].type = guessedCommandType;
commandEvaluations[index].index = originalIndex;
commandEvaluations[index].type = guessedCommandType;
if (guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
std::promise<ScoreValue> p;
scoreFutures[index].push_back(p.get_future());
commandEvaluations[index].lookaheadFutures.push_back(p.get_future());
p.set_value(currentUtility);
allIndices[index].immediateScore = currentUtility;
commandEvaluations[index].immediateScore = currentUtility;
} else if (IsDeterministic(guessedCommandType)) {
auto [immediateScore, lookaheadScore] =
CalcOne(pid,
@@ -1003,10 +1288,11 @@ auto AIScoreCalculator::CalcOne(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
alCache,
deadline);
allIndices[index].immediateScore = immediateScore;
scoreFutures[index].push_back(std::move(lookaheadScore));
commandEvaluations[index].immediateScore = immediateScore;
commandEvaluations[index].lookaheadFutures.push_back(std::move(lookaheadScore));
} else if (guessedDescriptor->HasOdds()) {
const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
@@ -1026,7 +1312,8 @@ auto AIScoreCalculator::CalcOne(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
alCache,
deadline);
// second attempt uses the average of (1 - successChance) and 0 as the roll (so 30%
// chance -> rolling 15)
@@ -1043,14 +1330,15 @@ auto AIScoreCalculator::CalcOne(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
alCache,
deadline);
allIndices[index].immediateScore =
commandEvaluations[index].immediateScore =
std::lerp(failureImmediateScore, successImmediateScore, successChance);
auto successSF = successLookaheadScore.share();
auto failureSF = failureLookaheadScore.share();
scoreFutures[index].push_back(std::async(
commandEvaluations[index].lookaheadFutures.push_back(std::async(
std::launch::deferred,
[successSF, failureSF, successChance]() -> double {
return std::lerp(failureSF.get(), successSF.get(), successChance);
@@ -1074,26 +1362,47 @@ auto AIScoreCalculator::CalcOne(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
alCache,
deadline);
sum += immediateScore;
scoreFutures[index].push_back(std::move(lookaheadScore));
commandEvaluations[index].lookaheadFutures.push_back(std::move(lookaheadScore));
}
allIndices[index].immediateScore = sum / maxRepeatCount;
commandEvaluations[index].immediateScore = sum / maxRepeatCount;
}
}
for (uint32_t i = 0; i < commandCount; i++) {
const auto count = static_cast<ScoreValue>(scoreFutures[i].size());
ScoreValue total = 0.0;
for (auto &oneFuture : scoreFutures[i]) { total += oneFuture.get(); }
allIndices[i].lookaheadScore = total / count;
}
// Return a future that will wait for all evaluations and find the best one
return std::async(
std::launch::deferred,
[commandEvaluations = std::move(commandEvaluations)]() mutable -> IndexAndScore {
std::vector<IndexAndScore> allResults;
allResults.reserve(commandEvaluations.size());
return *std::ranges::max_element(allIndices, CommandSorter);
// Wait for all futures and compute final scores
for (auto &eval : commandEvaluations) {
ScoreValue totalLookaheadScore = 0.0;
for (auto &future : eval.lookaheadFutures) {
totalLookaheadScore += future.get();
}
ScoreValue avgLookaheadScore =
eval.lookaheadFutures.empty()
? eval.immediateScore
: totalLookaheadScore / eval.lookaheadFutures.size();
allResults.push_back(IndexAndScore{
.index = eval.index,
.type = eval.type,
.lookaheadScore = avgLookaheadScore,
.immediateScore = eval.immediateScore});
}
// Find the best command using the existing sorter
auto bestIt = std::ranges::max_element(allResults, CommandSorter);
return *bestIt;
});
}
auto AIScoreCalculator::EvaluateCommand(
auto EvaluateCommand(
const PlayerId pid,
const bool isDefender,
const uint32_t commandIndex,
@@ -1105,16 +1414,23 @@ auto AIScoreCalculator::EvaluateCommand(
const SettingsGetter &settingsGetter,
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache) -> CommandEvaluationResult {
const ALCache &alCache,
std::chrono::steady_clock::time_point deadline) -> CommandEvaluationResult {
const CommandListSPtr guessedDescriptors = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
if (commandIndex >= guessedDescriptors->size()) { return {currentUtility, currentUtility}; }
if (commandIndex >= guessedDescriptors->size()) {
std::promise<ScoreValue> p;
p.set_value(currentUtility);
return {currentUtility, p.get_future()};
}
const auto &guessedDescriptor = guessedDescriptors->at(commandIndex);
if (const auto guessedCommandType = guessedDescriptor->GetCommandType();
guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
return {currentUtility, currentUtility};
std::promise<ScoreValue> p;
p.set_value(currentUtility);
return {currentUtility, p.get_future()};
} else if (IsDeterministic(guessedCommandType)) {
auto [immediateScore, lookaheadScore] =
CalcOne(pid,
@@ -1128,8 +1444,9 @@ auto AIScoreCalculator::EvaluateCommand(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
return {immediateScore, lookaheadScore.get()};
alCache,
deadline);
return {immediateScore, std::move(lookaheadScore)};
} else if (guessedDescriptor->HasOdds()) {
const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
@@ -1147,7 +1464,8 @@ auto AIScoreCalculator::EvaluateCommand(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
alCache,
deadline);
// Failure attempt
auto [failureImmediateScore, failureLookaheadScore] = CalcOne(
@@ -1162,15 +1480,24 @@ auto AIScoreCalculator::EvaluateCommand(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
alCache,
deadline);
// Return weighted average of success and failure
auto successSF = successLookaheadScore.share();
auto failureSF = failureLookaheadScore.share();
return {std::lerp(failureImmediateScore, successImmediateScore, successChance),
std::lerp(failureLookaheadScore.get(), successLookaheadScore.get(), successChance)};
std::async(
std::launch::deferred,
[successSF, failureSF, successChance]() -> double {
return std::lerp(failureSF.get(), successSF.get(), successChance);
})};
} else {
// For non-deterministic commands without odds, use multiple attempts
ScoreValue totalImmediateScore = 0.0;
ScoreValue totalLookaheadScore = 0.0;
std::vector<std::future<ScoreValue>> lookaheadFutures;
lookaheadFutures.reserve(maxRepeatCount);
for (int repeatIteration = 0; repeatIteration < maxRepeatCount; repeatIteration++) {
auto sequence = std::vector{
static_cast<double>(repeatIteration) / static_cast<double>(maxRepeatCount - 1)};
@@ -1186,12 +1513,23 @@ auto AIScoreCalculator::EvaluateCommand(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
alCache,
deadline);
totalImmediateScore += immediateScore;
totalLookaheadScore += lookaheadScore.get();
lookaheadFutures.push_back(std::move(lookaheadScore));
}
return {totalImmediateScore / maxRepeatCount, totalLookaheadScore / maxRepeatCount};
// Return a future that computes the average when needed
return {totalImmediateScore / maxRepeatCount,
std::async(
std::launch::deferred,
[lookaheadFutures = std::move(lookaheadFutures),
maxRepeatCount]() mutable -> double {
ScoreValue total = 0.0;
for (auto &future : lookaheadFutures) { total += future.get(); }
return total / maxRepeatCount;
})};
}
}
@@ -1207,8 +1545,9 @@ auto AIScoreCalculator::EvaluateCommand(
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache,
const size_t commandIndex) -> ScoreValue {
const auto result = EvaluateCommand(
const size_t commandIndex,
std::chrono::steady_clock::time_point deadline) -> std::future<ScoreValue> {
auto result = EvaluateCommand(
pid,
isDefender,
commandIndex,
@@ -1220,8 +1559,15 @@ auto AIScoreCalculator::EvaluateCommand(
settingsGetter,
allCastleCoords,
apdCache,
alCache);
return result.lookaheadScore;
alCache,
deadline);
return std::move(result.lookaheadScore);
}
void AIScoreCalculator::resetSearchPath() { t_currentSearchPath.clear(); }
void AIScoreCalculator::printPruningStats() { g_pruningStats.print(); }
void AIScoreCalculator::resetPruningStats() { g_pruningStats.reset(); }
} // namespace shardok
@@ -5,6 +5,7 @@
#ifndef EAGLE0_AISCORECALCULATOR_HPP
#define EAGLE0_AISCORECALCULATOR_HPP
#include <chrono>
#include <future>
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
@@ -30,103 +31,8 @@ using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
class AIScoreCalculator {
public:
struct IndexAndScore {
size_t index;
CommandType type;
ScoreValue lookaheadScore;
ScoreValue immediateScore;
};
private:
[[nodiscard]] static auto DefenderScatterStrategyScoreForState(
const GameStateW &gameState,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto DefenderHoldCastlesStrategyScoreForState(
const GameStateW &gameState,
const CoordsSet &castleCoords,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto FleeStrategyScoreForState(
const GameStateW &gameState,
PlayerId playerId) -> ScoreValue;
[[nodiscard]] static auto DefenderScoreForState(
const GameStateW &gameState,
const AIStrategy &defenderStrategy,
const CoordsSet &castleCoords,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto AttackerScoreForState(
const GameStateW &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:
// 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 GameStateW &state,
@@ -136,19 +42,7 @@ public:
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 +55,17 @@ 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<ScoreValue>;
// Reset search path for a new search (call at start of each AI search)
static void resetSearchPath();
// Print transposition pruning statistics
static void printPruningStats();
// Reset pruning statistics
static void resetPruningStats();
};
} // 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,
@@ -31,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
};
@@ -5,6 +5,7 @@
#include "AIUnitScoreCalculator.hpp"
#include <algorithm>
#include <cstdlib>
#include "AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
@@ -98,7 +99,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 +114,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;
}
@@ -342,7 +343,8 @@ auto UnitValue(
unit->battalion().type() == net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD;
const int coordsIndex = location.row() * map->column_count() + location.column();
const auto &terrain = map->terrain()->Get(coordsIndex);
const auto *terrain = map->terrain()->Get(coordsIndex);
double castleMultiplier = 1.0;
// Only give a multiplier for being in a castle if the castle is useful, and the unit is not
// undead
@@ -358,8 +360,8 @@ auto UnitValue(
{
for (const auto adjacentCoords = HexMapUtils::GetAdjacentCoords(map, location);
const auto &c : adjacentCoords) {
if (const auto &adjTerrain = GetTerrain(map, c);
adjTerrain->modifier().fire().present()) {
if (const auto *adjTerrain = GetTerrain(map, c);
adjTerrain && adjTerrain->modifier().fire().present()) {
onFireMultiplier *= kAdjacentFireMultiplier;
}
}
@@ -414,7 +416,7 @@ auto UnitValue(
if (const auto commandingUnitId = unit->commanding_unit_id(); commandingUnitId != -1) {
const Unit *commandingUnit = nullptr;
for (const Unit *attackerUnit : attackerUnits) {
if (attackerUnit->unit_id() == commandingUnitId) {
if (attackerUnit && attackerUnit->unit_id() == commandingUnitId) {
commandingUnit = attackerUnit;
break;
}
@@ -422,7 +424,7 @@ auto UnitValue(
if (commandingUnit == nullptr) {
for (const Unit *defenderUnit : defenderUnits) {
if (defenderUnit->unit_id() == commandingUnitId) {
if (defenderUnit && defenderUnit->unit_id() == commandingUnitId) {
commandingUnit = defenderUnit;
break;
}
@@ -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;
};
@@ -11,6 +11,7 @@ cc_library(
],
deps = [
":ai_attack_locations",
":ai_flee_decision_calculator",
":ai_score_utilities",
":ai_strategy",
":ai_water_crossing_command_chooser",
@@ -70,6 +71,7 @@ cc_library(
":ai_score_utilities",
":ai_strategy",
":ai_water_crossing_calculator",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/map:coords_set",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
@@ -120,12 +122,32 @@ cc_library(
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
],
deps = [
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
],
)
cc_library(
name = "ai_flee_decision_calculator",
srcs = ["AIFleeDecisionCalculator.cpp"],
hdrs = ["AIFleeDecisionCalculator.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
],
deps = [
":ai_score_utilities",
":ai_unit_score_calculator",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
],
)
cc_library(
name = "ai_command_filter",
srcs = ["AICommandFilter.cpp"],
@@ -146,6 +168,20 @@ cc_library(
],
)
cc_library(
name = "transposition_table",
srcs = ["TranspositionTable.cpp"],
hdrs = ["TranspositionTable.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",
],
)
cc_library(
name = "ai_score_calculator",
srcs = ["AIScoreCalculator.cpp"],
@@ -160,6 +196,7 @@ cc_library(
":ai_command_filter",
":ai_unit_score_calculator",
":ai_victory_condition_score_calculator",
":transposition_table",
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_guesser",
@@ -210,6 +247,7 @@ cc_library(
":ai_attack_locations",
":ai_distance_debuf",
":ai_score_utilities",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
@@ -227,6 +265,7 @@ cc_library(
],
deps = [
":ai_minimum_distance_and_target",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:hex_map_helpers",
@@ -244,6 +283,7 @@ cc_library(
deps = [
":ai_minimum_distance_and_target",
":ai_water_crossing_calculator",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
@@ -299,6 +339,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",
@@ -11,7 +11,7 @@
#include "AIAttackerStrategySelector.hpp"
#include "AIScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/common/TimeUtils.hpp"
#include "TranspositionTable.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
namespace shardok {
@@ -43,6 +43,11 @@ auto IterativeDeepeningAI::IterativeSearch(
const auto initialBudgetMs = initialBudget.remainingBudget;
SearchResult result;
// Increment TT age for replacement strategy (new search)
g_transpositionTable.incrementAge();
// DEBUG: Clear TT to see if that's causing the suspicious depth reaching
// g_transpositionTable.clear(); // Uncomment to test without cross-search caching
if (commands.empty()) {
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
printf("ID AI: Commands are empty, returning early\n");
@@ -51,11 +56,12 @@ auto IterativeDeepeningAI::IterativeSearch(
return result;
}
// Check if we're in SET_UP phase
// Check if we're in SET_UP phase and enforce maximum depth limit
bool isSetupPhase =
(state->status()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP);
int maxDepth = isSetupPhase ? 2 : std::numeric_limits<int>::max();
// Limit depth to prevent thread pool exhaustion and keep search reasonable
size_t maxDepth = isSetupPhase ? 2 : 8;
// Calculate current utility and create engine once for all command evaluations
const auto& settingsGetter = settings->GetGetter();
@@ -76,10 +82,10 @@ 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;
EvaluationCompletionReason completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
auto completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
// Main iterative deepening loop
while ((currentDepth == 1 || !IsTimeExpired(timeBudget)) && currentDepth <= maxDepth) {
@@ -89,27 +95,37 @@ auto IterativeDeepeningAI::IterativeSearch(
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,
commands,
cmdIndex,
currentDepth,
currentDepth, // Pass current iteration depth as desired search depth
currentUtility,
timeBudget);
futures.emplace_back(cmdIndex, std::move(future));
}
// Now wait for all futures and collect results
for (auto& [cmdIndex, future] : futures) {
auto cmdResult = future.get();
// Ensure scoresByDepth[cmdIndex] has enough space
if (scoresByDepth[cmdIndex].size() <= currentDepth) {
scoresByDepth[cmdIndex].resize(currentDepth + 1);
@@ -142,13 +158,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,
@@ -175,12 +191,12 @@ auto IterativeDeepeningAI::IterativeSearch(
// 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(
@@ -204,10 +220,11 @@ auto IterativeDeepeningAI::IterativeSearch(
// 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();
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;
@@ -242,6 +259,8 @@ auto IterativeDeepeningAI::IterativeSearch(
result.availableCommandCount);
}
// Print TranspositionTable statistics
g_transpositionTable.printStats();
return result;
}
@@ -255,12 +274,12 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
const int maxRepeatCount,
const std::vector<CommandProto>& commands,
const size_t commandIndex,
const int depth,
const int desiredDepth,
const ScoreValue currentUtility,
AITimeBudget& timeBudget) const -> SearchResult {
AITimeBudget& timeBudget) const -> std::future<SearchResult> {
SearchResult result;
result.bestCommandIndex = commandIndex;
result.depthAchieved = depth;
result.depthAchieved = desiredDepth;
result.searchCompleted = true;
result.minimumDepthCompleted = true;
result.availableCommandCount = commands.size();
@@ -268,7 +287,9 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
if (commandIndex >= commands.size()) {
result.bestScore = 0.0;
return result;
std::promise<SearchResult> p;
p.set_value(result);
return p.get_future();
}
try {
@@ -276,11 +297,18 @@ 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
// Note: CommandScore expects remainingLookahead, not desiredDepth
// desiredDepth 1 = evaluate immediate (remainingLookahead 0)
// desiredDepth 2 = look 1 move ahead (remainingLookahead 1)
// desiredDepth N = look N-1 moves ahead (remainingLookahead N-1)
auto commandScoreFuture = AIScoreCalculator::CommandScore(
playerId,
isDefender,
depth,
desiredDepth - 1, // Convert desiredDepth to remainingLookahead
maxRepeatCount,
guessedEngine,
strategy,
@@ -289,11 +317,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 commandScore = 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);
@@ -310,13 +342,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);
@@ -326,11 +360,21 @@ 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) {
// Only consider commands that were evaluated at previous depth
const size_t prevDepth = currentDepth - 1;
std::ranges::sort(indices, [&](const size_t a, const size_t b) {
// Bounds check - if indices are out of range, or inner vectors are too small, treat as not
// evaluated
if (a >= scoresByDepth.size() || b >= scoresByDepth.size() ||
a >= highestDepthCompleted.size() || b >= highestDepthCompleted.size()) {
return a < b; // Maintain stable order for out-of-bounds indices
}
// Check if the scores for previous depth exist
if (highestDepthCompleted[a] >= prevDepth && highestDepthCompleted[b] >= prevDepth) {
return scoresByDepth[a][prevDepth] > scoresByDepth[b][prevDepth];
// Additional safety check for inner vector size
if (scoresByDepth[a].size() > prevDepth && scoresByDepth[b].size() > prevDepth) {
return scoresByDepth[a][prevDepth] > scoresByDepth[b][prevDepth];
}
}
// Commands not evaluated at prev depth go to the end
return highestDepthCompleted[a] >= prevDepth;
@@ -341,7 +385,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;
@@ -349,9 +393,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"
@@ -35,7 +36,7 @@ public:
struct SearchResult {
size_t bestCommandIndex;
ScoreValue bestScore;
int depthAchieved;
size_t depthAchieved;
std::chrono::milliseconds timeUsed;
bool minimumDepthCompleted;
bool searchCompleted;
@@ -67,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;
@@ -79,29 +80,29 @@ 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]] static bool IsTimeExpired(const AITimeBudget& budget);
[[nodiscard]] SearchResult SearchCommandAtDepthWithEngine(
[[nodiscard]] std::future<SearchResult> SearchCommandAtDepthWithEngine(
const ShardokEngine& guessedEngine,
const GameSettings::Getter& settingsGetter,
int maxRepeatCount,
const std::vector<CommandProto>& commands,
size_t commandIndex,
int depth,
int desiredDepth,
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"
@@ -26,7 +30,7 @@ using net::eagle0::shardok::api::GameStateView;
static constexpr bool kPerformanceLogging = true;
void ApplyUpdate(GameStateView &currentView, const ActionResultView &update) {}
void ApplyUpdate(GameStateView & /*currentView*/, const ActionResultView & /*update*/) {}
auto RoundsRemaining(const GameSettingsSPtr &settings, const GameStateView &gsv) -> int {
const int maxRounds = settings->GetGetter().Backing().max_rounds();
@@ -96,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]);
}
@@ -175,23 +179,41 @@ auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateW &guessedState,
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
if (const auto fleeCommand = std::ranges::find_if(
realAvailableCommands,
[](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
return cmd.type() == net::eagle0::shardok::common::FLEE_COMMAND;
});
fleeCommand == realAvailableCommands.end()) {
const auto fleeCommand = std::ranges::find_if(
realAvailableCommands,
[](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
return cmd.type() == net::eagle0::shardok::common::FLEE_COMMAND;
});
if (fleeCommand == realAvailableCommands.end()) {
return LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
} else {
}
// Use the flee decision calculator
const auto fleeDecision = AIFleeDecisionCalculator::EvaluateFleeVsFight(
playerId,
settings->GetGetter(),
guessedState,
realAvailableCommands,
fleeCommand,
#ifdef DEBUG_FLEE_DECISIONS
true // Enable debug logging
#else
false
#endif
);
if (fleeDecision.shouldFlee) {
CommandChoiceResults results{};
results.chosenIndex =
static_cast<size_t>(std::distance(realAvailableCommands.begin(), fleeCommand));
results.chosenIndex = fleeDecision.commandIndex;
results.availableCommandCount = realAvailableCommands.size();
results.depthAchieved = 1; // Simple heuristic choice
results.depthAchieved = 1; // Heuristic choice
results.commandCountEvaluated = 1; // Only evaluated one command type
results.completionReason =
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS; // Heuristic choice
results.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
return results;
} else {
// Fight instead of flee
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
}
}
@@ -56,6 +56,7 @@ private:
const GameSettingsSPtr& settings,
const GameStateW& guessedState,
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
[[nodiscard]] auto ChooseCommandIndex(
const GameSettingsSPtr& settings,
const net::eagle0::shardok::api::GameStateView& gsv,
@@ -0,0 +1,113 @@
//
// TranspositionTable.cpp - Implementation of game state evaluation cache
//
#include "TranspositionTable.hpp"
#include <cstdio>
#include <cstring>
namespace shardok {
// Global instance
TranspositionTable g_transpositionTable;
TranspositionTable::TranspositionTable() : table(TABLE_SIZE) {
// Initialize all entries to zero
clear();
}
uint64_t TranspositionTable::hashGameState(const GameStateW& state) const {
// The FlatBuffer is contiguous in memory and units are sorted by ID,
// so we can just hash the raw bytes for order-independent hashing
// Use ComputeFNV1aHash to avoid creating a string copy
return state.ComputeFNV1aHash();
}
std::optional<ScoreValue>
TranspositionTable::probe(const GameStateW& state, int depth, PlayerId player) {
stats.probes++;
uint64_t hash = hashGameState(state);
size_t index = hash & INDEX_MASK;
const auto& entry = table[index];
// Check if this entry matches our position using FULL hash
uint64_t stored_hash = entry.hash_full.load(std::memory_order_relaxed);
uint8_t stored_depth = entry.depth.load(std::memory_order_relaxed);
uint8_t stored_player = entry.player_id.load(std::memory_order_relaxed);
if (stored_hash == hash && stored_depth >= depth && stored_player == player) {
stats.hits++;
float score = entry.score.load(std::memory_order_relaxed);
return static_cast<ScoreValue>(score);
}
// Track collisions (different position mapped to same index)
// Note: We use depth==0 to indicate empty entries, not hash==0
if (stored_depth != 0 && stored_hash != hash) { stats.collisions++; }
return std::nullopt;
}
void TranspositionTable::store(
const GameStateW& state,
int depth,
PlayerId player,
ScoreValue score) {
stats.stores++;
uint64_t hash = hashGameState(state);
size_t index = hash & INDEX_MASK;
auto& entry = table[index];
// Simple replacement strategy: always replace if:
// 1. Entry is from an older search (different age)
// 2. New search is deeper
// 3. Entry is empty (depth == 0)
uint16_t stored_age = entry.age.load(std::memory_order_relaxed);
uint8_t stored_depth = entry.depth.load(std::memory_order_relaxed);
bool should_replace = (stored_depth == 0) || // Empty entry (depth 0 means unused)
(stored_age != current_age) || // Old entry
(depth >= stored_depth); // Deeper or equal search
if (should_replace) {
// Store all fields with relaxed ordering (TT races are benign)
entry.hash_full.store(hash, std::memory_order_relaxed);
entry.score.store(static_cast<float>(score), std::memory_order_relaxed);
entry.depth.store(static_cast<uint8_t>(depth), std::memory_order_relaxed);
entry.player_id.store(static_cast<uint8_t>(player), std::memory_order_relaxed);
entry.age.store(current_age, std::memory_order_relaxed);
}
}
void TranspositionTable::clear() {
// Reset all entries
for (auto& entry : table) {
entry.hash_full.store(0, std::memory_order_relaxed);
entry.score.store(0.0f, std::memory_order_relaxed);
entry.depth.store(0, std::memory_order_relaxed);
entry.player_id.store(0, std::memory_order_relaxed);
entry.age.store(0, std::memory_order_relaxed);
}
stats.reset();
current_age = 0;
}
void TranspositionTable::printStats() const {
printf("TranspositionTable Stats:\n");
printf(" Probes: %llu\n", stats.probes.load());
printf(" Hits: %llu (%.1f%%)\n", stats.hits.load(), stats.hitRate());
printf(" Stores: %llu\n", stats.stores.load());
printf(" Collisions: %llu\n", stats.collisions.load());
printf(" Table size: %zu entries (%.1f MB)\n",
TABLE_SIZE,
(TABLE_SIZE * sizeof(TTEntry)) / (1024.0 * 1024.0));
}
} // namespace shardok
@@ -0,0 +1,91 @@
//
// TranspositionTable.hpp - Cache for game state evaluations to avoid redundant calculations
//
#ifndef EAGLE0_TRANSPOSITIONTABLE_HPP
#define EAGLE0_TRANSPOSITIONTABLE_HPP
#include <atomic>
#include <cstdint>
#include <optional>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
namespace shardok {
using ScoreValue = double;
// PlayerId already defined in ShardokCTypes.h
class TranspositionTable {
public:
// Statistics for monitoring effectiveness
struct Stats {
std::atomic<uint64_t> probes{0};
std::atomic<uint64_t> hits{0};
std::atomic<uint64_t> stores{0};
std::atomic<uint64_t> collisions{0};
double hitRate() const {
uint64_t p = probes.load();
return p > 0 ? (100.0 * hits.load() / p) : 0.0;
}
void reset() {
probes = 0;
hits = 0;
stores = 0;
collisions = 0;
}
};
private:
// Compact entry structure (actual size is greater than 16 bytes due to atomics and alignment)
struct TTEntry {
std::atomic<uint64_t> hash_full; // Full hash for validation
std::atomic<float> score; // Score as float to save space
std::atomic<uint8_t> depth; // Search depth (0-255)
std::atomic<uint8_t> player_id; // Player who is to move
std::atomic<uint16_t> age; // For replacement strategy
};
static constexpr size_t TABLE_SIZE_BITS = 22; // 2^22 entries
static constexpr size_t TABLE_SIZE = 1ULL << TABLE_SIZE_BITS; // 4M entries = 64MB
static constexpr size_t INDEX_MASK = TABLE_SIZE - 1;
std::vector<TTEntry> table;
Stats stats;
std::atomic<uint16_t> current_age{0};
// Hash function for FlatBuffer game state
uint64_t hashGameState(const GameStateW& state) const;
public:
TranspositionTable();
// Probe the table for a cached evaluation
std::optional<ScoreValue> probe(const GameStateW& state, int depth, PlayerId player);
// Store an evaluation in the table
void store(const GameStateW& state, int depth, PlayerId player, ScoreValue score);
// Clear the entire table
void clear();
// Increment age for replacement strategy (call at start of each search)
void incrementAge() { current_age++; }
// Get statistics
const Stats& getStats() const { return stats; }
// Print statistics to stdout
void printStats() const;
};
// Global instance for the AI to use
extern TranspositionTable g_transpositionTable;
} // namespace shardok
#endif // EAGLE0_TRANSPOSITIONTABLE_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() {
@@ -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));
@@ -8,7 +8,6 @@ cc_library(
visibility = ["//visibility:public"],
deps = [
":shardok_c_types",
"//src/main/cpp/net/eagle0/common:container_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/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
@@ -4,7 +4,8 @@
#include "GameStateW.hpp"
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
#include <algorithm>
#include <ranges>
namespace shardok {
@@ -24,13 +25,17 @@ auto GameStateW::GetOccupant(const net::eagle0::shardok::storage::fb::Coords& co
// Fast path: use bitfield cache if available
if (state->occupied_tiles() && !state->occupied_tiles()->empty()) {
const size_t tileIndex = coords.row() * columnCount + coords.column();
const size_t expectedBitfieldSize = (rowCount * columnCount + 7) / 8; // Ceiling division
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(byteIndex);
const uint8_t byte = state->occupied_tiles()->Get(static_cast<unsigned int>(byteIndex));
const bool isOccupied = (byte & (1 << bitOffset)) != 0;
if (!isOccupied) {
@@ -43,8 +48,8 @@ auto GameStateW::GetOccupant(const net::eagle0::shardok::storage::fb::Coords& co
// Used when bitfield not available OR when bitfield indicates occupation
if (!state->units()) { return nullptr; }
for (int i = 0; i < state->units()->size(); ++i) {
const auto* unit = state->units()->Get(i);
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()) {
@@ -62,7 +67,7 @@ auto GameStateW::GetKnownEnemyOccupant(
const auto* occupant = GetOccupant(coords);
if (occupant) {
if (!occupant->hidden() && occupant->player_id() != playerId &&
!common::Contains(allyPids, occupant->player_id())) {
!std::ranges::contains(allyPids, occupant->player_id())) {
return occupant;
}
}
@@ -82,26 +87,30 @@ void GameStateW::UpdateOccupiedTile(
// Clear old position in bitfield
if (oldCoords.row() >= 0 && oldCoords.row() < rowCount && oldCoords.column() >= 0 &&
oldCoords.column() < columnCount) {
const size_t tileIndex = oldCoords.row() * columnCount + oldCoords.column();
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(byteIndex);
uint8_t byte = mutableOccupiedTiles->Get(static_cast<unsigned int>(byteIndex));
byte &= ~(1 << bitOffset); // Clear the bit
mutableOccupiedTiles->Mutate(byteIndex, byte);
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 = newCoords.row() * columnCount + newCoords.column();
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(byteIndex);
uint8_t byte = mutableOccupiedTiles->Get(static_cast<unsigned int>(byteIndex));
byte |= (1 << bitOffset); // Set the bit
mutableOccupiedTiles->Mutate(byteIndex, byte);
mutableOccupiedTiles->Mutate(static_cast<unsigned int>(byteIndex), byte);
}
}
}
@@ -115,7 +124,8 @@ auto GameStateW::GetOccupiedTilesBitfield() const -> const flatbuffers::Vector<u
// 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 = (rowCount * columnCount + 7) / 8;
const size_t expectedBitfieldSize =
(static_cast<size_t>(rowCount) * static_cast<size_t>(columnCount) + 7) / 8;
if (state->occupied_tiles()->size() != expectedBitfieldSize) { return nullptr; }
@@ -5,6 +5,8 @@
#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"
@@ -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,10 @@
#include "ShardokException.hpp"
#include "src/main/cpp/net/eagle0/common/RandomGenerator.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 {
@@ -42,9 +45,9 @@ private:
}
[[nodiscard]] virtual auto InternalExecuteWithRoll(
const GameStateW& currentState,
const 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");
}
@@ -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>
@@ -310,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)
@@ -339,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);
}
@@ -376,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(
@@ -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);
}
}
@@ -117,21 +117,13 @@ auto ActionPointDistancesCache::GetMapId(const HexMap* map) -> MapId {
return MapId{.terrainTypesId = map->base_hash(), .modifierId = modifierId};
}
void ActionPointDistancesCache::ConsolidateThreadLocalCache_Racy() {
persistentCache.insert(std::begin(sharedCache), std::end(sharedCache));
sharedCache.clear();
persistentCache.insert(std::begin(sharedDistances), std::end(sharedDistances));
sharedDistances.clear();
// Clear the current thread's cache since persistent cache now has everything
tlsCache.clear();
}
ActionPointDistancesCache::~ActionPointDistancesCache() {
// We own the ActionPointDistances pointers in the persistent cache and in the shared cache.
for (const auto& [key, value] : persistentCache) { delete value; }
for (const auto& [key, value] : sharedCache) { delete value; }
}
auto ActionPointDistancesCache::GetRaw(
const HexMap* map,
const MapId& mapId,
@@ -143,14 +135,14 @@ auto ActionPointDistancesCache::GetRaw(
MakeCacheKey(mapId, battalionType, includeBravingWater, braveWaterActionPointCost);
// Check the persistent map first
if (auto persistentIt = persistentCache.find(cacheKey); persistentIt != persistentCache.end()) {
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 persistentIt->second;
return persistentCache.at(cacheKey).rawPtr;
}
#if CACHE_STATS_LOGGING_
@@ -158,12 +150,12 @@ auto ActionPointDistancesCache::GetRaw(
#endif
// Check thread-local cache first (no locks needed!)
if (auto localIt = tlsCache.find(cacheKey); localIt != tlsCache.end()) {
if (tlsCache.contains(cacheKey)) {
#if CACHE_STATS_LOGGING_
cacheStats.localHits++;
MaybePrintCacheStats();
#endif
return localIt->second; // Raw pointer - zero overhead access!
return tlsCache.at(cacheKey).rawPtr; // Raw pointer - zero overhead access!
}
#if CACHE_STATS_LOGGING_
@@ -171,19 +163,19 @@ auto ActionPointDistancesCache::GetRaw(
#endif
// Check shared cache before expensive ice-clearing operation
const ActionPointDistances* sharedResult;
if (sharedCache.if_contains(cacheKey, [&sharedResult](const auto& kv) {
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, sharedResult);
tlsCache.emplace(cacheKey, CacheEntry(sharedResult));
#if CACHE_STATS_LOGGING_
MaybePrintCacheStats();
#endif
return sharedResult;
return sharedResult.get();
}
// Cache miss in both caches - need to create ice-cleared map for pathfinding computation
@@ -192,10 +184,13 @@ auto ActionPointDistancesCache::GetRaw(
// Declaring here to keep the copied map in scope
const HexMap* mapToUse = map;
// ReSharper disable once CppTooWideScope
// ReSharper disable once CppJoinDeclarationAndAssignment
fb::HexMapW iceClearedMap;
if (hasIce) {
// Create ice-cleared map for pathfinding
// This prevents AI from considering ice as a valid path toward enemies
fb::HexMapW iceClearedMap = CreateIceClearedMap(map);
iceClearedMap = CreateIceClearedMap(map);
mapToUse = iceClearedMap.Get();
}
@@ -220,14 +215,14 @@ auto ActionPointDistancesCache::GetRaw(
auto result = creationResult.apd;
// Store in shared cache
sharedCache.lazy_emplace_l(
sharedDistances.lazy_emplace_l(
cacheKey,
[](const auto& kv) { /* already checked above */ },
[](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, result);
tlsCache.emplace(cacheKey, CacheEntry(result));
// Prevent unbounded cache growth - limit to reasonable size
if (tlsCache.size() > 100) {
@@ -241,7 +236,7 @@ auto ActionPointDistancesCache::GetRaw(
tlsCache.erase(tlsCache.begin(), it);
}
return result;
return result.get();
}
void ActionPointDistancesCache::ClearThreadLocalCache() { tlsCache.clear(); }
@@ -57,6 +57,36 @@ struct FullCacheKeyHash {
class ActionPointDistancesCache {
private:
struct CacheEntry {
shared_ptr<ActionPointDistances> sharedPtr;
const ActionPointDistances* rawPtr;
explicit CacheEntry(shared_ptr<ActionPointDistances> ptr)
: sharedPtr(std::move(ptr)),
rawPtr(sharedPtr.get()) {}
};
// 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,
@@ -64,41 +94,13 @@ private:
bool includeBravingWater,
int braveWaterActionPointCost) -> FullCacheKey;
// Tier 1: persistent map. This is NOT safe to write to while reads may be happening. Owns the
// ActionPointDistances pointers, so it must be cleared.
using PersistentMap =
gtl::flat_hash_map<FullCacheKey, const ActionPointDistances*, FullCacheKeyHash>;
PersistentMap persistentCache{};
// Tier 2: thread-local cache. This stores items that are not yet in the persistent cache.
// Does NOT own the ActionPointDistances objects; they are also present in the shared cache.
using TLSCache =
gtl::flat_hash_map<FullCacheKey, const ActionPointDistances*, FullCacheKeyHash>;
static thread_local TLSCache tlsCache;
// Tier 3: shared cache. This is thread-safe and can be accessed concurrently. Does own the
// ActionPointDistances objects, so it must be cleared. Can be consolidated into the persistent
// cache when no reads are happening.
using APDMap = gtl::parallel_flat_hash_map<
FullCacheKey,
const ActionPointDistances*,
FullCacheKeyHash,
std::equal_to<FullCacheKey>,
std::allocator<std::pair<const FullCacheKey, const ActionPointDistances*>>,
6,
std::mutex>;
APDMap sharedCache{};
public:
explicit ActionPointDistancesCache() {
// Pre-size persistent cache to reduce hash collisions
// Estimate: ~12 entries from pre-fetching + ~50-100 entries during gameplay
persistentCache.reserve(128);
}
~ActionPointDistancesCache();
// Returns raw pointer for zero overhead access
// Lifetime guaranteed by shared cache ownership
auto GetRaw(
@@ -9,7 +9,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
// Dynamic thread count based on hardware capabilities
static const int ASYNC_COUNT = [] {
static const int ASYNC_COUNT = []() {
const int cores = static_cast<int>(std::thread::hardware_concurrency());
// Use cores-2 to leave room for OS and other processes, minimum 4 threads
const int threadCount = std::max(4, cores - 4);
@@ -26,7 +26,7 @@ void FixedActionPointDistances::SetCacheDirectory(const string& newDir) {
static thread_local byte_vector _scratch;
FixedActionPointDistances::FixedActionPointDistances(const int8_t columnCount)
FixedActionPointDistances::FixedActionPointDistances(const HexMap* /*map*/, int columnCount)
: ActionPointDistances(columnCount) {}
auto FixedActionPointDistances::Create(
@@ -37,9 +37,12 @@ auto FixedActionPointDistances::Create(
bool includeBravingWater,
int braveWaterActionPointCost) -> CreationResult {
// Create the object using private constructor
auto apd = new FixedActionPointDistances(map->column_count());
auto apd = std::shared_ptr<FixedActionPointDistances>(
new FixedActionPointDistances(map, map->column_count()));
CreationResult result{.apd = apd, .loadedFromFile = false};
CreationResult result;
result.apd = apd;
result.loadedFromFile = false;
string path = "";
@@ -70,8 +73,8 @@ auto FixedActionPointDistances::Create(
for (int fromIndex = 0; fromIndex < indexCount; fromIndex++) {
apd->distances[fromIndex].insert(
apd->distances[fromIndex].end(),
&ptr[0],
&ptr[indexCount]);
&(ptr[0]),
&(ptr[indexCount]));
ptr += indexCount;
}
result.loadedFromFile = true;
@@ -94,7 +97,7 @@ auto FixedActionPointDistances::Create(
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,
@@ -19,7 +19,7 @@ using BattalionTypeSPtr = std::shared_ptr<const BattalionType>;
class FixedActionPointDistances final : public ActionPointDistances {
public:
struct CreationResult {
const FixedActionPointDistances *apd;
std::shared_ptr<FixedActionPointDistances> apd;
bool loadedFromFile;
};
@@ -29,7 +29,7 @@ private:
inline static string cacheDirectory = "";
// Private constructor - use Create factory method instead
explicit FixedActionPointDistances(int8_t columnCount);
explicit FixedActionPointDistances(const HexMap *map, int columnCount);
public:
static void SetCacheDirectory(const string &newDir);
@@ -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(
@@ -161,7 +161,7 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
const auto *unit = (Unit *)unitBytes.data();
maxChangedUnitId = std::max(maxChangedUnitId, unit->unit_id());
if (unit->unit_id() >= mutatingState->units()->size()) {
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
@@ -48,7 +48,7 @@ auto CopyWithExtraUnits(const GameStateW& original, int additionalCount) -> Game
// 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 = rowCount * columnCount;
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)
@@ -58,7 +58,9 @@ auto CopyWithExtraUnits(const GameStateW& original, int additionalCount) -> Game
const auto& location = unit.location();
if (location.row() >= 0 && location.row() < rowCount && location.column() >= 0 &&
location.column() < columnCount) {
const size_t tileIndex = location.row() * columnCount + location.column();
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
@@ -11,7 +11,7 @@
namespace shardok {
auto DefensiveAmbushAction::InternalExecute(
const GameStateW& currentState,
const 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),
@@ -23,7 +23,7 @@ FireOutAction::FireOutAction(
fireOutOdds(std::move(odds)) {}
auto FireOutAction::InternalExecute(
const GameStateW& currentState,
const GameStateW& /*currentState*/,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const auto fireOutRoll = generator->Percentile();
@@ -23,7 +23,7 @@ FireSpreadAction::FireSpreadAction(
fireSpreadOdds(std::move(odds)) {}
auto FireSpreadAction::InternalExecute(
const GameStateW& currentState,
const GameStateW& /*currentState*/,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const auto fireSpreadRoll = generator->Percentile();
@@ -71,8 +71,8 @@ public:
};
auto MeteorUnitDamageAction::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> {
CombatDamage attackerDamage =
CombatDamage::Builder()
.SetFire(attackerIntelligence * baseDamage * damageMultiplier)
@@ -131,8 +131,8 @@ public:
};
auto MeteorTileDamageAction::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> {
auto tm = fb::ToTileModifierProto(terrain->modifier());
MutatingAdjustBridgeIntegrity(&tm, integrityAdjustment);
@@ -154,7 +154,7 @@ auto MeteorTileDamageAction::InternalExecute(
}
auto MeteorCastAction::InternalExecute(
const GameStateW &currentState,
const GameStateW & /*currentState*/,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
vector<ActionResultProto> allResults{};
auto runningGameState = 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());
@@ -21,7 +21,7 @@ auto ChooseUndeadCommand(
const std::shared_ptr<RandomGenerator> &randomGenerator) -> CommandSPtr;
auto PerformUndeadCommandsAction::InternalExecute(
const GameStateW &currentState,
const GameStateW & /*currentState*/,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
GameStateW runningGameState = startingGameState;
vector<ActionResultProto> allResults{};
@@ -14,7 +14,7 @@ using net::eagle0::shardok::common::GameStatus;
[[nodiscard]] auto PlaceHiddenUnitCommand::InternalExecute(
const GameStateW& currentState,
const 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);
@@ -12,8 +12,8 @@ using net::eagle0::shardok::common::ActionType;
using net::eagle0::shardok::common::GameStatus;
auto PlaceUnitCommand::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> {
auto actorAfter = *actor;
actorAfter.mutable_location() = target;
@@ -78,7 +78,7 @@ auto effectiveSnow(const TerrainProto& terr) -> double {
}
auto SnowAdjustmentAction::InternalExecute(
const GameStateW& currentState,
const GameStateW& /*currentState*/,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
vector<ActionResult> results{};
@@ -17,8 +17,8 @@ static auto RequiredDailyFood(const Unit& unit, const SettingsGetter& settingsGe
}
auto StartPlayerTurnAction::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> {
ActionResult result{};
result.set_type(net::eagle0::shardok::common::ActionType::PLAYER_TURN_START);
result.mutable_next_player()->set_value(newFactionId);
@@ -12,7 +12,7 @@ namespace shardok {
auto UndeadChangeAction::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
if (undeadUnitIds.empty()) return {};
const int maxGrowthPer = (int)(reinforceRate * bodyCount / (double)undeadUnitIds.size());
@@ -9,8 +9,8 @@
namespace shardok {
auto UndeadFrozenAction::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> {
if (occupant->battalion().type() != net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD)
return {};
@@ -4,6 +4,9 @@
#include "UpdateGameStatusAction.hpp"
#include <algorithm>
#include <ranges>
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_status.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/player_info.hpp"
@@ -65,7 +68,7 @@ void UpdateGameStatusAction::ResolveHiddenLosers(ActionResult& actionResult) con
for (const auto* unit : *gameState->units()) {
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
if (!common::Contains(winningIds, unit->player_id()) && unit->hidden() &&
if (!std::ranges::contains(winningIds, unit->player_id()) && unit->hidden() &&
unit->can_flee() && unit->has_attached_hero() &&
unit->attached_hero().vigor() >= settingsGetter.Backing().minimum_vigor_to_act()) {
net::eagle0::shardok::storage::ResolvedUnit* fledRanger =
@@ -80,7 +83,7 @@ void UpdateGameStatusAction::ResolveHiddenLosers(ActionResult& actionResult) con
auto UpdateGameStatusAction::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{};
// If the game has already ended, just return that state
@@ -148,7 +151,7 @@ auto UpdateGameStatusAction::InternalExecute(
for (const PlayerId pid2 : survivors) {
if (pid1 == pid2) continue;
if (!common::ContainsWhere(
if (!std::ranges::any_of(
*p1Info->allies(),
[pid2](const net::eagle0::shardok::storage::fb::AlliedPlayer*
alliedPlayer) {
@@ -12,7 +12,7 @@ namespace shardok {
auto UpdateOpponentKnowledgeAction::InternalExecute(
const GameStateW &currentState,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator> & /*generator*/) const -> vector<ActionResult> {
ActionResult result{};
result.set_type(net::eagle0::shardok::common::ActionType::KNOWLEDGE_UPDATED);
@@ -31,7 +31,7 @@ auto UpdateOpponentKnowledgeAction::InternalExecute(
for (PlayerId pid = 0; pid < 10; pid++) {
if (pid == unitPid) continue;
if (pid >= playerCount) continue;
if (static_cast<unsigned int>(pid) >= playerCount) continue;
int bump = PlayerIsDefender(currentState, pid) ? defenderKnowledgeGain
: attackerKnowledgeGain;
MutatingBumpOpponentKnowledge(&unitAfter, pid, bump);
@@ -8,6 +8,9 @@
#include "FleeCommandFactory.hpp"
#include <algorithm>
#include <ranges>
#include "src/main/cpp/net/eagle0/shardok/library/commands/BecomeOutlawCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/commands/FleeCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
@@ -41,7 +44,7 @@ void FleeCommandFactory::AddAvailableFleeCommands(
for (const auto &adjTile : HexMapUtils::GetAdjacentTiles(map, unit->location())) {
const auto *occupant = Occupant(allUnits, adjTile.coords);
if (occupant && !occupant->hidden()) {
if (common::Contains(allyPids, occupant->player_id())) {
if (std::ranges::contains(allyPids, occupant->player_id())) {
adjacentFriendliesMod += perAdjacentFriendly;
} else {
adjacentEnemiesMod += perAdjacentEnemy;
@@ -55,7 +58,7 @@ void FleeCommandFactory::AddAvailableFleeCommands(
for (const auto &twoAwayCoords : TilesWithExactDistance(map, unit->location(), 2)) {
const auto *occupant = Occupant(allUnits, twoAwayCoords);
if (occupant && !occupant->hidden()) {
if (common::Contains(allyPids, occupant->player_id())) {
if (std::ranges::contains(allyPids, occupant->player_id())) {
adjacentFriendliesMod += int32_t(perAdjacentFriendly * adjustmentForTwoAway);
} else {
adjacentEnemiesMod += int32_t(perAdjacentEnemy * adjustmentForTwoAway);
@@ -4,6 +4,9 @@
#include "HideCommandFactory.hpp"
#include <algorithm>
#include <ranges>
#include "src/main/cpp/net/eagle0/shardok/library/commands/HideCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ZoneOfControlCalculator.hpp"
@@ -26,7 +29,7 @@ auto HideCommandFactory::MakeHideCommand(const Unit *actor, const Coords &target
auto HideCommandFactory::PositionIsHideable(
const Unit *actor,
CommandList &existingCommands,
CommandList & /*existingCommands*/,
const HexMap *hexMap,
const Coords &target,
const Units *units,
@@ -36,7 +39,7 @@ auto HideCommandFactory::PositionIsHideable(
const auto occupant = Occupant(units, target);
if (occupant) {
if (occupant->player_id() == actor->player_id()) return false;
if (common::Contains(allyPids, occupant->player_id())) return false;
if (std::ranges::contains(allyPids, occupant->player_id())) return false;
if (!occupant->hidden()) return false;
}
@@ -8,7 +8,6 @@
#include "HolyWaveCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/commands/HolyWaveCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
@@ -46,7 +45,7 @@ void HolyWaveCommandFactory::AddAvailableCommands(
AddAvailableHolyWaveCommands(commands, params.unit, params.remainingActionPoints);
}
auto CanHolyWave(const SettingsGetter &settings, const Unit *unit) -> bool {
auto CanHolyWave(const SettingsGetter & /*settings*/, const Unit *unit) -> bool {
if (!unit->has_attached_hero()) return false;
return unit->attached_hero().profession_info().profession() ==
@@ -21,9 +21,9 @@ void ScoutCommandFactory::AddAvailableScoutCommands(
CommandList& commands,
const Unit* unit,
const ActionPoints remainingActionPoints,
const Coords& position,
const Coords& /*position*/,
const HexMap* hexMap,
const Units* units) const {
const Units* /*units*/) const {
if (CanScout(settings, unit)) {
const auto& hero = unit->attached_hero();
@@ -13,7 +13,7 @@ using net::eagle0::shardok::common::ActionType;
auto ControlCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
const Unit* actor = currentState->units()->Get(actorId);
if (!actor->has_attached_hero() ||
actor->attached_hero().control_info().controlled_unit_id() == -1) {
@@ -38,7 +38,7 @@ auto ApplyAndAdd(
}
auto EndTurnCommand::InternalExecute(
const GameStateW &currentState,
const GameStateW & /*currentState*/,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
if (GetPlayerId() != gameState->current_player()) {
throw ShardokInternalErrorException(
@@ -73,8 +73,8 @@ auto EndTurnCommand::InternalExecute(
// Any heroes in fire get burninated
// Decrement stun counters, increment cast state
const auto beforeFireUnitsCount = runningGameState->units()->size();
for (int i = 0; i < beforeFireUnitsCount; i++) {
const auto *beforeUnit = runningGameState->units()->Get(i);
for (size_t i = 0; i < beforeFireUnitsCount; i++) {
const auto *beforeUnit = runningGameState->units()->Get(static_cast<unsigned int>(i));
if (beforeUnit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT)
continue;
if (beforeUnit->player_id() != GetPlayerId()) continue;
@@ -112,10 +112,10 @@ auto EndTurnCommand::InternalExecute(
endTurnResult.mutable_next_player()->set_value(NextPlayerId(gameState, GetPlayerId()));
const auto unitsAtEndOfRoundCount = runningGameState->units()->size();
for (int i = 0; i < unitsAtEndOfRoundCount; i++) {
for (size_t i = 0; i < unitsAtEndOfRoundCount; i++) {
// Grab the unit fresh because it might have been modified by a previous iteration of the
// loop
const auto *eorUnit = runningGameState->units()->Get(i);
const auto *eorUnit = runningGameState->units()->Get(static_cast<unsigned int>(i));
if (eorUnit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT)
continue;
if (eorUnit->player_id() != GetPlayerId()) continue;
@@ -11,7 +11,7 @@ namespace shardok {
auto FortifyCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
ActionResult fortifyResult{};
fortifyResult.set_type(net::eagle0::shardok::common::FORTIFIED);
fortifyResult.mutable_player()->set_value(GetPlayerId());
@@ -4,6 +4,8 @@
#include "HideCommand.hpp"
#include <algorithm>
#include <ranges>
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
@@ -29,7 +31,7 @@ HideCommand::HideCommand(
auto HideCommand::InternalExecute(
const GameStateW &currentState,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator> & /*generator*/) const -> vector<ActionResult> {
auto actorAfter = *currentState->units()->Get(actorId);
MutatingSpendActionPoints(&actorAfter, cost);
@@ -60,7 +62,7 @@ auto HideCommand::InternalExecute(
if (!oneOverOccupant) continue;
PlayerId occupantPid = oneOverOccupant->player_id();
if (occupantPid == GetPlayerId()) continue;
if (common::Contains(alliedPids, occupantPid)) continue;
if (std::ranges::contains(alliedPids, occupantPid)) continue;
if (!oneOverOccupant->has_attached_hero()) continue;
if (oneOverOccupant->attached_hero().profession_info().profession() ==
@@ -9,6 +9,7 @@
#include "HolyWaveCommand.hpp"
#include <algorithm>
#include <ranges>
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"
@@ -50,7 +51,7 @@ public:
auto HolyWaveDamageAction::InternalExecute(
const GameStateW &currentState,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator> & /*generator*/) const -> vector<ActionResult> {
ActionResult resultProto;
resultProto.mutable_player()->set_value(playerId);
resultProto.mutable_actor()->set_value(actingUnitId);
@@ -131,7 +132,7 @@ public:
auto HolyWaveInspireAction::InternalExecute(
const GameStateW &currentState,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator> & /*generator*/) const -> vector<ActionResult> {
ActionResult resultProto;
resultProto.mutable_player()->set_value(playerId);
resultProto.mutable_actor()->set_value(actingUnitId);
@@ -197,7 +198,7 @@ HolyWaveCommand::HolyWaveCommand(
auto HolyWaveCommand::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;
const Unit *actorBefore = currentState->units()->Get(actorId);
@@ -237,7 +238,7 @@ auto HolyWaveCommand::InternalExecute(
undeadIds.push_back(occupant->unit_id());
} else if (
occupant->player_id() == GetPlayerId() ||
common::Contains(allyPids, GetPlayerId())) {
std::ranges::contains(allyPids, GetPlayerId())) {
allFriendlyUnitIds.push_back(occupant->unit_id());
}
}
@@ -34,7 +34,7 @@ auto LightningBoltCommand::InternalExecute(
return ExecuteWithRoll(currentState, attackerRoll);
}
auto LightningBoltCommand::ExecuteWithRoll(const GameStateW& currentState, double attackerRoll)
auto LightningBoltCommand::ExecuteWithRoll(const GameStateW& currentState, double /*attackerRoll*/)
const -> vector<ActionResult> {
const Unit* attackerBefore = currentState->units()->Get(attackerId);
if (!attackerBefore->has_attached_hero()) {
@@ -24,7 +24,7 @@ MeteorCancelCommand::MeteorCancelCommand(
auto MeteorCancelCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
const Unit* caster = currentState->units()->Get(casterId);
auto casterAfter = *caster;
@@ -39,7 +39,7 @@ auto MeteorStartCommand::GetCommandProto() const -> CommandProto {
auto MeteorStartCommand::InternalExecute(
const GameStateW &currentState,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator> & /*generator*/) const -> vector<ActionResult> {
const auto *caster = currentState->units()->Get(casterId);
auto casterAfter = *caster;
@@ -14,7 +14,7 @@ namespace shardok {
auto shardok::MeteorTargetCommand::InternalExecute(
const GameStateW &currentState,
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator> & /*generator*/) const -> vector<ActionResult> {
const Unit *caster = currentState->units()->Get(casterId);
auto casterAfter = *caster;
@@ -4,6 +4,8 @@
#include "MoveCommand.hpp"
#include <algorithm>
#include <ranges>
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"
@@ -97,7 +99,7 @@ auto shardok::MoveCommand::InternalExecute(
const auto* occupant = currentState.GetOccupant(adj);
if (!occupant) continue;
if (occupant->player_id() == mover.player_id()) continue;
if (common::Contains(allyPids, occupant->player_id())) continue;
if (std::ranges::contains(allyPids, occupant->player_id())) continue;
if (!occupant->has_attached_hero()) continue;
if (occupant->attached_hero().profession_info().profession() ==
net::eagle0::shardok::storage::fb::Profession_RANGER) {
@@ -113,7 +115,7 @@ auto shardok::MoveCommand::InternalExecute(
auto occupant = currentState.GetOccupant(adj);
if (!occupant) continue;
if (occupant->player_id() == mover.player_id()) continue;
if (common::Contains(allyPids, occupant->player_id())) continue;
if (std::ranges::contains(allyPids, occupant->player_id())) continue;
if (occupant->hidden()) {
auto occupantAfter = *occupant;
occupantAfter.mutate_hidden(false);
@@ -17,7 +17,7 @@ using net::eagle0::shardok::common::ActionType;
auto ReinforceCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
const Unit* actingUnit = currentState->units()->Get(actingUnitId);
auto actorAfter = *actingUnit;
MutatingSpendActionPoints(&actorAfter, cost);
@@ -21,7 +21,7 @@ auto ReleaseUnitCommand::GetCommandProto() const -> CommandProto {
auto ReleaseUnitCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
auto controllingUnitAfter = *currentState->units()->Get(actingUnitId);
controllingUnitAfter.mutate_remaining_action_points(0);
controllingUnitAfter.mutable_attached_hero().mutable_control_info().mutate_controlled_unit_id(
@@ -18,7 +18,7 @@ using net::eagle0::shardok::common::ActionType::REPAIR_FAILED;
auto RepairCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
const auto* actor = currentState->units()->Get(actorId);
if (!actor->has_attached_hero()) { throw ActionRequiresHeroException("repair"); }
@@ -13,7 +13,7 @@
namespace shardok {
auto RetreatCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
ActionResult result{};
result.set_type(net::eagle0::shardok::common::ActionType::RETREATED);
result.mutable_player()->set_value(GetPlayerId());
@@ -26,7 +26,7 @@ StartFireCommand::StartFireCommand(
odds(std::move(odds)){};
auto StartFireCommand::InternalExecute(
const GameStateW& currentState,
const GameStateW& /*currentState*/,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
return ExecuteWithRoll(generator->Percentile());
}
@@ -11,7 +11,7 @@ namespace shardok {
auto UnitRestCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
auto actorAfter = *currentState->units()->Get(actorId);
if (actorAfter.has_attached_hero()) {
@@ -10,7 +10,7 @@ namespace shardok {
auto UnitStopCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
ActionResult result{};
result.set_type(net::eagle0::shardok::common::ActionType::UNIT_STOP);
result.mutable_player()->set_value(GetPlayerId());
@@ -10,6 +10,7 @@ cc_library(
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
],
deps = [
"//src/main/cpp/net/eagle0/common:byte_hasher",
"//src/main/cpp/net/eagle0/common:filesystem_utils",
"@flatbuffers",
],
@@ -7,6 +7,12 @@
#include <sys/mman.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
namespace shardok {
// Uses mmap and munmap to directly allocate pages
@@ -44,7 +50,7 @@ public:
}
return b;
}
auto deallocate(uint8_t* buf, const size_t size) -> void override { free(buf); }
auto deallocate(uint8_t* buf, const size_t /*size*/) -> void override { free(buf); }
~MallocAllocator() override = default;
};
@@ -54,9 +60,9 @@ class ReusingAllocator final : public WrapperAllocator {
private:
std::unique_ptr<WrapperAllocator> fallbackAllocator;
const size_t pageSize = getpagesize();
const size_t pageSize = static_cast<size_t>(getpagesize());
auto RoundUpToPage(const size_t size) const -> size_t {
[[nodiscard]] auto RoundUpToPage(const size_t size) const -> size_t {
return (size + pageSize - 1) & ~(pageSize - 1);
}
@@ -138,7 +144,7 @@ public:
auto WrapperAllocator::GetDefaultAllocator() -> WrapperAllocator* {
static pthread_once_t once = PTHREAD_ONCE_INIT;
static WrapperAllocator* allocator = nullptr;
pthread_once(&once, []() {
pthread_once(&once, [] {
// Change this to use one of the other allocators
allocator = new MallocAllocator();
});
@@ -146,4 +152,4 @@ auto WrapperAllocator::GetDefaultAllocator() -> WrapperAllocator* {
return allocator;
}
} // namespace shardok
} // namespace shardok
@@ -7,8 +7,10 @@
#include <flatbuffers/flatbuffers.h>
#include <cstdlib>
#include <utility>
#include "src/main/cpp/net/eagle0/common/ByteHasher.hpp"
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
namespace shardok {
@@ -69,7 +71,7 @@ public:
Wrapper(const Wrapper& copiedFrom)
: size(copiedFrom.size),
offset(copiedFrom.offset),
buffer(CopyBuffer(copiedFrom)){};
buffer(CopyBuffer(copiedFrom)) {}
auto operator=(const Wrapper& other) -> Wrapper& {
if (&other != this) {
@@ -78,10 +80,9 @@ public:
WrapperAllocator::GetDefaultAllocator()->deallocate(buffer, size);
}
buffer = CopyBuffer(other.buffer, other.size);
} else {
} else if (buffer != nullptr && other.buffer != nullptr) {
memcpy(buffer, other.buffer, other.size);
}
memcpy(buffer, other.buffer, other.size);
size = other.size;
offset = other.offset;
@@ -103,7 +104,9 @@ public:
}
}
operator FB*() { return Get(); } // NOLINT(*-explicit-constructor)
// ReSharper disable once CppNonExplicitConversionOperator
operator FB*() { return Get(); } // NOLINT(*-explicit-constructor)
// ReSharper disable once CppNonExplicitConversionOperator
operator const FB*() const { return Get(); } // NOLINT(*-explicit-constructor)
auto Get() const -> const FB* { return flatbuffers::GetRoot<FB>(buffer + offset); }
@@ -119,27 +122,47 @@ public:
}
[[nodiscard]] auto ToByteString() const -> string {
char* str = (char*)malloc(sizeof(size_t) + size);
memcpy(str, &offset, sizeof(size_t));
memcpy(str + sizeof(size_t), reinterpret_cast<char const*>(buffer), size);
return string(str, sizeof(size_t) + size);
string result;
result.reserve(sizeof(size_t) + size);
result.append(reinterpret_cast<const char*>(&offset), sizeof(size_t));
result.append(reinterpret_cast<const char*>(buffer), size);
return result;
}
// Compute FNV-1a hash directly on the buffer content without copying
// Note: Offset is not included in hash as it's just an implementation detail
[[nodiscard]] auto ComputeFNV1aHash() const -> uint64_t {
// Hash only the actual FlatBuffer content, not the offset
return HashBuffer(buffer, size);
}
static auto FromByteString(const string& str) -> Wrapper {
if (str.size() < sizeof(size_t)) {
return Wrapper(); // Return empty wrapper for invalid data
}
size_t offset;
memcpy(&offset, str.data(), sizeof(size_t));
size_t size = str.size() - sizeof(size_t);
auto toReturn = Wrapper((uint8_t*)str.data() + sizeof(size_t), offset, size);
auto toReturn = Wrapper(
const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(str.data())) + sizeof(size_t),
offset,
size);
return toReturn;
}
[[nodiscard]] auto ToByteVector() const -> byte_vector { return byte_vector(ToByteString()); }
static auto FromByteVector(const byte_vector& bv) -> Wrapper {
if (bv.size() < sizeof(size_t)) {
return Wrapper(); // Return empty wrapper for invalid data
}
size_t offset;
memcpy(&offset, bv.data(), sizeof(size_t));
size_t size = bv.size() - sizeof(size_t);
// Note: CopyBuffer will handle const_cast safely by making a copy
auto toReturn = Wrapper(const_cast<uint8_t*>(bv.data()) + sizeof(size_t), offset, size);
return toReturn;

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