Compare commits

..
Author SHA1 Message Date
adminandClaude 47481c2de6 Fix occupants cache performance by sharing cache across copies
Changes:
- Changed occupantsCache_ from unique_ptr to shared_ptr in GameStateW
- Modified copy constructors to share cache instead of rebuilding
- Updated RebuildOccupantsCache() and InitializeCache() to use make_shared
- Removed unnecessary RebuildOccupantsCache() call in GameStateGuesser.cpp

This fixes the performance regression where cache copies were expensive
due to full cache rebuilds on every GameStateW copy operation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 20:42:41 -07:00
admin 4810e8e9a5 replaced some more caches 2025-07-20 18:04:39 -07:00
admin d46423bea8 fix the crasher 2025-07-20 18:04:39 -07:00
admin 3a2f9c0c8c a couple more 2025-07-20 18:04:39 -07:00
admin c0accd9d6b more use of new occupants cache 2025-07-20 18:04:38 -07:00
admin c3c80e3c41 put the occupants cache in GameStateW 2025-07-20 18:04:38 -07:00
244 changed files with 3062 additions and 5806 deletions
+3 -1
View File
@@ -74,7 +74,7 @@ bazel run gazelle # Update Go build files
### Code Formatting
```bash
# ALWAYS run clang-format after making any C++ or C# code changes
# MANDATORY: ALWAYS run clang-format after making any C++ or C# code changes
clang-format -i <modified_files>
# Format all C++ files in a directory:
@@ -84,6 +84,8 @@ find . -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i
find . -name "*.cs" | xargs clang-format -i
```
**IMPORTANT FOR CLAUDE CODE**: After making any changes to C++ (.cpp, .hpp) or C# (.cs) files, you MUST immediately run clang-format on those files using the Bash tool. This is not optional - it's a mandatory step that must be done every time you modify code files. Use the exact command: `clang-format -i <file_path>` for each modified file.
## 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.4.0")
bazel_dep(name = "toolchains_llvm", version = "1.2.0")
# Configure and register the toolchain.
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
llvm.toolchain(
name = "llvm_toolchain",
llvm_version = "20.1.2",
llvm_version = "19.1.0",
)
use_repo(llvm, "llvm_toolchain")
@@ -22,14 +22,14 @@ register_toolchains(
dev_dependency = True,
)
bazel_dep(name = "rules_pkg", version = "1.1.0")
bazel_dep(name = "bazel_skylib", version = "1.8.1")
bazel_dep(name = "rules_pkg", version = "1.0.1")
bazel_dep(name = "bazel_skylib", version = "1.7.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.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")
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")
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
+35 -61
View File
@@ -13,8 +13,7 @@
"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/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1",
"https://bcr.bazel.build/modules/abseil-cpp/20250127.1/source.json": "03c90ee57977264436d3231676dcddae116c4769a5d02b6fc16c2c9e019b583a",
"https://bcr.bazel.build/modules/abseil-cpp/20240722.0/source.json": "59af9f8a8a4817092624e21263fe1fb7d7951a3b06f0570c610c7e5a9caf5f29",
"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",
@@ -41,7 +40,6 @@
"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",
@@ -59,8 +57,7 @@
"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.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/source.json": "7ebaefba0b03efe59cac88ed5bbc67bcf59a3eff33af937345ede2a38b2d368a",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953",
"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",
@@ -94,8 +91,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.45.0/MODULE.bazel": "ecd19ebe9f8e024e1ccffb6d997cc893a974bcc581f1ae08f386bdd448b10687",
"https://bcr.bazel.build/modules/gazelle/0.45.0/source.json": "111d182facc5f5e80f0b823d5f077b74128f40c3fd2eccc89a06f34191bd3392",
"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/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",
@@ -107,8 +104,7 @@
"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.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46",
"https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713",
"https://bcr.bazel.build/modules/googletest/1.15.2/source.json": "dbdda654dcb3a0d7a8bc5d0ac5fc7e150b58c2a986025ae5bc634bb2cb61f470",
"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",
@@ -150,18 +146,14 @@
"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.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
"https://bcr.bazel.build/modules/platforms/0.0.10/source.json": "f22828ff4cf021a6b577f1bf6341cb9dcd7965092a439f64fc1bb3b7a5ae4bd5",
"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",
@@ -174,9 +166,7 @@
"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",
@@ -195,9 +185,8 @@
"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",
@@ -231,9 +220,7 @@
"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.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_go/0.50.1/source.json": "205765fd30216c70321f84c9a967267684bdc74350af3f3c46c857d9f80a4fa2",
"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",
@@ -245,14 +232,12 @@
"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",
@@ -274,8 +259,7 @@
"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.1.0/MODULE.bazel": "9db8031e71b6ef32d1846106e10dd0ee2deac042bd9a2de22b4761b0c3036453",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/source.json": "fef768df13a92ce6067e1cd0cdc47560dace01354f1d921cfb1d632511f7d608",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
"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",
@@ -297,8 +281,7 @@
"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/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_python/0.40.0/source.json": "939d4bd2e3110f27bfb360292986bb79fd8dcefb874358ccd6cdaa7bda029320",
"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",
@@ -314,12 +297,11 @@
"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.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
"https://bcr.bazel.build/modules/stardoc/0.7.1/source.json": "b6500ffcd7b48cd72c29bb67bcac781e12701cc0d6d55d266a652583cfcdab01",
"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.4.0/MODULE.bazel": "05239402b7374293359c2f22806f420b75aa5d6f4b15a2eaa809a2c214d58b31",
"https://bcr.bazel.build/modules/toolchains_llvm/1.4.0/source.json": "229a516d282b17a82be54c6e3ae220a1b750fb55a8495567e5c7a9d09423f3e2",
"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/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",
@@ -681,6 +663,23 @@
"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=",
@@ -1021,29 +1020,6 @@
]
}
},
"@@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=",
@@ -1413,8 +1389,8 @@
},
"@@toolchains_llvm~//toolchain/extensions:llvm.bzl%llvm": {
"general": {
"bzlTransitiveDigest": "afRF0aFOIUrkYl3o040WQ606ep1qciEXzjnAxT3Kek8=",
"usagesDigest": "wl2njI0HdPm89XuKGMWIa1gyCAQ5gvOs+ksjFd4QbZs=",
"bzlTransitiveDigest": "dZkcElkfice3BvKPXtFwrvPABm8ac+SwfynSinwK4dQ=",
"usagesDigest": "Phpqeo3bkW3l2tD5aOSKApubwu0KPfl0KuPgjY6+ueM=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -1428,9 +1404,8 @@
"distribution": "auto",
"exec_arch": "",
"exec_os": "",
"libclang_rt": {},
"llvm_mirror": "",
"llvm_version": "20.1.2",
"llvm_version": "19.1.0",
"llvm_versions": {},
"netrc": "",
"sha256": {},
@@ -1445,7 +1420,6 @@
"absolute_paths": false,
"archive_flags": {},
"compile_flags": {},
"conly_flags": {},
"coverage_compile_flags": {},
"coverage_link_flags": {},
"cxx_builtin_include_directories": {},
@@ -1459,7 +1433,7 @@
"link_flags": {},
"link_libs": {},
"llvm_versions": {
"": "20.1.2"
"": "19.1.0"
},
"opt_compile_flags": {},
"opt_link_flags": {},
-206
View File
@@ -1,206 +0,0 @@
# Occupants Vector Optimization - Conversion Report
## Overview
This document details the implementation of an embedded occupants vector in the GameState flatbuffer to replace O(n)
unit iteration with O(1) position lookups. It also catalogs all Occupant() and KnownEnemyOccupant() calls that could not
be converted to use the new optimized methods.
## Completed Conversions
### Successfully Converted Occupant() Calls (16 total)
#### Commands Directory (11 conversions)
1. **HideCommand.cpp**:
- Line 43: `Occupant(currentState->units(), target)``currentState.GetOccupant(target)`
- Line 59: `Occupant(currentState->units(), adjCoords)``currentState.GetOccupant(adjCoords)`
2. **ScoutCommand.cpp**:
- Line 63: `Occupant(currentState->units(), target)``currentState.GetOccupant(target)`
- Line 73: `Occupant(currentState->units(), adjacentCoords)``currentState.GetOccupant(adjacentCoords)`
3. **ReduceCommand.cpp**:
- Line 66: `Occupant(currentState->units(), target)``currentState.GetOccupant(target)`
4. **RaiseDeadCommand.cpp**:
- Line 53: `Occupant(currentState->units(), target)``currentState.GetOccupant(target)`
5. **HolyWaveCommand.cpp**:
- Line 233: `Occupant(runningState->units(), coords)``runningState.GetOccupant(coords)`
6. **MoveCommand.cpp**:
- Line 66: `Occupant(allUnits, destination)``currentState.GetOccupant(destination)`
- Line 98: `Occupant(allUnits, adj)``currentState.GetOccupant(adj)`
- Line 114: `Occupant(allUnits, adj)``currentState.GetOccupant(adj)`
#### Actions Directory (4 conversions)
1. **UpdateGameStatusAction.cpp**:
- Line 232: `Occupant(gameState->units(), criticalTile)``currentState.GetOccupant(criticalTile)`
2. **MeteorCastAction.cpp**:
- Line 186: `Occupant(runningGameState->units(), target)``runningGameState.GetOccupant(target)`
- Line 251: `Occupant(runningGameState->units(), splashCoords)``runningGameState.GetOccupant(splashCoords)`
- Line 304: `Occupant(runningGameState->units(), coords)``runningGameState.GetOccupant(coords)`
3. **UpdateOpponentKnowledgeAction.cpp**:
- Line 42: `Occupant(currentState->units(), adjCoords)``currentState.GetOccupant(adjCoords)`
#### Engine Directory (1 conversion)
1. **ShardokEngine.cpp**:
- Line 463: `Occupant(GetCurrentGameState()->units(), modifiedCoords)``gameState.GetOccupant(modifiedCoords)`
#### Factory Classes Directory (previously converted)
1. **PlayerSetupCommandFactory.cpp**:
- Line 31: `Occupant(gameState->units(), *possiblePosition)``gameState.GetOccupant(*possiblePosition)`
- Line 40: `Occupant(gameState->units(), possibleHidingPosition)``gameState.GetOccupant(possibleHidingPosition)`
2. **FallIntoWaterAction.cpp**:
- Line 154: `Occupant(currentState->units(), adjWithTerrain.adjacentCoords)`
`currentState.GetOccupant(adjWithTerrain.adjacentCoords)`
- Line 175: `Occupant(currentState->units(), bestCoords)``currentState.GetOccupant(bestCoords)`
### KnownEnemyOccupant() Conversions
**Result: 0 conversions possible**
All KnownEnemyOccupant() calls are in command factory methods that receive decomposed game state parameters (Units*,
vector<PlayerId>, etc.) rather than complete GameStateW objects.
## Remaining Unconverted Calls
### Occupant() Calls That Cannot Be Converted
#### 1. PerformUndeadCommandsAction.cpp (2 calls - No GameStateW access)
- **Line 69**: `Occupant(units, FromCoordsProto(possibleAttackCommandProto.target()))`
- **Line 99**: `Occupant(units, adjCoords)`
- **Reason**: These calls are in the `ChooseUndeadCommand()` function which only receives `const Units* units`
parameter, not a full GameStateW.
- **Location**: `src/main/cpp/net/eagle0/shardok/library/actions/PerformUndeadCommandsAction.cpp`
#### 2. AICommandFilter.cpp (1 call - Raw pointer access)
- **Line 399**: `KnownEnemyOccupant(pid, units, allyPids, fireLocation)` (in EXTINGUISH_FIRE_COMMAND case)
- **Reason**: Method receives `const GameState* gameState` parameter, not GameStateW. Has TODO comment noting this
limitation.
- **Location**: `src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.cpp`
#### 3. UpdateGameStatusAction.cpp - Member Variable Usage
- **Various calls**: Uses `gameState` member variable of type `const GameState*`
- **Reason**: Class was designed to take raw GameState pointer in constructor, though InternalExecute method has
GameStateW access.
- **Location**: `src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.cpp`
#### 4. IceAndSnowAdjustmentActionFactory.cpp (1 call - Factory pattern)
- **Line 42**: `Occupant(units, coords)`
- **Reason**: Factory method receives individual parameters, not GameStateW.
- **Location**: `src/main/cpp/net/eagle0/shardok/library/action_factories/IceAndSnowAdjustmentActionFactory.cpp`
### KnownEnemyOccupant() Calls That Cannot Be Converted
#### Command Factory Methods (8 calls - No GameStateW access)
1. **RepairCommandFactory.cpp** - Line 44
2. **FearCommandFactory.cpp** - Line 35
3. **LightningBoltCommandFactory.cpp** - Line 54
4. **ReduceCommandFactory.cpp** - Line 48
5. **ChallengeDuelCommandFactory.cpp** - Line 35
6. **HideCommandFactory.cpp** - Line 45
7. **MeleeCommandFactory.cpp** - Line 58
8. **ArcheryCommandFactory.cpp** - Line 89
**Common Reason**: All command factory methods follow a pattern where they receive individual game state components (
`Units* units`, `vector<PlayerId> allyPids`, etc.) rather than a complete GameStateW object.
#### Utility Functions (3 calls - Utility function parameters)
1. **HexMapUtils.cpp** - Lines 81, 670
2. **ZoneOfControlCalculator.cpp** - Line 143
**Reason**: These are utility functions that take decomposed parameters for reusability across different contexts.
## Performance Impact
### Achieved Improvements
- **16 Occupant() calls** converted from O(n) iteration to O(1) lookup
- Eliminated cache invalidation issues with thread-local approach
- Automatic copying of occupants vector with GameState copies
- **Estimated Performance Gain**: 2-5% reduction in AI search time for typical game states
### Trade-offs
- **Memory Overhead**: 168 bytes per GameState (14×12 map = 168 int16 values)
- **Incremental Updates**: ActionResultApplier now maintains occupants vector via UpdateOccupant() calls
- **Copy Cost**: Slightly higher GameState copy overhead offset by O(1) lookup benefits
## Architectural Patterns Identified
### Convertible Patterns
1. **Command InternalExecute methods**: Have access to `const GameStateW& currentState`
2. **Action InternalExecute methods**: Have access to `const GameStateW& currentState`
3. **Factory methods with GameStateW parameters**: Can access embedded occupants vector
### Non-Convertible Patterns
1. **Command Factory methods**: Receive decomposed parameters (`Units*`, `HexMap*`, etc.)
2. **Utility functions**: Take individual components for reusability
3. **Engine methods**: Often work with raw `GameState*` pointers
4. **Legacy member variables**: Classes storing `const GameState*` instead of `GameStateW`
## Recommendations for Future Work
### Potential Additional Conversions
1. **Refactor command factories** to accept GameStateW instead of decomposed parameters
2. **Update ShardokEngine** to use GameStateW internally where possible
3. **Create GameStateW constructors** from raw GameState* to enable more conversions
4. **Modernize legacy classes** to use GameStateW member variables
### Copy-on-Write Consideration
The user suggested implementing copy-on-write (COW) for GameStateW to reduce memory allocation overhead during AI
search. This could provide additional performance benefits by eliminating unnecessary copying of the occupants vector.
## Technical Implementation Details
### Core Changes Made
1. **game_state.fbs**: Added `occupants:[int16];` field
2. **GameStateW.cpp**: Implemented GetOccupant() and UpdateOccupant() methods
3. **GameStateCopier.cpp**: Populates occupants vector during GameState creation
4. **ActionResultApplier.cpp**: Maintains occupants vector during unit movement
### Key Method Signatures
```cpp
// O(1) occupant lookup
auto GameStateW::GetOccupant(const Coords& coords) const -> const Unit*;
// O(1) enemy occupant lookup
auto GameStateW::GetKnownEnemyOccupant(
PlayerId playerId,
const std::vector<PlayerId>& allyPids,
const Coords& coords) const -> const Unit*;
// Incremental occupants vector maintenance
void GameStateW::UpdateOccupant(
UnitId unitId,
const Coords& oldCoords,
const Coords& newCoords);
```
## Conclusion
The occupants vector optimization successfully converted 12 high-frequency Occupant() calls to O(1) lookups while
maintaining correctness through automatic copying and incremental updates. The remaining 15+ unconverted calls are
primarily in architectural layers (command factories, utilities) that would require broader refactoring to convert. The
performance improvement achieved represents a solid foundation that could be extended with future architectural
modernization.
+7 -15
View File
@@ -22,6 +22,13 @@ 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"],
@@ -88,21 +95,6 @@ cc_library(
],
)
cc_library(
name = "task_result",
hdrs = ["TaskResult.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
)
cc_library(
name = "thread_pool",
hdrs = ["ThreadPool.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
deps = [":task_result"],
)
cc_library(
name = "time_utils",
hdrs = ["TimeUtils.hpp"],
+4 -15
View File
@@ -7,23 +7,12 @@
#include <cstdint>
// FNV-1a 64-bit constants
constexpr uint64_t FNV_PRIME = 0x00000100000001B3ULL;
constexpr uint64_t FNV_OFFSET_BASIS = 0xcbf29ce484222325ULL;
constexpr uint64_t FNV_PRIME = 0x100000001b3;
constexpr uint64_t FNV_OFFSET_BASIS = 0xcbf29ce484222325;
// FNV-1a algorithm: XOR first, then multiply
static inline auto MixIn(uint64_t& hash, const uint8_t byte) {
hash ^= byte;
hash *= FNV_PRIME;
}
// Hash an entire buffer using FNV-1a
static inline auto HashBuffer(const uint8_t* data, size_t size) -> uint64_t {
uint64_t hash = FNV_OFFSET_BASIS;
if (data != nullptr) {
for (size_t i = 0; i < size; ++i) { MixIn(hash, data[i]); }
}
return hash;
hash = hash * FNV_PRIME;
hash = hash ^ byte;
}
#endif // EAGLE0_BYTEHASHER_HPP
@@ -0,0 +1,173 @@
//
// 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(static_cast<size_t>(size));
auto bv = byte_vector(size);
inputFileStream.read((char*)bv.data(), size);
return bv;
@@ -84,9 +84,7 @@ auto RandomGenerator::ChanceOpenEndedPercentileAtOrAbove(const double value) ->
auto StdLibraryGenerator::DoubleZeroToOne() -> double { return unifDouble(engine); }
StdLibraryGenerator::StdLibraryGenerator() : RandomGenerator() {
engine.seed(static_cast<std::mt19937_64::result_type>(std::time(nullptr)));
}
StdLibraryGenerator::StdLibraryGenerator() : RandomGenerator() { engine.seed(std::time(nullptr)); }
auto StdLibraryGenerator::IntBetween(const int min, const int max) -> int {
std::uniform_int_distribution<int> unifInt(min, max - 1);
@@ -1,39 +0,0 @@
//
// TaskResult.hpp - Result wrapper for task execution with status information
//
#ifndef EAGLE0_TASK_RESULT_HPP
#define EAGLE0_TASK_RESULT_HPP
namespace eagle0::common {
enum class TaskStatus { SUCCESS = 0, DEADLINE_EXCEEDED = 1, CANCELLED = 2 };
template<typename T>
struct TaskResult {
T value;
TaskStatus status;
TaskResult() : value{}, status(TaskStatus::SUCCESS) {}
TaskResult(T val) : value(std::move(val)), status(TaskStatus::SUCCESS) {}
TaskResult(T val, TaskStatus stat) : value(std::move(val)), status(stat) {}
// Convenience methods for checking status
T get() const { return value; }
bool succeeded() const { return status == TaskStatus::SUCCESS; }
bool deadlineExceeded() const { return status == TaskStatus::DEADLINE_EXCEEDED; }
bool cancelled() const { return status == TaskStatus::CANCELLED; }
// Factory methods for cleaner construction
static TaskResult Success(T val) { return TaskResult(std::move(val), TaskStatus::SUCCESS); }
static TaskResult DeadlineExceeded(T val = T{}) {
return TaskResult(std::move(val), TaskStatus::DEADLINE_EXCEEDED);
}
static TaskResult Cancelled(T val = T{}) {
return TaskResult(std::move(val), TaskStatus::CANCELLED);
}
};
} // namespace eagle0::common
#endif // EAGLE0_TASK_RESULT_HPP
@@ -1,14 +0,0 @@
//
// 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
@@ -1,307 +0,0 @@
//
// ThreadPool.hpp - Priority-based thread pool with deadline support
//
#ifndef EAGLE0_THREADPOOL_HPP
#define EAGLE0_THREADPOOL_HPP
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include "TaskResult.hpp"
namespace eagle0::common {
// Metrics structure for ThreadPool session statistics
struct ThreadPoolMetrics {
size_t tasks_enqueued = 0;
size_t tasks_succeeded = 0;
size_t tasks_deadline_exceeded = 0;
size_t tasks_cancelled = 0;
double average_thread_load = 0.0; // Average percentage of threads busy over time
std::chrono::milliseconds session_duration{0};
};
class ThreadPool {
public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
private:
struct Task {
std::function<void()> function;
TimePoint deadline;
bool has_deadline;
Task(std::function<void()> f, TimePoint d, bool has_d)
: function(std::move(f)),
deadline(d),
has_deadline(has_d) {}
};
std::vector<std::thread> workers;
std::deque<Task> tasks; // Simple FIFO queue instead of priority queue
mutable std::mutex queue_mutex; // mutable for const methods like queue_size()
std::condition_variable condition;
std::atomic<bool> stop{false};
// Metrics tracking
mutable std::mutex metrics_mutex; // mutable for const methods like isSessionActive()
bool session_active = false;
TimePoint session_start;
std::atomic<size_t> tasks_enqueued{0};
std::atomic<size_t> tasks_succeeded{0};
std::atomic<size_t> tasks_deadline_exceeded{0};
std::atomic<size_t> tasks_cancelled{0};
std::atomic<size_t> active_threads{0};
// Thread load tracking
std::vector<std::pair<TimePoint, size_t>> thread_load_samples; // (timestamp, active_count)
public:
explicit ThreadPool(size_t num_threads = std::thread::hardware_concurrency()) {
for (size_t i = 0; i < num_threads; ++i) {
workers.emplace_back([this] {
while (true) {
Task task{nullptr, TimePoint{}, false};
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this] { return stop.load() || !tasks.empty(); });
if (stop.load() && tasks.empty()) { return; }
if (!tasks.empty()) {
task = std::move(tasks.front());
tasks.pop_front();
} else {
continue;
}
}
// Execute the task (deadline checking is now handled inside the task)
if (task.function) {
// Track thread activity
active_threads++;
recordThreadLoadSample();
task.function();
active_threads--;
recordThreadLoadSample();
}
}
});
}
}
private:
// Helper to record thread load samples
void recordThreadLoadSample() {
if (session_active) {
std::lock_guard<std::mutex> lock(metrics_mutex);
thread_load_samples.emplace_back(Clock::now(), active_threads.load());
}
}
public:
// Enqueue a task without deadline
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<TaskResult<std::invoke_result_t<F, Args...>>> {
using return_type = std::invoke_result_t<F, Args...>;
using result_type = TaskResult<return_type>;
auto actualTask = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
auto task = std::make_shared<std::packaged_task<result_type()>>(
[this, actualTask = std::move(actualTask)]() mutable -> result_type {
result_type res = result_type(actualTask());
// Track completion status
if (session_active) {
switch (res.status) {
case TaskStatus::SUCCESS: tasks_succeeded++; break;
case TaskStatus::DEADLINE_EXCEEDED: tasks_deadline_exceeded++; break;
case TaskStatus::CANCELLED: tasks_cancelled++; break;
}
}
return res;
});
std::future<result_type> result = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if (stop.load()) { throw std::runtime_error("enqueue on stopped ThreadPool"); }
tasks.emplace_back([task]() { (*task)(); }, TimePoint{}, false);
if (session_active) { tasks_enqueued++; }
}
condition.notify_one();
return result;
}
// Enqueue a task with deadline
template<class F>
auto enqueue_with_deadline(F&& f, TimePoint deadline)
-> std::future<TaskResult<std::invoke_result_t<F>>> {
using return_type = std::invoke_result_t<F>;
using result_type = TaskResult<return_type>;
auto actualTask = std::forward<F>(f);
auto task = std::make_shared<std::packaged_task<result_type()>>(
[this, actualTask = std::move(actualTask), deadline]() mutable -> result_type {
result_type res;
if (Clock::now() > deadline) {
res = result_type(return_type{}, TaskStatus::DEADLINE_EXCEEDED);
} else {
res = result_type(actualTask());
}
// Track completion status
if (session_active) {
switch (res.status) {
case TaskStatus::SUCCESS: tasks_succeeded++; break;
case TaskStatus::DEADLINE_EXCEEDED: tasks_deadline_exceeded++; break;
case TaskStatus::CANCELLED: tasks_cancelled++; break;
}
}
return res;
});
std::future<result_type> result = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if (stop.load()) { throw std::runtime_error("enqueue on stopped ThreadPool"); }
tasks.emplace_back([task]() { (*task)(); }, deadline, true);
if (session_active) { tasks_enqueued++; }
}
condition.notify_one();
return result;
}
// Get current queue size (approximate, for monitoring)
size_t queue_size() const {
std::unique_lock<std::mutex> lock(queue_mutex);
return tasks.size();
}
// Get detailed queue information for debugging
void debug_queue_state() const {
std::unique_lock<std::mutex> lock(queue_mutex);
printf("ThreadPool: Queue size: %zu\n", tasks.size());
if (!tasks.empty()) {
int with_deadline = 0;
int without_deadline = 0;
for (const auto& task : tasks) {
if (task.has_deadline) {
with_deadline++;
} else {
without_deadline++;
}
}
printf("ThreadPool: Tasks with deadline: %d, without deadline: %d\n",
with_deadline,
without_deadline);
}
}
// Start a new metrics session
void beginSession() {
std::lock_guard<std::mutex> lock(metrics_mutex);
session_active = true;
session_start = Clock::now();
// Reset all metrics
tasks_enqueued = 0;
tasks_succeeded = 0;
tasks_deadline_exceeded = 0;
tasks_cancelled = 0;
thread_load_samples.clear();
// Record initial thread load
thread_load_samples.emplace_back(session_start, active_threads.load());
}
// End the current session and return metrics
ThreadPoolMetrics endSession() {
std::lock_guard<std::mutex> lock(metrics_mutex);
if (!session_active) {
return ThreadPoolMetrics{}; // Return empty metrics if no session active
}
auto session_end = Clock::now();
session_active = false;
// Record final thread load
thread_load_samples.emplace_back(session_end, active_threads.load());
// Calculate metrics
ThreadPoolMetrics metrics;
metrics.tasks_enqueued = tasks_enqueued.load();
metrics.tasks_succeeded = tasks_succeeded.load();
metrics.tasks_deadline_exceeded = tasks_deadline_exceeded.load();
metrics.tasks_cancelled = tasks_cancelled.load();
metrics.session_duration =
std::chrono::duration_cast<std::chrono::milliseconds>(session_end - session_start);
// Calculate average thread load
if (thread_load_samples.size() >= 2 && workers.size() > 0) {
double total_load_time = 0.0;
auto total_duration =
std::chrono::duration<double>(
thread_load_samples.back().first - thread_load_samples.front().first)
.count();
for (size_t i = 1; i < thread_load_samples.size(); ++i) {
auto duration =
std::chrono::duration<double>(
thread_load_samples[i].first - thread_load_samples[i - 1].first)
.count();
auto load = static_cast<double>(thread_load_samples[i - 1].second) / workers.size();
total_load_time += load * duration;
}
metrics.average_thread_load =
(total_duration > 0) ? (total_load_time / total_duration) : 0.0;
} else {
metrics.average_thread_load = 0.0;
}
return metrics;
}
// Check if a session is currently active
bool isSessionActive() const {
std::lock_guard<std::mutex> lock(metrics_mutex);
return session_active;
}
~ThreadPool() {
stop.store(true);
condition.notify_all();
for (std::thread& worker : workers) {
if (worker.joinable()) { worker.join(); }
}
}
};
} // namespace eagle0::common
#endif // EAGLE0_THREADPOOL_HPP
@@ -8,8 +8,6 @@ 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{};
@@ -17,9 +15,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(kDefaultMorale);
shardokBattalion.mutate_armament(static_cast<float>(battalion.armament()));
shardokBattalion.mutate_training(static_cast<float>(battalion.training()));
shardokBattalion.mutate_morale(battalion.morale());
shardokBattalion.mutate_armament(battalion.armament());
shardokBattalion.mutate_training(battalion.training());
return shardokBattalion;
}
@@ -39,28 +37,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(static_cast<int8_t>(hero.strength()));
shardokHero.mutate_strength_xp(static_cast<int16_t>(hero.strength_xp()));
shardokHero.mutate_strength(hero.strength());
shardokHero.mutate_strength_xp(hero.strength_xp());
shardokHero.mutate_agility(static_cast<int8_t>(hero.agility()));
shardokHero.mutate_agility_xp(static_cast<int16_t>(hero.agility_xp()));
shardokHero.mutate_agility(hero.agility());
shardokHero.mutate_agility_xp(hero.agility_xp());
shardokHero.mutate_constitution(static_cast<int8_t>(hero.constitution()));
shardokHero.mutate_constitution_xp(static_cast<int16_t>(hero.constitution_xp()));
shardokHero.mutate_constitution(hero.constitution());
shardokHero.mutate_constitution_xp(hero.constitution_xp());
shardokHero.mutate_charisma(static_cast<int8_t>(hero.charisma()));
shardokHero.mutate_charisma_xp(static_cast<int16_t>(hero.charisma_xp()));
shardokHero.mutate_charisma(hero.charisma());
shardokHero.mutate_charisma_xp(hero.charisma_xp());
shardokHero.mutate_wisdom(static_cast<int8_t>(hero.wisdom()));
shardokHero.mutate_wisdom_xp(static_cast<int16_t>(hero.wisdom_xp()));
shardokHero.mutate_wisdom(hero.wisdom());
shardokHero.mutate_wisdom_xp(hero.wisdom_xp());
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_integrity(hero.integrity());
shardokHero.mutate_ambition(hero.ambition());
shardokHero.mutate_gregariousness(hero.gregariousness());
shardokHero.mutate_bravery(hero.bravery());
shardokHero.mutate_vigor(static_cast<float>(hero.vigor()));
shardokHero.mutate_starting_vigor(static_cast<float>(hero.vigor()));
shardokHero.mutate_vigor(hero.vigor());
shardokHero.mutate_starting_vigor(hero.vigor());
return shardokHero;
}
@@ -72,14 +70,7 @@ auto ConvertUnit(
Unit shardokUnit{};
shardokUnit.mutate_player_id(shardokPlayerId);
// Range check eagle_player_id for int8 conversion
int32_t eagle_id = unit.eagle_player_id();
if (eagle_id < -128 || eagle_id > 127) {
throw std::runtime_error(
"eagle_player_id " + std::to_string(eagle_id) + " out of int8 range");
}
shardokUnit.mutate_eagle_player_id(static_cast<int8_t>(eagle_id));
shardokUnit.mutate_eagle_player_id(unit.eagle_player_id());
shardokUnit.mutate_hidden(false);
shardokUnit.mutate_fortified(false);
if (unit.has_hero()) {
@@ -95,22 +86,19 @@ auto ConvertUnit(
shardokUnit.mutate_stun_rounds_remaining(0);
for (const PlayerId pid : allPlayerIds) {
shardokUnit.mutable_opponent_knowledge()->Mutate(
static_cast<flatbuffers::uoffset_t>(pid),
0);
shardokUnit.mutable_opponent_knowledge()->Mutate(pid, 0);
}
shardokUnit.mutate_has_moved_in_zoc(false);
shardokUnit.mutate_targeted_unit(-1);
shardokUnit.mutate_volleys_remaining(0);
shardokUnit.mutate_food_remaining(static_cast<float>(unit.food()));
shardokUnit.mutate_food_remaining(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(
static_cast<int8_t>(unit.starting_position_index().value()));
shardokUnit.mutate_starting_position_index(unit.starting_position_index().value());
} else {
shardokUnit.mutate_starting_position_index(-1);
}
@@ -9,10 +9,7 @@
#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 (unsigned int i = 0; i < hexMap->attacker_starting_positions()->size(); i++) {
for (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,9 +5,7 @@
#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,7 +3,6 @@
//
#include <iostream>
#include <memory>
#include "MapInfoCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -53,7 +52,7 @@ auto main(const int argc, char** argv) -> int {
outputStream << " \"positions\": {";
bool firstPosition = true;
for (const auto& [position, count] : mapInfo.positionsRequiringCrossing) {
for (const auto& kv : mapInfo.positionsRequiringCrossing) {
if (firstPosition) {
outputStream << endl;
firstPosition = false;
@@ -61,7 +60,7 @@ auto main(const int argc, char** argv) -> int {
outputStream << "," << endl;
}
outputStream << " \"" << position << "\": " << count;
outputStream << " \"" << kv.first << "\": " << kv.second;
}
outputStream << endl << " }" << endl << " }";
}
@@ -4,8 +4,6 @@
#include "AIAttackGroups.hpp"
#include <iterator>
#include <ranges>
#include <unordered_map>
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -223,15 +221,11 @@ auto GenerateTargetPriorities(
Power(unit);
}
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};
});
tpl.priorityOrder = common::Map(targetsWithDistance, [](const TargetAndDistance& tad) {
return TargetAndAttackLocations{
.target = tad.target,
.attackLocations = tad.attackLocations};
});
}
return allTargetsUnitsAndDistances;
@@ -4,17 +4,15 @@
#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/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
using Unit = net::eagle0::shardok::storage::fb::Unit;
// 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;
constexpr double MAXIMUM_RATIO_FOR_ATTACKER_TO_FLEE = 0.50;
auto AIAttackerStrategySelector::BestAttackerStrategy(
const PlayerId attackerPid,
@@ -24,9 +22,11 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
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{};
@@ -41,6 +41,8 @@ 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;
}
@@ -49,6 +51,7 @@ 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 {
@@ -58,13 +61,7 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
}
AIStrategy chosenStrategy;
// Use sophisticated combat success estimation instead of simple troop ratio
if (canFlee && AIFleeDecisionCalculator::ShouldConsiderFleeing(
attackerPid,
gameState,
settings,
FLEE_CONSIDERATION_THRESHOLD)) {
if (canFlee && attackerTroops < MAXIMUM_RATIO_FOR_ATTACKER_TO_FLEE * defenderTroops) {
chosenStrategy = FleeStrategy;
} else if (const CoordsSet startCrossingLocations =
waterCrossingCommandChooser
@@ -73,10 +70,7 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
chosenStrategy = CrossRiversStrategy(startCrossingLocations);
} else if (attackerUnitCount < criticalTileCoords.size()) {
chosenStrategy = AttackUnitsStrategy(GenerateTargetPriorities(
Occupants(
*gameState->units(),
gameState->hex_map()->row_count(),
gameState->hex_map()->column_count()),
gameState.GetOccupantsVector(),
gameState->hex_map(),
defenderPositions,
attackerPid,
@@ -90,10 +84,7 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
// Otherwise, try to hold the castles.
else if (defenderOccupiedCriticalTileCount > 0) {
chosenStrategy = AttackCastlesStrategy(GenerateTargetPriorities(
Occupants(
*gameState->units(),
gameState->hex_map()->row_count(),
gameState->hex_map()->column_count()),
gameState.GetOccupantsVector(),
gameState->hex_map(),
criticalTileCoords,
attackerPid,
@@ -10,9 +10,12 @@
#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(
@@ -5,6 +5,7 @@
#include "AICommandFilter.hpp"
#include <algorithm>
#include <cmath>
#include "src/main/cpp/net/eagle0/shardok/library/BattalionType.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexCubeUtils.hpp"
@@ -13,15 +14,15 @@
namespace shardok {
using fb::Unit;
using net::eagle0::shardok::common::CommandType;
using net::eagle0::shardok::storage::fb::Unit;
CoordsSet AICommandFilter::BuildEnemyLocations(const GameStateW& gameState, PlayerId pid) {
CoordsSet AICommandFilter::BuildEnemyLocations(const GameState* gameState, PlayerId pid) {
CoordsSet enemyLocations(gameState->hex_map());
const auto* units = gameState->units();
for (size_t i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(static_cast<unsigned int>(i));
for (int i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(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());
@@ -35,7 +36,7 @@ std::vector<size_t> AICommandFilter::FilterCommands(
const CommandListSPtr& commands,
PlayerId pid,
bool isDefender,
const GameStateW& gameState,
const GameState* gameState,
const SettingsGetter& settings,
const APDCache& apdCache) {
std::vector<size_t> filteredIndices;
@@ -103,14 +104,16 @@ bool AICommandFilter::IsWastefulAction(
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameStateW& gameState,
const GameState* gameState,
const SettingsGetter& settings,
const APDCache& apdCache,
const CoordsSet& enemyLocations,
const CoordsSet& castleLocations,
double minDistToEnemies) {
const auto cmdType = cmd.GetCommandType();
// Handle different spell types
switch (cmd.GetCommandType()) {
switch (cmdType) {
case CommandType::METEOR_START_COMMAND: {
// Meteor preparation filtering
// Meteor takes 3 rounds (start -> target -> cast) and locks the mage in place
@@ -209,8 +212,8 @@ bool AICommandFilter::IsWastefulAction(
bool nearObjective = false;
for (const auto& enemyCoords : enemyLocations) {
const Cube enemyCube = OffsetToCube(enemyCoords);
if (const int hexDistance = CubeDistance(unitCube, enemyCube);
hexDistance <= 3) {
const int hexDistance = CubeDistance(unitCube, enemyCube);
if (hexDistance <= 3) {
nearObjective = true;
break;
}
@@ -389,8 +392,9 @@ bool AICommandFilter::IsWastefulAction(
static_cast<int8_t>(targetCoords.column())};
// Check if any enemy occupies the fire location - let them burn!
const auto* units = gameState->units();
std::vector<PlayerId> allyPids; // Empty for now - assume 2-player game
if (gameState.GetKnownEnemyOccupant(pid, allyPids, fireLocation)) {
if (KnownEnemyOccupant(pid, units, allyPids, fireLocation)) {
return true; // Don't extinguish fires under enemies
}
break;
@@ -406,7 +410,7 @@ bool AICommandFilter::IsWastefulMovement(
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameStateW& gameState,
const GameState* gameState,
const SettingsGetter& settings,
const APDCache& apdCache,
const CoordsSet& enemyLocations,
@@ -486,12 +490,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 GameState* 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
@@ -499,15 +503,15 @@ bool AICommandFilter::IsStrategicBlunder(
}
double AICommandFilter::MinDistanceToEnemyUnits(
const GameStateW& gameState,
const GameState* gameState,
PlayerId pid,
const CoordsSet& enemyLocations) {
// Calculate minimum distance from any player unit to any enemy unit
double minDistance = std::numeric_limits<double>::max();
const auto* units = gameState->units();
for (size_t i = 0; i < units->size(); ++i) {
const auto* playerUnit = units->Get(static_cast<unsigned int>(i));
for (int i = 0; i < units->size(); ++i) {
const auto* playerUnit = units->Get(i);
if (playerUnit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
playerUnit->player_id() == pid) {
const auto& playerCoords = playerUnit->location();
@@ -525,7 +529,7 @@ double AICommandFilter::MinDistanceToEnemyUnits(
}
double AICommandFilter::MinDistanceToCastles(
const GameStateW& gameState,
const GameState* gameState,
PlayerId pid,
const CoordsSet& castleLocations) {
// Calculate minimum distance from any player unit to any castle
@@ -537,8 +541,8 @@ double AICommandFilter::MinDistanceToCastles(
}
// Find minimum hex distance from any player unit to any castle
for (size_t i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(static_cast<unsigned int>(i));
for (int i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(i);
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->player_id() == pid) {
const auto& unitCoords = unit->location();
@@ -556,7 +560,7 @@ double AICommandFilter::MinDistanceToCastles(
}
bool AICommandFilter::IsPlayerOutnumbered(
const GameStateW& gameState,
const GameState* gameState,
PlayerId pid,
double threshold) {
const int playerUnitCount = CountPlayerUnits(gameState, pid);
@@ -568,12 +572,12 @@ bool AICommandFilter::IsPlayerOutnumbered(
return ratio < threshold;
}
int AICommandFilter::CountPlayerUnits(const GameStateW& gameState, PlayerId pid) {
int AICommandFilter::CountPlayerUnits(const GameState* gameState, PlayerId pid) {
int count = 0;
const auto* units = gameState->units();
for (size_t i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(static_cast<unsigned int>(i));
for (int i = 0; i < units->size(); ++i) {
const auto* unit = units->Get(i);
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->player_id() == pid) {
count++;
@@ -584,9 +588,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 GameState* gameState) {
// Simplified implementation - return false for now
// TODO: Implement proper castle abandonment detection when API is available
return false;
@@ -40,20 +40,20 @@ public:
const CommandListSPtr& commands,
PlayerId pid,
bool isDefender,
const GameStateW& gameState,
const GameState* gameState,
const SettingsGetter& settings,
const APDCache& apdCache);
private:
// Helper to build enemy locations once for efficiency
static CoordsSet BuildEnemyLocations(const GameStateW& gameState, PlayerId pid);
static CoordsSet BuildEnemyLocations(const GameState* gameState, PlayerId pid);
// Spell preparation filters
static bool IsWastefulAction(
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameStateW& gameState,
const GameState* gameState,
const SettingsGetter& settings,
const APDCache& apdCache,
const CoordsSet& enemyLocations,
@@ -65,7 +65,7 @@ private:
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameStateW& gameState,
const GameState* gameState,
const SettingsGetter& settings,
const APDCache& apdCache,
const CoordsSet& enemyLocations,
@@ -76,29 +76,27 @@ private:
const ShardokCommand& cmd,
PlayerId pid,
bool isDefender,
const GameStateW& gameState,
const GameState* gameState,
const SettingsGetter& settings,
double minDistToEnemies);
// Helper functions for distance and position analysis
static double MinDistanceToEnemyUnits(
const GameStateW& gameState,
const GameState* gameState,
PlayerId pid,
const CoordsSet& enemyLocations);
static double MinDistanceToCastles(
const GameStateW& gameState,
const GameState* gameState,
PlayerId pid,
const CoordsSet& castleLocations);
static bool IsPlayerOutnumbered(const GameStateW& gameState, PlayerId pid, double threshold);
static bool IsPlayerOutnumbered(const GameState* gameState, PlayerId pid, double threshold);
static int CountPlayerUnits(const GameStateW& gameState, PlayerId pid);
static int CountPlayerUnits(const GameState* gameState, PlayerId pid);
static bool WouldAbandonCriticalCastle(
const ShardokCommand& cmd,
PlayerId pid,
const GameStateW& gameState);
static bool
WouldAbandonCriticalCastle(const ShardokCommand& cmd, PlayerId pid, const GameState* gameState);
};
} // namespace shardok
@@ -4,9 +4,6 @@
#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"
@@ -17,7 +14,7 @@ constexpr double MAXIMUM_RATIO_FOR_DEFENDER_TO_FLEE = 0.15;
constexpr double MINIMUM_RATIO_FOR_DEFENDER_TO_HOLD = 0.60;
auto AIDefenderStrategySelector::BestDefenderStrategy(
const GameStateW& gameState,
const GameState* gameState,
const CoordsSet& criticalTileCoords,
const APDCache& apdCache,
const SettingsGetter& settings) -> AIStrategy {
@@ -60,9 +57,7 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
++attackerNonUndeadUnitCount;
if (!std::ranges::contains(
attackerUnitIdsRequiringWaterCrossing,
unit->unit_id())) {
if (!common::Contains(attackerUnitIdsRequiringWaterCrossing, unit->unit_id())) {
++attackerNonUndeadUnitNotRequiringWaterCrossingCount;
}
}
@@ -7,15 +7,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/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 GameStateW& gameState,
const GameState* gameState,
const CoordsSet& criticalTileCoords,
const APDCache& apdCache,
const SettingsGetter& settings) -> AIStrategy;
@@ -1,228 +0,0 @@
//
// 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
@@ -1,67 +0,0 @@
//
// AIFleeDecisionCalculator.hpp
// eagle0
//
// Handles AI flee decision logic including combat success estimation
// and flee vs fight evaluation for final round scenarios
//
#ifndef AIFleeDecisionCalculator_hpp
#define AIFleeDecisionCalculator_hpp
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
namespace shardok {
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
class AIFleeDecisionCalculator {
public:
// Configuration for flee decision thresholds
struct FleeThresholds {
int minimumFleeOddsThreshold; // Minimum flee success odds to consider fleeing
int desperateFleeThreshold; // Flee threshold when combat is hopeless
};
// Result of flee vs fight evaluation
struct FleeDecision {
bool shouldFlee;
size_t commandIndex; // Index of command to execute (flee or fight)
const char* reasoning; // Debug explanation of decision
};
// Evaluate whether to flee or fight in the final round
[[nodiscard]] static auto EvaluateFleeVsFight(
PlayerId playerId,
const SettingsGetter& settings,
const GameStateW& guessedState,
const vector<CommandProto>& availableCommands,
const vector<CommandProto>::const_iterator& fleeCommand,
bool enableDebugLogging = false) -> FleeDecision;
// Estimate probability of combat success for the attacker
[[nodiscard]] static auto EstimateCombatSuccess(
PlayerId attackerPlayerId,
const GameStateW& guessedState,
const SettingsGetter& settings) -> double;
// Determine if the attacker should consider fleeing based on combat odds
// Returns true if fleeing should be considered as an option
[[nodiscard]] static auto ShouldConsiderFleeing(
PlayerId attackerPlayerId,
const GameStateW& guessedState,
const SettingsGetter& settings,
double fleeConsiderationThreshold = 0.5) -> bool;
private:
// Helper to get flee command index
[[nodiscard]] static auto GetFleeCommandIndex(
const vector<CommandProto>::const_iterator& fleeCommand,
const vector<CommandProto>& availableCommands) -> size_t;
};
} // namespace shardok
#endif /* AIFleeDecisionCalculator_hpp */
File diff suppressed because it is too large Load Diff
@@ -5,13 +5,11 @@
#ifndef EAGLE0_AISCORECALCULATOR_HPP
#define EAGLE0_AISCORECALCULATOR_HPP
#include <chrono>
#include <future>
#include "src/main/cpp/net/eagle0/common/TaskResult.hpp"
#include "src/main/cpp/net/eagle0/common/ThreadPool.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
@@ -33,13 +31,103 @@ using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
class AIScoreCalculator {
public:
// Start a new metrics collection session
static void BeginMetricsSession();
struct IndexAndScore {
size_t index;
CommandType type;
ScoreValue lookaheadScore;
ScoreValue immediateScore;
};
// End the current session and return metrics
static eagle0::common::ThreadPoolMetrics EndMetricsSession();
// Evaluate the score of a guessed game state based on the current AI strategy. DOES NOT perform
// or evaluate any commands.
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 GameState *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:
[[nodiscard]] static auto GuessedStateScore(
bool isDefender,
const GameStateW &state,
@@ -49,7 +137,19 @@ public:
const APDCache &apdCache,
const ALCache &alCache) -> ScoreValue;
// Evaluates the score for a particular command index for the given player, using lookahead.
[[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;
[[nodiscard]] static auto CommandScore(
PlayerId pid,
bool isDefender,
@@ -62,9 +162,7 @@ public:
const CoordsSet &allCastleCoords,
const APDCache &apdCache,
const ALCache &alCache,
size_t commandIndex,
std::chrono::steady_clock::time_point deadline)
-> std::future<eagle0::common::TaskResult<ScoreValue>>;
size_t commandIndex) -> ScoreValue;
};
} // namespace shardok
@@ -16,7 +16,7 @@ auto HasAttachedHeroWithProfession(
unit->attached_hero().profession_info().profession() == profession;
}
auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int {
auto CastleClaimCapableAttackerUnitCount(const GameState *gameState) -> int {
int count = 0;
for (const auto *unit : *gameState->units()) {
@@ -32,7 +32,7 @@ auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int {
return count;
}
auto PlayerInfoForPid(const GameStateW &gs, const PlayerId pid) -> const PlayerInfo * {
auto PlayerInfoForPid(const GameState *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,7 +7,6 @@
#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"
@@ -26,8 +25,8 @@ auto HasAttachedHeroWithProfession(
const Unit *unit,
net::eagle0::shardok::storage::fb::Profession profession) -> bool;
auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int;
auto PlayerInfoForPid(const GameStateW &, PlayerId pid) -> const PlayerInfo *;
auto CastleClaimCapableAttackerUnitCount(const GameState *gameState) -> int;
auto PlayerInfoForPid(const GameState *gs, PlayerId pid) -> const PlayerInfo *;
} // namespace shardok
@@ -32,8 +32,8 @@ auto CalculateTimeBudget(
bool isClose = false;
const auto *units = state->units();
for (size_t i = 0; i < units->size() && !isClose; ++i) {
const auto *myUnit = units->Get(static_cast<unsigned int>(i));
for (int i = 0; i < units->size() && !isClose; ++i) {
const auto *myUnit = units->Get(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 (size_t j = 0; j < units->size(); ++j) {
const auto *enemyUnit = units->Get(static_cast<unsigned int>(j));
for (int j = 0; j < units->size(); ++j) {
const auto *enemyUnit = units->Get(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 size_t minDepth = settingsGetter.Backing().min_lookahead_turns();
const int 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)
size_t minDepthRequired; // Minimum depth from minLookaheadTurns
int minDepthRequired; // Minimum depth from minLookaheadTurns
bool isCloseToEnemy; // Proximity flag for budget selection
};
@@ -7,6 +7,8 @@
#include <algorithm>
#include "AIAttackLocations.hpp"
#include "AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
@@ -98,7 +100,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 +115,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;
}
@@ -131,8 +133,8 @@ auto lightningValue(const Unit *unit) -> double {
auto meteorDropRawValue(
const Coords &targetCoords,
const HexMap *map,
const vector<const Unit *> &enemyOccupants,
const vector<const Unit *> &friendlyOccupants) -> double {
const vector<const Unit *> &occupants,
const Unit *castingUnit) -> double {
double rawMeteorDropValue = 0.0;
if (targetCoords.row() >= 0) {
const double castleMultiplier = GetTerrain(map, targetCoords)->modifier().castle().present()
@@ -140,13 +142,16 @@ auto meteorDropRawValue(
: 1.0;
const int index = targetCoords.row() * map->column_count() + targetCoords.column();
if (const Unit *directOccupant = enemyOccupants[index]) {
rawMeteorDropValue += directOccupant->battalion().size() * kMeteorDirectTargetingEnemy *
castleMultiplier;
}
if (const Unit *directFriendly = friendlyOccupants[index]) {
rawMeteorDropValue += directFriendly->battalion().size() *
kMeteorDirectTargetingFriendly * castleMultiplier;
if (const Unit *directOccupant = occupants[index]) {
if (directOccupant->player_id() == castingUnit->player_id()) {
// Friendly unit
rawMeteorDropValue += directOccupant->battalion().size() *
kMeteorDirectTargetingFriendly * castleMultiplier;
} else {
// Enemy unit
rawMeteorDropValue += directOccupant->battalion().size() *
kMeteorDirectTargetingEnemy * castleMultiplier;
}
}
for (const auto &splashCoords : HexMapUtils::GetAdjacentCoords(map, targetCoords)) {
@@ -157,14 +162,16 @@ auto meteorDropRawValue(
const auto splashIndex =
splashCoords.row() * map->column_count() + splashCoords.column();
if (const Unit *splashOccupant = enemyOccupants[splashIndex]) {
rawMeteorDropValue += splashOccupant->battalion().size() *
kMeteorSplashTargetingEnemy * splashCastleMultiplier;
}
if (const Unit *splashFriendly = friendlyOccupants[splashIndex]) {
rawMeteorDropValue += splashFriendly->battalion().size() *
kMeteorSplashTargetingFriendly * splashCastleMultiplier;
if (const Unit *splashOccupant = occupants[splashIndex]) {
if (splashOccupant->player_id() == castingUnit->player_id()) {
// Friendly unit
rawMeteorDropValue += splashOccupant->battalion().size() *
kMeteorSplashTargetingFriendly * splashCastleMultiplier;
} else {
// Enemy unit
rawMeteorDropValue += splashOccupant->battalion().size() *
kMeteorSplashTargetingEnemy * splashCastleMultiplier;
}
}
}
} else {
@@ -177,14 +184,12 @@ auto meteorDropRawValue(
auto bestTargetValue(
const Unit *unit,
const HexMap *map,
const vector<const Unit *> &enemyOccupants,
const vector<const Unit *> &friendlyOccupants,
const vector<const Unit *> &occupants,
const int meteorRange) -> double {
double maxValue = 0.0;
for (const Coords &target : TilesWithinDistance(map, unit->location(), meteorRange)) {
if (const double thisTargetValue =
meteorDropRawValue(target, map, enemyOccupants, friendlyOccupants);
if (const double thisTargetValue = meteorDropRawValue(target, map, occupants, unit);
thisTargetValue > maxValue) {
maxValue = thisTargetValue;
}
@@ -197,20 +202,15 @@ auto meteorValue(
const Unit *unit,
const int roundsRemaining,
const HexMap *map,
const vector<const Unit *> &enemyUnits,
const vector<const Unit *> &friendlyUnits,
const vector<const Unit *> &occupants,
const int meteorRange,
const double minVigorToCast) -> double {
const auto enemyOccupants = Occupants(enemyUnits, map->row_count(), map->column_count());
const auto friendlyOccupants = Occupants(friendlyUnits, map->row_count(), map->column_count());
switch (unit->attached_hero().profession_info().meteor_cast_state()) {
case net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE:
case net::eagle0::shardok::storage::fb::MultiroundMagicState_START: {
if ((roundsRemaining > 8 ||
(unit->attached_hero().vigor() > 50 && roundsRemaining > 4))) {
const double maxValue =
bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
const double maxValue = bestTargetValue(unit, map, occupants, meteorRange);
double multiplier = kMeteorStartRoundMultiplier;
if (unit->attached_hero().profession_info().meteor_cast_state() ==
@@ -233,18 +233,18 @@ auto meteorValue(
return meteorDropRawValue(
unit->attached_hero().profession_info().cast_target(),
map,
enemyOccupants,
friendlyOccupants);
occupants,
unit);
} else {
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
return bestTargetValue(unit, map, occupants, meteorRange);
}
case net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST:
return kMeteorCastRoundMultiplier *
meteorDropRawValue(
unit->attached_hero().profession_info().cast_target(),
map,
enemyOccupants,
friendlyOccupants);
occupants,
unit);
default: break;
}
@@ -252,6 +252,7 @@ auto meteorValue(
return 0.0;
}
// Original version that accepts unit vectors for backward compatibility
auto GetRangedAttackBonus(
const Unit *unit,
const bool isAttacker,
@@ -263,10 +264,35 @@ auto GetRangedAttackBonus(
const vector<const Unit *> &defenderUnits,
const int meteorRange,
const double minVigorToCast) -> double {
vector<double> rangedAttackValues{};
// Create occupants vector for meteor calculations
vector<const Unit *> allUnits;
allUnits.insert(allUnits.end(), attackerUnits.begin(), attackerUnits.end());
allUnits.insert(allUnits.end(), defenderUnits.begin(), defenderUnits.end());
const auto occupants = Occupants(allUnits, map->row_count(), map->column_count());
return GetRangedAttackBonus(
unit,
isAttacker,
map,
terrain,
attackLocations,
roundsRemaining,
occupants,
meteorRange,
minVigorToCast);
}
const auto &enemyUnits = isAttacker ? defenderUnits : attackerUnits;
const auto &friendlyUnits = isAttacker ? attackerUnits : defenderUnits;
// Optimized version that accepts occupants vector directly
auto GetRangedAttackBonus(
const Unit *unit,
const bool isAttacker,
const HexMap *map,
const Terrain *terrain,
const AttackLocations &attackLocations,
const int roundsRemaining,
const vector<const Unit *> &occupants,
const int meteorRange,
const double minVigorToCast) -> double {
vector<double> rangedAttackValues{};
const auto &unitLocation = unit->location();
@@ -280,14 +306,8 @@ auto GetRangedAttackBonus(
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE) {
rangedAttackValues.push_back(lightningValue(unit));
} else {
const double mv = meteorValue(
unit,
roundsRemaining,
map,
enemyUnits,
friendlyUnits,
meteorRange,
minVigorToCast);
const double mv =
meteorValue(unit, roundsRemaining, map, occupants, meteorRange, minVigorToCast);
rangedAttackValues.push_back(mv);
}
}
@@ -325,16 +345,29 @@ auto GetRangedAttackBonus(
auto UnitValue(
const Unit *unit,
const bool isAttacker,
const vector<const Unit *> &attackerUnits,
const GameStateW &gameState,
const bool attackerWantsCastles,
const bool includeCastleBonus,
const vector<const Unit *> &defenderUnits,
const HexMap *map,
const int roundsRemaining,
const AttackLocations &locationsThisSideCanAttackFrom,
const CoordsSet &locationsInDangerFromEnemy,
const ActionPointDistances *distances,
const SettingsGetter &settings) -> ScoreValue {
const HexMap *map = gameState->hex_map();
// Get attacker and defender units from the game state
vector<const Unit *> attackerUnits{};
vector<const Unit *> defenderUnits{};
for (const Unit *u : *gameState->units()) {
if (u->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
const auto *pi = PlayerInfoForPid(gameState, u->player_id());
if (pi == nullptr) continue;
if (pi->is_defender()) {
defenderUnits.push_back(u);
} else {
attackerUnits.push_back(u);
}
}
const auto &location = unit->location();
if (location.row() < 0) return 0; // unplaced unit
@@ -376,8 +409,7 @@ auto UnitValue(
terrain,
locationsThisSideCanAttackFrom,
roundsRemaining,
attackerUnits,
defenderUnits,
gameState.GetOccupantsVector(),
settings.Backing().meteor_range(),
settings.Backing().meteor_cast_vigor_cost());
@@ -5,6 +5,7 @@
#ifndef EAGLE0_AIUNITSCORECALCULATOR_HPP
#define EAGLE0_AIUNITSCORECALCULATOR_HPP
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistances.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -34,14 +35,24 @@ auto GetRangedAttackBonus(
int meteorRange,
double minVigorToCast) -> double;
// Overload that accepts occupants vector for cached performance
auto GetRangedAttackBonus(
const Unit *unit,
bool isAttacker,
const HexMap *map,
const Terrain *terrain,
const AttackLocations &attackLocations,
int roundsRemaining,
const vector<const Unit *> &occupants,
int meteorRange,
double minVigorToCast) -> double;
auto UnitValue(
const Unit *unit,
bool isAttacker,
const vector<const Unit *> &attackerUnits,
const GameStateW &gameState,
bool attackerWantsCastles,
bool includeCastleBonus,
const vector<const Unit *> &defenderUnits,
const HexMap *map,
int roundsRemaining,
const AttackLocations &locationsThisSideCanAttackFrom,
const CoordsSet &locationsInDangerFromEnemy,
@@ -4,11 +4,9 @@
#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"
@@ -127,14 +125,13 @@ auto DefenderHoldsCriticalTilesVictoryScore(
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();
const auto cc = gameState->hex_map()->column_count();
const auto occupants = Occupants(*gameState->units(), rc, cc);
const auto& occupants = gameState.GetOccupantsVector();
for (const Coords& criticalTileLocation : criticalTileLocations) {
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
@@ -180,9 +177,8 @@ auto AttackerHoldsCriticalTilesVictoryScore(
ScoreValue total = 0.0;
const auto rc = gameState->hex_map()->row_count();
const auto cc = gameState->hex_map()->column_count();
const auto occupants = Occupants(*gameState->units(), rc, cc);
const auto& occupants = gameState.GetOccupantsVector();
for (const Coords& criticalTileLocation : criticalTileLocations) {
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
@@ -253,7 +249,7 @@ auto LastPlayerStandingVictoryScore(
const APDCache& apdCache,
const ALCache& alCache,
const SettingsGetter& settings) -> ScoreValue {
if (!std::ranges::contains(
if (!common::Contains(
*player->victory_conditions(),
net::eagle0::shardok::storage::fb::
VictoryCondition_VICTORY_CONDITION_LAST_PLAYER_STANDING)) {
@@ -11,7 +11,7 @@
namespace shardok {
auto UnitIdsRequiringWaterCrossing(
const GameStateW &gameState,
const GameState *gameState,
const PlayerId pid,
const CoordsSet &destinations,
const APDCache &apdCache,
@@ -74,9 +74,9 @@ auto UnitIdsRequiringWaterCrossing(
}
auto UnitIdsToCreateWaterCrossing(
const GameStateW &gameState,
const GameState *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 GameStateW &gameState,
const GameState *gameState,
const vector<UnitId> &unitIdsCreatingCrossing,
const CoordsSet &tilesToStartCrossingFrom,
const MapId &mapId,
@@ -5,7 +5,6 @@
#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"
@@ -30,7 +29,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 GameStateW& gameState,
const GameState* gameState,
PlayerId pid,
const CoordsSet& destinations,
const APDCache& apdCache,
@@ -38,7 +37,7 @@ auto UnitIdsRequiringWaterCrossing(
// Units belonging to the player that are capable of creating water crossings
auto UnitIdsToCreateWaterCrossing(
const GameStateW& gameState,
const GameState* gameState,
PlayerId pid,
const APDCache& apdCache,
const SettingsGetter& settings) -> vector<UnitId>;
@@ -68,7 +67,7 @@ auto WaterCrossingTiles(
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
auto IntendedCrossingStarts(
const GameStateW& gameState,
const GameState* gameState,
const vector<UnitId>& unitIdsCreatingCrossing,
const CoordsSet& tilesToStartCrossingFrom,
const MapId& mapId,
@@ -4,10 +4,8 @@
#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 {
@@ -19,10 +17,10 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
[[nodiscard]] auto AIWaterCrossingCommandChooser::WaterCrossingScore(
const SettingsGetter &settingsGetter,
const GameStateW &gameState,
const GameState *gameState,
const CoordsSet &castleCoords,
const CoordsSet &startCrossingFrom) const -> ScoreValue {
uint32_t castleClaimCount = 0;
int castleClaimCount = 0;
for (const auto *unit : *gameState->units()) {
if (unit->player_id() != playerId) continue;
const auto status = unit->status();
@@ -85,7 +83,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 (std::ranges::contains(unitIdsCreatingCrossing, uid)) continue;
if (common::Contains(unitIdsCreatingCrossing, uid)) continue;
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = settingsGetter.GetBattalionType(unit->battalion().type());
@@ -121,11 +119,11 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
auto AIWaterCrossingCommandChooser::StartCrossingFrom(
const SettingsGetter &settingsGetter,
const GameStateW &gameState,
const GameState *gameState,
const CoordsSet &castleCoords) const -> CoordsSet {
CoordsSet startCrossingFrom(gameState->hex_map());
uint32_t castleClaimCount = 0;
int castleClaimCount = 0;
for (const auto *unit : *gameState->units()) {
if (unit->player_id() != playerId) continue;
const auto status = unit->status();
@@ -8,7 +8,6 @@
#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"
@@ -35,12 +34,12 @@ public:
auto StartCrossingFrom(
const SettingsGetter &settingsGetter,
const GameStateW &gameState,
const GameState *gameState,
const CoordsSet &castleCoords) const -> CoordsSet;
[[nodiscard]] auto WaterCrossingScore(
const SettingsGetter &settingsGetter,
const GameStateW &gameState,
const GameState *gameState,
const CoordsSet &castleCoords,
const CoordsSet &startCrossingFrom) const -> ScoreValue;
};
+4 -28
View File
@@ -11,10 +11,10 @@ cc_library(
],
deps = [
":ai_attack_locations",
":ai_flee_decision_calculator",
":ai_score_utilities",
":ai_strategy",
":ai_water_crossing_command_chooser",
"//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",
@@ -71,7 +71,6 @@ 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",
@@ -122,32 +121,12 @@ 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"],
@@ -183,9 +162,8 @@ cc_library(
":ai_unit_score_calculator",
":ai_victory_condition_score_calculator",
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
"//src/main/cpp/net/eagle0/common:task_result",
"//src/main/cpp/net/eagle0/common:thread_pool",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_guesser",
],
)
@@ -215,6 +193,8 @@ cc_library(
],
deps = [
":ai_attack_locations",
":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/util:hex_map_utils",
],
@@ -252,7 +232,6 @@ 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",
@@ -270,7 +249,6 @@ 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",
@@ -310,7 +288,6 @@ cc_library(
":ai_score_calculator",
":ai_time_budget",
":ai_water_crossing_command_chooser",
"//src/main/cpp/net/eagle0/common:task_result",
"//src/main/cpp/net/eagle0/common:time_utils",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
@@ -327,7 +304,6 @@ cc_library(
deps = [
":ai_attacker_strategy_selector",
":ai_defender_strategy_selector",
":ai_flee_decision_calculator",
":ai_iterative_deepening",
":ai_score_calculator",
":ai_time_budget",
@@ -5,14 +5,12 @@
#include "IterativeDeepeningAI.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
#include <utility>
#include "AIAttackerStrategySelector.hpp"
#include "AIScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/common/TaskResult.hpp"
#include "src/main/cpp/net/eagle0/common/TimeUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
@@ -45,26 +43,11 @@ auto IterativeDeepeningAI::IterativeSearch(
const auto initialBudgetMs = initialBudget.remainingBudget;
SearchResult result;
// Start ThreadPool metrics session
AIScoreCalculator::BeginMetricsSession();
if (commands.empty()) {
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
printf("ID AI: Commands are empty, returning early\n");
#endif
result.searchCompleted = true;
// End session and print metrics (only if session was long enough to be interesting)
auto metrics = AIScoreCalculator::EndMetricsSession();
if (metrics.session_duration.count() >= 100) {
printf("ThreadPool Metrics (empty commands):\n");
printf(" Tasks enqueued: %zu\n", metrics.tasks_enqueued);
printf(" Tasks succeeded: %zu\n", metrics.tasks_succeeded);
printf(" Tasks deadline exceeded: %zu\n", metrics.tasks_deadline_exceeded);
printf(" Average thread load: %.1f%%\n", metrics.average_thread_load * 100.0);
printf(" Session duration: %lldms\n", metrics.session_duration.count());
}
return result;
}
@@ -72,7 +55,7 @@ auto IterativeDeepeningAI::IterativeSearch(
bool isSetupPhase =
(state->status()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP);
size_t maxDepth = isSetupPhase ? 2 : std::numeric_limits<int>::max();
int maxDepth = isSetupPhase ? 2 : std::numeric_limits<int>::max();
// Calculate current utility and create engine once for all command evaluations
const auto& settingsGetter = settings->GetGetter();
@@ -93,40 +76,31 @@ auto IterativeDeepeningAI::IterativeSearch(
highestDepthCompleted.clear();
highestDepthCompleted.resize(commands.size(), 0);
size_t currentDepth = 1;
int currentDepth = 1;
size_t previousBestCommand = 0; // Track best command from previous depth
size_t evaluatedCountAtHighestDepth = 0;
auto completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
EvaluationCompletionReason completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
// Main iterative deepening loop
while ((currentDepth == 1 || !IsTimeExpired(timeBudget)) && currentDepth <= maxDepth) {
// Track depth timing
auto depthStartTime = std::chrono::steady_clock::now();
auto elapsedSinceStart =
std::chrono::duration_cast<std::chrono::milliseconds>(depthStartTime - startTime);
printf("ID AI: Starting depth %zu at %lldms\n", currentDepth, elapsedSinceStart.count());
// Get command indices sorted by best score from previous depth
std::vector<size_t> sortedIndices = GetCommandsSortedByPreviousDepth(
currentDepth,
scoresByDepth,
highestDepthCompleted);
size_t evaluatedCount = 0;
int evaluatedCount = 0;
bool allEvaluated = true;
bool allEndTurnCommands = true; // Track if all commands are END_TURN
// Start all command evaluations for this depth
std::vector<std::pair<size_t, std::future<SearchResult>>> futures;
futures.reserve(sortedIndices.size());
// Try to evaluate all commands at this depth, within budget constraints
for (size_t cmdIndex : sortedIndices) {
if (currentDepth > 1 && IsTimeExpired(timeBudget)) {
allEvaluated = false;
break;
}
auto future = SearchCommandAtDepthWithEngine(
auto cmdResult = SearchCommandAtDepthWithEngine(
guessedEngine,
settingsGetter,
maxRepeatCount,
@@ -136,42 +110,18 @@ auto IterativeDeepeningAI::IterativeSearch(
currentUtility,
timeBudget);
futures.emplace_back(cmdIndex, std::move(future));
}
auto afterTaskSubmission = std::chrono::steady_clock::now();
auto submissionTime = std::chrono::duration_cast<std::chrono::milliseconds>(
afterTaskSubmission - depthStartTime);
printf("ID AI: Submitted %zu tasks for depth %zu (took %lldms)\n",
futures.size(),
currentDepth,
submissionTime.count());
// Now wait for all futures and collect results
printf("ID AI: Waiting for %zu futures at depth %zu\n", futures.size(), currentDepth);
auto waitStartTime = std::chrono::steady_clock::now();
for (auto& [cmdIndex, future] : futures) {
auto cmdResult = future.get();
// Only record results for successfully completed evaluations
if (cmdResult.searchCompleted) {
// Ensure scoresByDepth[cmdIndex] has enough space
if (scoresByDepth[cmdIndex].size() <= currentDepth) {
scoresByDepth[cmdIndex].resize(currentDepth + 1);
}
scoresByDepth[cmdIndex][currentDepth] = cmdResult.bestScore;
highestDepthCompleted[cmdIndex] = currentDepth;
evaluatedCount++;
// Check if this command is not END_TURN_COMMAND
if (commands[cmdIndex].type() != net::eagle0::shardok::common::END_TURN_COMMAND) {
allEndTurnCommands = false;
}
// Ensure scoresByDepth[cmdIndex] has enough space
if (scoresByDepth[cmdIndex].size() <= currentDepth) {
scoresByDepth[cmdIndex].resize(currentDepth + 1);
}
scoresByDepth[cmdIndex][currentDepth] = cmdResult.bestScore;
highestDepthCompleted[cmdIndex] = currentDepth;
evaluatedCount++;
// Check if this command is not END_TURN_COMMAND
if (commands[cmdIndex].type() != net::eagle0::shardok::common::END_TURN_COMMAND) {
allEndTurnCommands = false;
}
// If searchCompleted is false, we don't increment evaluatedCount or update
// highestDepthCompleted This means the iterative deepening logic will correctly handle
// incomplete evaluations
}
// Find the best command at current depth and check if it changed
@@ -192,13 +142,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 %lu:\n", currentDepth);
printf(" Depth %lu best: command %zu (score %.2f) - %s\n",
printf("ID AI: Best command changed at depth %d:\n", currentDepth);
printf(" Depth %d best: command %zu (score %.2f) - %s\n",
currentDepth - 1,
previousBestCommand,
scoresByDepth[previousBestCommand][currentDepth - 1],
commands[previousBestCommand].DebugString().c_str());
printf(" Depth %lu best: command %zu (score %.2f) - %s\n",
printf(" Depth %d best: command %zu (score %.2f) - %s\n",
currentDepth,
currentBestCommand,
currentBestScore,
@@ -209,24 +159,6 @@ auto IterativeDeepeningAI::IterativeSearch(
previousBestCommand = currentBestCommand;
}
// Log depth completion timing
auto depthEndTime = std::chrono::steady_clock::now();
auto depthDuration = std::chrono::duration_cast<std::chrono::milliseconds>(
depthEndTime - depthStartTime);
auto waitDuration =
std::chrono::duration_cast<std::chrono::milliseconds>(depthEndTime - waitStartTime);
auto totalElapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(depthEndTime - startTime);
printf("ID AI: Completed depth %zu at %lldms (depth took %lldms, wait took %lldms, "
"evaluated %zu/%zu)\n",
currentDepth,
totalElapsed.count(),
depthDuration.count(),
waitDuration.count(),
evaluatedCount,
sortedIndices.size());
// Only proceed to next depth if we completed all commands at current depth
if (!allEvaluated) {
completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
@@ -243,12 +175,12 @@ auto IterativeDeepeningAI::IterativeSearch(
// This indicates we've hit END_TURN in the lookahead
if (currentDepth > 1 && evaluatedCount > 0) {
bool scoresUnchanged = true;
size_t unchangedCount = 0;
int 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 (size_t cmdIndex = sortedIndices[i];
scoresByDepth[cmdIndex].size() > currentDepth &&
if (scoresByDepth[cmdIndex].size() > currentDepth &&
scoresByDepth[cmdIndex].size() > currentDepth - 1) {
// Check if score changed between depth N-1 and depth N
if (std::abs(
@@ -270,14 +202,12 @@ auto IterativeDeepeningAI::IterativeSearch(
}
// Check if we've used more than 50% of total budget
auto totalElapsedCheck = std::chrono::steady_clock::now() - startTime;
auto totalElapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(totalElapsedCheck);
double budgetUsedPercent = static_cast<double>(totalElapsedMs.count()) /
static_cast<double>(initialBudgetMs.count());
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();
if (budgetUsedPercent > 0.5) {
printf("ID AI: Stopping after depth %lu - used %.1f%% of time budget\n",
printf("ID AI: Stopping after depth %d - used %.1f%% of time budget\n",
currentDepth,
budgetUsedPercent * 100);
completionReason = EvaluationCompletionReason::NOT_ENOUGH_TIME_TO_CONTINUE;
@@ -312,28 +242,55 @@ auto IterativeDeepeningAI::IterativeSearch(
result.availableCommandCount);
}
// End session and print ThreadPool metrics (only for sessions >= 100ms)
auto metrics = AIScoreCalculator::EndMetricsSession();
if (metrics.session_duration.count() >= 100) {
printf("ThreadPool Metrics (depth %zu, %s):\n",
result.depthAchieved,
completionReason == EvaluationCompletionReason::RAN_OUT_OF_TIME ? "timeout"
: completionReason == EvaluationCompletionReason::RAN_OUT_OF_COMMANDS ? "complete"
: completionReason == EvaluationCompletionReason::NOT_ENOUGH_TIME_TO_CONTINUE
? "no_time"
: "unknown");
printf(" Tasks enqueued: %zu\n", metrics.tasks_enqueued);
printf(" Tasks succeeded: %zu\n", metrics.tasks_succeeded);
printf(" Tasks deadline exceeded: %zu\n", metrics.tasks_deadline_exceeded);
printf(" Tasks cancelled: %zu\n", metrics.tasks_cancelled);
printf(" Average thread load: %.1f%%\n", metrics.average_thread_load * 100.0);
printf(" Session duration: %lldms\n", metrics.session_duration.count());
printf(" Tasks per ms: %.2f\n",
metrics.session_duration.count() > 0 ? static_cast<double>(metrics.tasks_enqueued) /
metrics.session_duration.count()
: 0.0);
return result;
}
auto IterativeDeepeningAI::SearchAtDepth(
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
const int depth) const -> SearchResult {
SearchResult result;
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
printf("SearchAtDepth: depth=%d, commands=%zu\n", depth, commands.size());
#endif
if (commands.empty()) {
result.searchCompleted = true;
return result;
}
const auto& settingsGetter = settings->GetGetter();
const auto guessedEngine = ShardokEngine(settings, state);
const auto maxRepeatCount = settingsGetter.Backing().ai_utility_repeat_count();
const ScoreValue currentUtility = AIScoreCalculator::GuessedStateScore(
isDefender,
state,
strategy,
castleCoords,
settingsGetter,
apdCache,
alCache);
// Perform search at specified depth
const auto indexAndScore = AIScoreCalculator::BestCommandIndex(
playerId,
isDefender,
depth, // Use the specified depth for lookahead
maxRepeatCount,
guessedEngine,
strategy,
currentUtility,
settingsGetter,
castleCoords,
apdCache,
alCache);
result.bestCommandIndex = indexAndScore.index;
result.bestScore = indexAndScore.lookaheadScore;
result.searchCompleted = true;
return result;
}
@@ -341,6 +298,40 @@ bool IterativeDeepeningAI::IsTimeExpired(const AITimeBudget& budget) {
return budget.remainingBudget <= std::chrono::milliseconds(0);
}
auto IterativeDeepeningAI::SearchAllCommandsAtDepth(
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
const int depth) const -> std::vector<SearchResult> {
// Use SearchAtDepth to get the best overall result
const auto bestResult = SearchAtDepth(settings, state, commands, depth);
std::vector<SearchResult> results;
results.reserve(commands.size());
for (size_t i = 0; i < commands.size(); ++i) {
SearchResult result;
result.bestCommandIndex = i;
result.depthAchieved = depth;
result.searchCompleted = true;
result.minimumDepthCompleted = true;
result.availableCommandCount = bestResult.availableCommandCount;
result.commandCountEvaluated = bestResult.commandCountEvaluated;
// For the best command, use the actual score
// For others, use a slightly lower score (this is a simplification for Phase 2)
if (i == bestResult.bestCommandIndex) {
result.bestScore = bestResult.bestScore;
} else {
result.bestScore = bestResult.bestScore * 0.95; // Slightly lower but reasonable
}
results.push_back(result);
}
return results;
}
auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
const ShardokEngine& guessedEngine,
const GameSettings::Getter& settingsGetter,
@@ -349,7 +340,7 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
const size_t commandIndex,
const int depth,
const ScoreValue currentUtility,
AITimeBudget& timeBudget) const -> std::future<SearchResult> {
AITimeBudget& timeBudget) const -> SearchResult {
SearchResult result;
result.bestCommandIndex = commandIndex;
result.depthAchieved = depth;
@@ -360,9 +351,7 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
if (commandIndex >= commands.size()) {
result.bestScore = 0.0;
std::promise<SearchResult> p;
p.set_value(result);
return p.get_future();
return result;
}
try {
@@ -370,11 +359,8 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
AIEvaluationCounter counter;
const auto startTime = std::chrono::steady_clock::now();
// Calculate deadline from remaining time budget
const auto deadline = startTime + timeBudget.remainingBudget;
// Get the future from CommandScore - don't wait yet
auto commandScoreFuture = AIScoreCalculator::CommandScore(
// Use CommandScore to evaluate the specific command at the given depth
const auto commandScore = AIScoreCalculator::CommandScore(
playerId,
isDefender,
depth,
@@ -386,15 +372,11 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
castleCoords,
apdCache,
alCache,
commandIndex,
deadline);
// Calculate time and adjust budget before waiting
// This is needed because we need to update timeBudget synchronously
const auto commandResult = commandScoreFuture.get();
commandIndex);
// Calculate time used and adjust based on concurrent evaluations
const auto elapsed = std::chrono::steady_clock::now() - startTime;
const int concurrentCount = AIEvaluationCounter::GetCurrentCount();
const int concurrentCount = counter.GetCurrentCount();
const auto adjustedElapsed = elapsed / std::max(1, concurrentCount);
const auto adjustedElapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(adjustedElapsed);
@@ -402,15 +384,7 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
// Deduct adjusted time from remaining budget
timeBudget.remainingBudget -= adjustedElapsedMs;
// Check if we got a valid result or if evaluation failed/timed out
if (!commandResult.succeeded()) {
// Command evaluation failed or timed out - mark as incomplete
result.bestScore = 0.0;
result.searchCompleted = false;
result.minimumDepthCompleted = false;
} else {
result.bestScore = commandResult.value;
}
result.bestScore = commandScore;
} catch (const std::exception& e) {
// If evaluation fails, return a neutral score rather than crashing
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
@@ -419,15 +393,13 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
result.bestScore = 0.0;
}
std::promise<SearchResult> p;
p.set_value(result);
return p.get_future();
return result;
}
auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
const size_t currentDepth,
int currentDepth,
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<size_t>& highestDepthCompleted) -> std::vector<size_t> {
const std::vector<int>& highestDepthCompleted) const -> std::vector<size_t> {
std::vector<size_t> indices(scoresByDepth.size());
std::iota(indices.begin(), indices.end(), 0);
@@ -437,8 +409,8 @@ auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
}
// Sort by score at previous depth
const size_t prevDepth = currentDepth - 1;
std::ranges::sort(indices, [&](const size_t a, const size_t b) {
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
if (highestDepthCompleted[a] >= prevDepth && highestDepthCompleted[b] >= prevDepth) {
return scoresByDepth[a][prevDepth] > scoresByDepth[b][prevDepth];
@@ -452,7 +424,7 @@ auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
auto IterativeDeepeningAI::SelectBestResult(
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<size_t>& highestDepthCompleted) -> SearchResult {
const std::vector<int>& highestDepthCompleted) const -> SearchResult {
SearchResult result;
result.bestScore = -std::numeric_limits<ScoreValue>::infinity();
result.searchCompleted = false;
@@ -460,8 +432,9 @@ 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) {
const size_t depth = highestDepthCompleted[i];
if (ScoreValue score = scoresByDepth[i][depth]; score > result.bestScore) {
int depth = highestDepthCompleted[i];
ScoreValue score = scoresByDepth[i][depth];
if (score > result.bestScore) {
result.bestScore = score;
result.bestCommandIndex = i;
result.depthAchieved = depth;
@@ -6,7 +6,6 @@
#define EAGLE0_ITERATIVEDEEPENINGAI_HPP
#include <chrono>
#include <future>
#include <vector>
#include "AIStrategy.hpp"
@@ -36,7 +35,7 @@ public:
struct SearchResult {
size_t bestCommandIndex;
ScoreValue bestScore;
size_t depthAchieved;
int depthAchieved;
std::chrono::milliseconds timeUsed;
bool minimumDepthCompleted;
bool searchCompleted;
@@ -68,7 +67,7 @@ public:
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
const AITimeBudget& initialBudget) const;
const AITimeBudget& timeBudget) const;
private:
PlayerId playerId;
@@ -80,12 +79,24 @@ private:
// Reusable vectors to reduce memory allocations
mutable std::vector<std::vector<ScoreValue>> scoresByDepth;
mutable std::vector<size_t> highestDepthCompleted;
mutable std::vector<int> highestDepthCompleted;
mutable std::vector<size_t> reusableSortedIndices;
[[nodiscard]] SearchResult SearchAtDepth(
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
int depth) const;
[[nodiscard]] static bool IsTimeExpired(const AITimeBudget& budget);
[[nodiscard]] std::future<SearchResult> SearchCommandAtDepthWithEngine(
[[nodiscard]] std::vector<SearchResult> SearchAllCommandsAtDepth(
const GameSettingsSPtr& settings,
const GameStateW& state,
const std::vector<CommandProto>& commands,
int depth) const;
[[nodiscard]] SearchResult SearchCommandAtDepthWithEngine(
const ShardokEngine& guessedEngine,
const GameSettings::Getter& settingsGetter,
int maxRepeatCount,
@@ -95,14 +106,14 @@ private:
ScoreValue currentUtility,
AITimeBudget& timeBudget) const;
[[nodiscard]] static std::vector<size_t> GetCommandsSortedByPreviousDepth(
size_t currentDepth,
[[nodiscard]] std::vector<size_t> GetCommandsSortedByPreviousDepth(
int currentDepth,
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<size_t>& highestDepthCompleted);
const std::vector<int>& highestDepthCompleted) const;
[[nodiscard]] static SearchResult SelectBestResult(
[[nodiscard]] SearchResult SelectBestResult(
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<size_t>& highestDepthCompleted);
const std::vector<int>& highestDepthCompleted) const;
};
} // namespace shardok
@@ -8,14 +8,10 @@
#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"
@@ -30,7 +26,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();
@@ -46,29 +42,7 @@ ShardokAIClient::ShardokAIClient(
: playerId(playerId),
isDefender(isDefender),
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
waterCrossingCommandChooser(playerId, apdCache) {
// Pre-generate the most common cache entries for better performance
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
// Pre-fetch for all battalion types, both with and without brave water
using BattalionTypeId = net::eagle0::shardok::storage::fb::BattalionTypeId;
for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
typeId <= BattalionTypeId::BattalionTypeId_MAX;
typeId++) {
const auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
const auto battalionType = settings.GetBattalionType(battalionTypeId);
// Pre-fetch without brave water (braveWaterActionPointCost = -1)
apdCache->GetRaw(hexMap, mapId, battalionType, false, -1);
// Pre-fetch with brave water (includeBravingWater = true, braveWaterActionPointCost = 0)
apdCache->GetRaw(hexMap, mapId, battalionType, true, 0);
}
// Consolidate all the pre-fetched entries into the persistent cache
apdCache->ConsolidateThreadLocalCache_Racy();
}
waterCrossingCommandChooser(playerId, apdCache) {}
void CheckCommand(const CommandProto &realDescriptor, const CommandProto &guessedDescriptor) {
string diff;
@@ -100,7 +74,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
const auto commandCount = guessedCommands.size();
assert(commandCount == realAvailableCommands.size());
for (size_t i = 0; i < commandCount; i++) {
for (int i = 0; i < commandCount; i++) {
CheckCommand(realAvailableCommands[i], guessedCommands[i]);
}
@@ -179,41 +153,23 @@ auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateW &guessedState,
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
const auto fleeCommand = std::ranges::find_if(
realAvailableCommands,
[](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
return cmd.type() == net::eagle0::shardok::common::FLEE_COMMAND;
});
if (fleeCommand == realAvailableCommands.end()) {
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()) {
return LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
}
// Use the flee decision calculator
const auto fleeDecision = AIFleeDecisionCalculator::EvaluateFleeVsFight(
playerId,
settings->GetGetter(),
guessedState,
realAvailableCommands,
fleeCommand,
#ifdef DEBUG_FLEE_DECISIONS
true // Enable debug logging
#else
false
#endif
);
if (fleeDecision.shouldFlee) {
CommandChoiceResults results{};
results.chosenIndex = fleeDecision.commandIndex;
results.availableCommandCount = realAvailableCommands.size();
results.depthAchieved = 1; // Heuristic choice
results.commandCountEvaluated = 1; // Only evaluated one command type
results.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
return results;
} else {
// Fight instead of flee
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
CommandChoiceResults results{};
results.chosenIndex =
static_cast<size_t>(std::distance(realAvailableCommands.begin(), fleeCommand));
results.availableCommandCount = realAvailableCommands.size();
results.depthAchieved = 1; // Simple heuristic choice
results.commandCountEvaluated = 1; // Only evaluated one command type
results.completionReason =
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS; // Heuristic choice
return results;
}
}
@@ -275,7 +231,6 @@ auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const
const auto &gsv = engine.GetGameStateView(GetPlayerId());
const auto results = ChooseCommandIndex(settings, gsv, availableCommands);
apdCache->ConsolidateThreadLocalCache_Racy();
return results;
}
}
@@ -56,7 +56,6 @@ 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,
@@ -13,7 +13,6 @@
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
using namespace shardok;
@@ -81,10 +80,6 @@ int main(int argc, char* argv[]) {
// Set exec path so FilesystemUtils can find resource files
FilesystemUtils::SetExecPath(argv[0]);
// Set cache directory for ActionPointDistances
FixedActionPointDistances::SetCacheDirectory(
FilesystemUtils::CacheFilesDirectory() + "apdCache/");
try {
std::cout << "Shardok AI Performance Runner\n";
std::cout << "==============================\n";
@@ -5,10 +5,7 @@
#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,11 +8,9 @@
#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"
@@ -87,10 +85,10 @@ void ShardokGameController::LockedNotifyClients() const { updateCondition.notify
auto ShardokGameController::LockedAIClientForPid(PlayerId pid) const
-> shared_ptr<ShardokAIClient> {
const auto it = std::ranges::find_if(aiClients, [pid](const auto &client) {
return client->GetPlayerId() == pid;
});
return (it != aiClients.end()) ? *it : nullptr;
return common::FindIf(
aiClients,
[pid](const auto &client) { return client->GetPlayerId() == pid; })
.value_or(nullptr);
}
void ShardokGameController::DoAIThread() {
@@ -167,7 +165,7 @@ void ShardokGameController::PostCommand(
CheckFactionId(engine, shardokPlayerId, eagleFactionId);
const auto expectedToken = static_cast<int64_t>(engine->GetUnfilteredHistoryCount());
const auto expectedToken = engine->GetUnfilteredHistoryCount();
if (token < expectedToken) {
printf("Double token in postCommand\n");
// The client is missing some updates; probably it's a double-submit
@@ -195,7 +193,7 @@ void ShardokGameController::PostPlacementCommands(
CheckFactionId(engine, shardokPlayerId, eagleFactionId);
const auto expectedToken = static_cast<int64_t>(engine->GetUnfilteredHistoryCount());
const auto expectedToken = engine->GetUnfilteredHistoryCount();
if (token < expectedToken) {
printf("Double token in postPlacementCommands\n");
// The client is missing some updates; probably it's a double-submit
@@ -242,11 +240,9 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
incomingRegistrations--;
}
updates.mainResults.reserve(awrs.size());
std::ranges::transform(
awrs,
std::back_inserter(updates.mainResults),
[](const ShardokActionWithResultingState &a) { return a.action_result(); });
updates.mainResults = common::Map(awrs, [](const ShardokActionWithResultingState &a) {
return a.action_result();
});
const auto playerInfos = engine->GetPlayerInfos();
updates.filteredResults.reserve(playerInfos.size() + 1);
@@ -8,9 +8,6 @@
#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"
@@ -135,30 +132,30 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
}
if (battType->adjustsMorale &&
unit->battalion().morale() < settings.Backing().minimum_morale_to_act()) {
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->CanDoWithLowMorale();
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->CanDoWithLowMorale();
});
}
if (unit->stun_rounds_remaining() > 0) {
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->CanDoWhileStunned();
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->CanDoWhileStunned();
});
}
if (hasHero && unit->attached_hero().vigor() < settings.Backing().minimum_vigor_to_act()) {
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->CanDoWithLowVigor();
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->CanDoWithLowVigor();
});
}
if (unitMovedIntoZoc) {
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->CanDoAfterMovingIntoZoc();
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->CanDoAfterMovingIntoZoc();
});
}
if (std::ranges::any_of(oneUnitCommands, [](const CommandSPtr &cmd) {
if (common::ContainsWhere(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->IsRequiredToEndTurn();
})) {
std::erase_if(oneUnitCommands, [](const CommandSPtr &cmd) {
return !cmd->IsRequiredToEndTurn();
common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
return cmd->IsRequiredToEndTurn();
});
}
@@ -186,7 +183,7 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
/* onlyFollowUps=*/false);
}
if (!std::ranges::any_of(commands, [](const CommandSPtr &command) {
if (!common::ContainsWhere(commands, [](const CommandSPtr &command) {
return command->IsRequiredToEndTurn();
})) {
commands.push_back(std::make_shared<EndTurnCommand>(playerId, gameState, settings));
@@ -8,6 +8,7 @@ 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",
@@ -1,63 +1,137 @@
//
// Created by Dan Crosby on 2025-01-21.
// Created by Dan Crosby on 2025-01-17.
//
#include "GameStateW.hpp"
#include <algorithm>
#include <ranges>
#include <stdexcept>
#include <utility>
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
namespace shardok {
auto GameStateW::GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
-> const Unit* {
const auto* state = Get();
if (!state || !state->hex_map()) { return nullptr; }
/**
* @class OccupantsCache
* @brief Cache for efficient unit location lookups.
*
* This cache maintains a vector indexed by map position (row * columnCount + column)
* containing pointers to units at each position. This converts O(n) unit lookups
* into O(1) operations.
*/
class OccupantsCache {
public:
OccupantsCache(const GameStateW& gameState) { Regenerate(gameState); }
const int16_t rowCount = state->hex_map()->row_count();
const int16_t columnCount = state->hex_map()->column_count();
void Regenerate(const GameStateW& gameState) {
const auto* state = gameState.Get();
if (!state || !state->hex_map() || !state->units()) {
occupants_.clear();
rowCount_ = 0;
columnCount_ = 0;
return;
}
// Check bounds
if (coords.row() < 0 || coords.row() >= rowCount || coords.column() < 0 ||
coords.column() >= columnCount) {
return nullptr;
}
rowCount_ = state->hex_map()->row_count();
columnCount_ = state->hex_map()->column_count();
const size_t mapSize = rowCount_ * columnCount_;
// Fast path: use bitfield cache if available
if (state->occupied_tiles() && !state->occupied_tiles()->empty()) {
const size_t tileIndex =
static_cast<size_t>(coords.row()) * static_cast<size_t>(columnCount) +
static_cast<size_t>(coords.column());
const size_t expectedBitfieldSize =
(static_cast<size_t>(rowCount) * static_cast<size_t>(columnCount) + 7) /
8; // Ceiling division
// Clear and resize the vector
occupants_.clear();
occupants_.resize(mapSize, nullptr);
if (state->occupied_tiles()->size() == expectedBitfieldSize) {
const size_t byteIndex = tileIndex / 8;
const size_t bitOffset = tileIndex % 8;
const uint8_t byte = state->occupied_tiles()->Get(static_cast<unsigned int>(byteIndex));
const bool isOccupied = (byte & (1 << bitOffset)) != 0;
if (!isOccupied) {
return nullptr; // Fast path: definitely no unit here (90% of cases)
// Populate the cache
for (const auto* unit : *state->units()) {
if (unit &&
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
const auto& location = unit->location();
if (location.row() >= 0 && location.row() < rowCount_ && location.column() >= 0 &&
location.column() < columnCount_) {
const size_t index = location.row() * columnCount_ + location.column();
occupants_[index] = unit;
}
}
}
}
// Slow path: O(n) search through units
// Used when bitfield not available OR when bitfield indicates occupation
if (!state->units()) { return nullptr; }
for (size_t i = 0; i < state->units()->size(); ++i) {
const auto* unit = state->units()->Get(static_cast<unsigned int>(i));
if (unit && unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->location().row() == coords.row() &&
unit->location().column() == coords.column()) {
return unit;
[[nodiscard]] auto GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
-> const GameStateW::Unit* {
if (coords.row() < 0 || coords.row() >= rowCount_ || coords.column() < 0 ||
coords.column() >= columnCount_) {
return nullptr;
}
const size_t index = coords.row() * columnCount_ + coords.column();
return occupants_[index];
}
return nullptr;
[[nodiscard]] auto GetOccupantsVector() const -> const std::vector<const GameStateW::Unit*>& {
return occupants_;
}
private:
std::vector<const GameStateW::Unit*> occupants_;
int16_t rowCount_ = 0;
int16_t columnCount_ = 0;
};
// GameStateW implementation
GameStateW::GameStateW() : BaseType() { InitializeCache(); }
GameStateW::GameStateW(const GameStateW& other)
: BaseType(other),
occupantsCache_(other.occupantsCache_) {
// Share the cache - no need to rebuild!
}
GameStateW::GameStateW(GameStateW&& other) noexcept
: BaseType(std::move(other)),
occupantsCache_(std::move(other.occupantsCache_)) {}
GameStateW& GameStateW::operator=(const GameStateW& other) {
if (this != &other) {
BaseType::operator=(other);
occupantsCache_ = other.occupantsCache_; // Share the cache
}
return *this;
}
GameStateW& GameStateW::operator=(GameStateW&& other) noexcept {
if (this != &other) {
BaseType::operator=(std::move(other));
occupantsCache_ = std::move(other.occupantsCache_);
}
return *this;
}
GameStateW::GameStateW(const BaseType& base) : BaseType(base) { InitializeCache(); }
GameStateW::GameStateW(BaseType&& base) : BaseType(std::move(base)) { InitializeCache(); }
GameStateW::GameStateW(flatbuffers::FlatBufferBuilder& fbb) : BaseType(fbb) { InitializeCache(); }
GameStateW::~GameStateW() = default;
auto GameStateW::GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
-> const Unit* {
if (!occupantsCache_) {
throw std::runtime_error(
"GameStateW::GetOccupant() called with uninitialized occupants cache. "
"This indicates a bug where the cache was invalidated without being rebuilt. "
"Call RebuildOccupantsCache() or InitializeCache() after any mutations.");
}
return occupantsCache_->GetOccupant(coords);
}
auto GameStateW::GetOccupantsVector() const -> const std::vector<const Unit*>& {
if (!occupantsCache_) {
throw std::runtime_error(
"GameStateW::GetOccupantsVector() called with uninitialized occupants cache. "
"This indicates a bug where the cache was invalidated without being rebuilt. "
"Call RebuildOccupantsCache() or InitializeCache() after any mutations.");
}
return occupantsCache_->GetOccupantsVector();
}
auto GameStateW::GetKnownEnemyOccupant(
@@ -67,69 +141,20 @@ auto GameStateW::GetKnownEnemyOccupant(
const auto* occupant = GetOccupant(coords);
if (occupant) {
if (!occupant->hidden() && occupant->player_id() != playerId &&
!std::ranges::contains(allyPids, occupant->player_id())) {
!common::Contains(allyPids, occupant->player_id())) {
return occupant;
}
}
return nullptr;
}
void GameStateW::UpdateOccupiedTile(
const net::eagle0::shardok::storage::fb::Coords& oldCoords,
const net::eagle0::shardok::storage::fb::Coords& newCoords) {
const auto* state = Get();
auto* mutableOccupiedTiles = (*this)->mutable_occupied_tiles();
if (!state || !state->hex_map() || !mutableOccupiedTiles) { return; }
const int16_t rowCount = state->hex_map()->row_count();
const int16_t columnCount = state->hex_map()->column_count();
// Clear old position in bitfield
if (oldCoords.row() >= 0 && oldCoords.row() < rowCount && oldCoords.column() >= 0 &&
oldCoords.column() < columnCount) {
const size_t tileIndex =
static_cast<size_t>(oldCoords.row()) * static_cast<size_t>(columnCount) +
static_cast<size_t>(oldCoords.column());
const size_t byteIndex = tileIndex / 8;
const size_t bitOffset = tileIndex % 8;
if (byteIndex < mutableOccupiedTiles->size()) {
uint8_t byte = mutableOccupiedTiles->Get(static_cast<unsigned int>(byteIndex));
byte &= ~(1 << bitOffset); // Clear the bit
mutableOccupiedTiles->Mutate(static_cast<unsigned int>(byteIndex), byte);
}
}
// Set new position in bitfield
if (newCoords.row() >= 0 && newCoords.row() < rowCount && newCoords.column() >= 0 &&
newCoords.column() < columnCount) {
const size_t tileIndex =
static_cast<size_t>(newCoords.row()) * static_cast<size_t>(columnCount) +
static_cast<size_t>(newCoords.column());
const size_t byteIndex = tileIndex / 8;
const size_t bitOffset = tileIndex % 8;
if (byteIndex < mutableOccupiedTiles->size()) {
uint8_t byte = mutableOccupiedTiles->Get(static_cast<unsigned int>(byteIndex));
byte |= (1 << bitOffset); // Set the bit
mutableOccupiedTiles->Mutate(static_cast<unsigned int>(byteIndex), byte);
}
}
void GameStateW::RebuildOccupantsCache() {
occupantsCache_.reset();
occupantsCache_ = std::make_shared<OccupantsCache>(*this);
}
auto GameStateW::GetOccupiedTilesBitfield() const -> const flatbuffers::Vector<uint8_t>* {
const auto* state = Get();
if (!state || !state->hex_map()) { return nullptr; }
void GameStateW::InvalidateOccupantsCache() { occupantsCache_.reset(); }
if (!state->occupied_tiles() || state->occupied_tiles()->empty()) { return nullptr; }
// Verify the bitfield size matches expected map size
const int16_t rowCount = state->hex_map()->row_count();
const int16_t columnCount = state->hex_map()->column_count();
const size_t expectedBitfieldSize =
(static_cast<size_t>(rowCount) * static_cast<size_t>(columnCount) + 7) / 8;
if (state->occupied_tiles()->size() != expectedBitfieldSize) { return nullptr; }
return state->occupied_tiles();
}
void GameStateW::InitializeCache() { occupantsCache_ = std::make_shared<OccupantsCache>(*this); }
} // namespace shardok
@@ -5,7 +5,8 @@
#ifndef EAGLE0_GAMESTATEW_HPP
#define EAGLE0_GAMESTATEW_HPP
#include <cstdint>
#include <memory>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
@@ -14,6 +15,9 @@
namespace shardok {
// Forward declaration for the cache
class OccupantsCache;
/**
* @class GameStateW
* @brief A wrapper class for the FlatBuffer-generated GameState type.
@@ -26,6 +30,10 @@ namespace shardok {
* This class is part of the shardok namespace and is designed to simplify
* interactions with the GameState FlatBuffer type while maintaining the
* flexibility and functionality of the Wrapper base class.
*
* The class includes an occupants cache for efficient unit location lookups,
* which significantly improves performance for operations that frequently
* query which units occupy specific map coordinates.
*/
class GameStateW : public Wrapper<net::eagle0::shardok::storage::fb::GameState> {
public:
@@ -36,50 +44,56 @@ public:
using BaseType::BaseType;
// Default constructor
GameStateW() : BaseType() {}
GameStateW();
// Copy constructor
GameStateW(const GameStateW& other) : BaseType(other) {}
GameStateW(const GameStateW& other);
// Move constructor
GameStateW(GameStateW&& other) noexcept : BaseType(std::move(other)) {}
GameStateW(GameStateW&& other) noexcept;
// Copy assignment
GameStateW& operator=(const GameStateW& other) {
BaseType::operator=(other);
return *this;
}
GameStateW& operator=(const GameStateW& other);
// Move assignment
GameStateW& operator=(GameStateW&& other) noexcept {
BaseType::operator=(std::move(other));
return *this;
}
GameStateW& operator=(GameStateW&& other) noexcept;
// Constructor from base type
GameStateW(const BaseType& base) : BaseType(base) {}
GameStateW(BaseType&& base) : BaseType(std::move(base)) {}
GameStateW(const BaseType& base);
GameStateW(BaseType&& base);
// Constructor from FlatBufferBuilder
explicit GameStateW(flatbuffers::FlatBufferBuilder& fbb);
// Destructor
~GameStateW();
/**
* @brief Get the unit occupying the specified coordinates using occupied tiles bitfield.
* @brief Get the unit occupying the specified coordinates.
* @param coords The coordinates to check.
* @return Pointer to the unit at the coordinates, or nullptr if none.
*
* Fast path: O(1) bitfield check for empty tiles (~90% of cases).
* Slow path: O(n) unit search only when bitfield indicates occupation (~10% of cases).
* This is an O(1) operation using the internal occupants cache.
*/
[[nodiscard]] auto GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
-> const Unit*;
/**
* @brief Get the known enemy unit occupying the specified coordinates using occupied tiles
* bitfield.
* @brief Get all occupants indexed by their position.
* @return Vector where index = row * columnCount + column contains unit pointer or nullptr.
*
* This provides direct access to the occupants cache for bulk operations.
*/
[[nodiscard]] auto GetOccupantsVector() const -> const std::vector<const Unit*>&;
/**
* @brief Get the known enemy unit occupying the specified coordinates.
* @param playerId The player ID to check enemies for.
* @param allyPids Vector of allied player IDs.
* @param coords The coordinates to check.
* @return Pointer to the enemy unit at the coordinates, or nullptr if none.
*
* Uses the bitfield-optimized GetOccupant() internally.
* This is an O(1) operation using the internal occupants cache.
*/
[[nodiscard]] auto GetKnownEnemyOccupant(
PlayerId playerId,
@@ -87,23 +101,45 @@ public:
const net::eagle0::shardok::storage::fb::Coords& coords) const -> const Unit*;
/**
* @brief Update the occupied tiles bitfield when a unit changes position.
* @param oldCoords The previous coordinates (use {-1, -1} if unit was off-map).
* @param newCoords The new coordinates (use {-1, -1} if unit is now off-map).
* @brief Rebuild the occupants cache.
*
* This method invalidates the existing cache and immediately rebuilds it to reflect
* the current state of units. Call this method after any mutations to the game state
* that might affect unit positions.
*
* @warning This method is NOT thread-safe. It should only be called:
* - After mutations when the GameStateW instance is owned by a single thread
* - When transitioning ownership between threads (with proper synchronization)
*
* Concurrent calls to RebuildOccupantsCache() or calls concurrent with GetOccupant()
* will result in undefined behavior due to race conditions.
*/
void UpdateOccupiedTile(
const net::eagle0::shardok::storage::fb::Coords& oldCoords,
const net::eagle0::shardok::storage::fb::Coords& newCoords);
void RebuildOccupantsCache();
/**
* @brief Get the occupied tiles bitfield for efficient tile occupancy checking.
* @return Pointer to the bitfield data, or nullptr if not available.
* @brief Initialize the occupants cache.
* This rebuilds the cache from the current game state.
*
* Returns the raw bitfield where bit at index (row*column_count + col) indicates
* if that tile is occupied. Useful for caching the bitfield to avoid repeated
* GameStateW lookups in performance-critical code like MoveCommand.
* @warning This method is NOT thread-safe. It should only be called:
* - During GameStateW construction (automatically handled)
* - After mutations when the GameStateW instance is owned by a single thread
* - When transitioning ownership between threads (with proper synchronization)
*
* Concurrent calls to InitializeCache() or calls concurrent with GetOccupant()
* will result in undefined behavior due to race conditions.
*/
[[nodiscard]] auto GetOccupiedTilesBitfield() const -> const flatbuffers::Vector<uint8_t>*;
void InitializeCache();
/**
* @brief Force regeneration of the occupants cache.
* @deprecated Use RebuildOccupantsCache() instead. This method is kept for
* backward compatibility but should not be used in new code.
*/
void InvalidateOccupantsCache();
private:
// The occupants cache implementation - shared across copies for efficiency
std::shared_ptr<OccupantsCache> occupantsCache_;
};
} // namespace shardok
@@ -12,10 +12,7 @@
#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,10 +14,7 @@
#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 {
@@ -45,9 +42,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,8 +9,6 @@
#include "ShardokEngine.hpp"
#include <algorithm>
#include <optional>
#include <ranges>
#include <utility>
#include <vector>
@@ -39,6 +37,15 @@ using net::eagle0::shardok::storage::ShardokActionWithResultingState;
using GameStatusProto = net::eagle0::shardok::common::GameStatus;
using TileModifierProto = net::eagle0::shardok::common::TileModifier;
[[nodiscard]] auto ShardokEngine::GetCurrentGameState() const
-> net::eagle0::shardok::storage::fb::GameState const * {
return gameState.Get();
}
[[nodiscard]] auto ShardokEngine::GetCurrentGameStateW() const -> const GameStateW & {
return gameState;
}
[[nodiscard]] auto ShardokEngine::GetCurrentGameStateBytes() const -> byte_vector {
return gameState.ToByteVector();
}
@@ -250,7 +257,7 @@ auto ShardokEngine::PostWhilePlayerHasOnlyOneOption(
}
auto updateAction = UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
false /* atEndOfRound */);
@@ -264,7 +271,7 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
if (static_cast<uint32_t>(GetCurrentGameState()->current_player()) >=
GetCurrentGameState()->player_infos()->size()) {
ApplyAndAddActionResults(UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
true /* atEndOfRound */)
@@ -288,7 +295,7 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
} else {
const auto updateAction = UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
false /* atEndOfRound */);
@@ -312,19 +319,21 @@ void ShardokEngine::PostPlacementCommands(
availableCommandsFactory->GetPlayerSetupCommands(gameState, player);
// first make sure they're all valid and there are no duplicates
for (size_t i = 0; i < placementInfos.size(); i++) {
for (int i = 0; i < placementInfos.size(); i++) {
const UnitPlacementInfo &pi = placementInfos[i];
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()) {
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()) {
throw ShardokClientErrorException("No such placement info found");
}
// check that we're not double-filling any location or double-placing any unit
for (size_t j = i + 1; j < placementInfos.size(); j++) {
for (int j = i + 1; j < placementInfos.size(); j++) {
const UnitPlacementInfo &other = placementInfos[j];
if (pi.unitId == other.unitId)
@@ -339,18 +348,19 @@ void ShardokEngine::PostPlacementCommands(
// now execute
for (const auto &pi : placementInfos) {
const auto it = std::ranges::find_if(*placementCommands, [pi](const CommandSPtr &cmd) {
auto command = common::FindIf(*placementCommands, [pi](const CommandSPtr &cmd) {
return cmd->GetCommandProto().actor().value() == pi.unitId &&
cmd->GetCommandProto().target() == pi.location;
});
for (vector<ActionResult> onePlacementResults = (*it)->Execute(gameState, randomGenerator);
for (vector<ActionResult> onePlacementResults =
(*command)->Execute(gameState, randomGenerator);
const ActionResultProto &oneResult : onePlacementResults) {
HandleActionResult(oneResult, randomGenerator);
}
}
const auto updateAction = UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
false /* atEndOfRound */);
@@ -375,17 +385,17 @@ void ShardokEngine::PostFinishedPlacementCommand(
const auto placementCommands =
availableCommandsFactory->GetPlayerSetupCommands(gameState, player);
const auto it = std::ranges::find_if(*placementCommands, [](const CommandSPtr &cmd) {
const auto command = common::FindIf(*placementCommands, [](const CommandSPtr &cmd) {
return cmd->GetCommandProto().type() ==
net::eagle0::shardok::common::END_PLAYER_SETUP_COMMAND;
});
if (it == placementCommands->end()) {
if (!command.has_value()) {
throw ShardokClientErrorException("No finish placement command found");
}
cachedAvailableCommands = nullptr;
PostActionUnchecked(*it, randomGenerator, std::nullopt);
PostActionUnchecked(command.value(), randomGenerator, std::nullopt);
}
void ShardokEngine::PostCommand(
@@ -432,7 +442,7 @@ void ShardokEngine::PostActionUnchecked(
}
const auto updateAction = UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
false /* atEndOfRound */);
@@ -454,7 +464,7 @@ void ShardokEngine::HandleActionResult(
const Coords modifiedCoords = FromCoordsProto(modifierWithCoords.coords());
const TileModifierProto &modifier = modifierWithCoords.modifiers();
const Unit *occupant = gameState.GetOccupant(modifiedCoords);
const Unit *occupant = Occupant(GetCurrentGameState()->units(), modifiedCoords);
// Check for swept away hero
if (const Terrain *terrain = GetTerrain(GetCurrentGameState()->hex_map(), modifiedCoords);
occupant && IsWater(terrain->type()) && !IsTraversible(modifier) &&
@@ -599,7 +609,7 @@ auto ShardokEngine::EndGameUnits() const -> vector<net::eagle0::shardok::storage
"Trying to get the end game units before the game is over");
}
const auto &gs = GetCurrentGameState();
const auto *gs = GetCurrentGameState();
vector<net::eagle0::shardok::storage::ResolvedUnit> endgameUnits;
AddUnits(endgameUnits, *gs->units());
@@ -60,14 +60,15 @@ private:
[[nodiscard]] auto HandleUnitFallingIntoWater(
const Terrain *terrain,
const fb::Unit *unit,
const net::eagle0::shardok::storage::fb::Unit *unit,
std::shared_ptr<RandomGenerator> randomGenerator) const -> vector<ActionResult>;
void HandleActionResult(
const ActionResult &actionResult,
const std::shared_ptr<RandomGenerator> &randomGenerator);
[[nodiscard]] auto GetUnit(const UnitId uid) const -> const fb::Unit * {
[[nodiscard]] auto GetUnit(const UnitId uid) const
-> const net::eagle0::shardok::storage::fb::Unit * {
return GetCurrentGameState()->units()->Get(uid);
}
@@ -111,7 +112,8 @@ public:
[[nodiscard]] auto GetGameStateAtStartOfAction(ActionId startingActionId) const -> GameStateW;
[[nodiscard]] auto GetCurrentGameState() const -> const GameStateW & { return gameState; }
[[nodiscard]] auto GetCurrentGameState() const -> fb::GameState const *;
[[nodiscard]] auto GetCurrentGameStateW() const -> const GameStateW &;
[[nodiscard]] auto GetCurrentGameStateBytes() const -> byte_vector;
@@ -125,7 +127,7 @@ public:
// Controller API
[[nodiscard]] auto GetGameHistory(ActionId lastUpdatedActionId) const
-> vector<ShardokActionWithResultingState>;
-> vector<net::eagle0::shardok::storage::ShardokActionWithResultingState>;
[[nodiscard]] auto GetUnfilteredHistoryCount() const -> size_t {
return actionHistory.size() + startingHistoryCount;
@@ -141,7 +143,8 @@ public:
[[nodiscard]] auto GetFilteredGameHistory(PlayerId askingPlayer) const
-> vector<net::eagle0::shardok::api::ActionResultView>;
[[nodiscard]] auto GetUnitById(PlayerId askingPlayer, UnitId unitId) const -> UnitView;
[[nodiscard]] auto GetUnitById(PlayerId askingPlayer, UnitId unitId) const
-> net::eagle0::shardok::api::UnitView;
void PostPlacementCommands(
PlayerId player,
@@ -174,7 +177,7 @@ public:
[[nodiscard]] auto GetMonth() const -> int { return GetCurrentGameState()->month(); }
[[nodiscard]] auto GetPlayerInfos() const -> vector<PlayerInfoProto> {
const auto &currentGameState = GetCurrentGameState();
const auto *currentGameState = GetCurrentGameState();
vector<PlayerInfoProto> protos{};
for (const auto *const piFB : *currentGameState->player_infos()) {
protos.push_back(fb::ToPlayerInfoProto(piFB));
@@ -182,18 +185,18 @@ public:
return protos;
}
[[nodiscard]] auto GetGameStatus() const
-> const net::eagle0::shardok::storage::fb::GameStatus * {
auto GetGameStatus() const -> const net::eagle0::shardok::storage::fb::GameStatus * {
return GetCurrentGameState()->status();
}
[[nodiscard]] auto GetGameSettings() const -> GameSettingsSPtr { return gameSettings; }
auto GetGameSettings() const -> GameSettingsSPtr { return gameSettings; }
static inline auto GameIsOver(const fb::GameStatus *status) -> bool {
static inline auto GameIsOver(const net::eagle0::shardok::storage::fb::GameStatus *status)
-> bool {
return (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY);
}
[[nodiscard]] inline auto GameIsOver() const -> bool { return GameIsOver(GetGameStatus()); }
inline auto GameIsOver() const -> bool { return GameIsOver(GetGameStatus()); }
};
} // namespace shardok
@@ -87,11 +87,11 @@ auto PlayerSetupCommandFactory::AddAvailablePlayerSetupCommands(
if (placedUnits.size() >= 10) return;
if (unplacedUnits.empty()) return;
for (const auto &[unitId, unit] : unplacedUnits) {
for (const auto &kv : unplacedUnits) {
AddAvailablePlaceAndHideUnitCommandsForOneUnit(
existingCommands,
isDefender,
unit,
kv.second,
gameState);
}
}
@@ -5,15 +5,14 @@
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include <algorithm>
#include <chrono>
#include <unordered_map>
#include "src/main/cpp/net/eagle0/common/ByteHasher.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#define CACHE_STATS_LOGGING_ false
#define CACHE_STATS_FREQUENCY_SECONDS_ 1
namespace shardok {
@@ -23,45 +22,22 @@ thread_local ActionPointDistancesCache::TLSCache ActionPointDistancesCache::tlsC
#if CACHE_STATS_LOGGING_
// Thread-local statistics for performance monitoring
thread_local struct {
int persistentHits = 0;
int persistentMisses = 0;
int localHits = 0;
int localMisses = 0;
int sharedAccesses = 0;
int evictionEvents = 0;
int apdLoadedFromFile = 0;
int apdGeneratedFresh = 0;
std::chrono::steady_clock::time_point lastReportTime = std::chrono::steady_clock::now();
} cacheStats;
// Helper function to print stats periodically
static void MaybePrintCacheStats() {
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - cacheStats.lastReportTime).count() >=
CACHE_STATS_FREQUENCY_SECONDS_) {
printf("Thread cache stats: %d persistent hits, %d persistent misses, %d local hits, "
"%d local misses, %d shared accesses, %d eviction events, "
"%d APD loaded from file, %d APD generated fresh\n",
cacheStats.persistentHits,
cacheStats.persistentMisses,
cacheStats.localHits,
cacheStats.localMisses,
cacheStats.sharedAccesses,
cacheStats.evictionEvents,
cacheStats.apdLoadedFromFile,
cacheStats.apdGeneratedFresh);
cacheStats.lastReportTime = now;
}
}
#endif
class BadHashException final : public std::exception {
class BadHashException : public std::exception {
public:
BadHashException() = default;
[[nodiscard]] auto what() const noexcept -> const char* override { return "Bad map hash!"; };
};
constexpr int kBattalionTypeCount = 6;
// Helper function to check if any ice is present on the map
static auto HasIceOnMap(const HexMap* map) -> bool {
return std::ranges::any_of(*map->terrain(), [](const auto* terrain) {
@@ -81,11 +57,13 @@ static auto CreateIceClearedMap(const HexMap* map) -> fb::HexMapW {
// Now modify the ice on the mutable copy
auto* mutableMap = mapCopy.Get();
const auto* terrainVec = mutableMap->mutable_terrain();
auto* terrainVec = mutableMap->mutable_terrain();
for (size_t i = 0; i < terrainVec->size(); i++) {
auto* terrain = terrainVec->GetMutableObject(i);
// Only process tiles with ice
if (auto* terrain = terrainVec->GetMutableObject(i); terrain->modifier().ice().present()) {
if (terrain->modifier().ice().present()) {
terrain->mutable_modifier().mutable_ice().mutate_present(false);
terrain->mutable_modifier().mutable_ice().mutate_integrity(0.0f);
}
@@ -98,11 +76,16 @@ static auto CreateIceClearedMap(const HexMap* map) -> fb::HexMapW {
return mapCopy;
}
ActionPointDistancesCache::ActionPointDistancesCache() {
bravingDistances.resize(kBattalionTypeCount);
noBravingDistances.resize(kBattalionTypeCount);
}
auto ActionPointDistancesCache::MakeCacheKey(
const MapId& mapId,
const BattalionTypeSPtr& battalionType,
const bool includeBravingWater,
const int braveWaterActionPointCost) -> FullCacheKey {
bool includeBravingWater,
int braveWaterActionPointCost) -> FullCacheKey {
return FullCacheKey{
mapId,
static_cast<int>(battalionType->typeId),
@@ -116,13 +99,6 @@ auto ActionPointDistancesCache::GetMapId(const HexMap* map) -> MapId {
return MapId{.terrainTypesId = map->base_hash(), .modifierId = modifierId};
}
void ActionPointDistancesCache::ConsolidateThreadLocalCache_Racy() {
persistentCache.insert(std::begin(sharedDistances), std::end(sharedDistances));
sharedDistances.clear();
// Clear the current thread's cache since persistent cache now has everything
tlsCache.clear();
}
auto ActionPointDistancesCache::GetRaw(
const HexMap* map,
@@ -134,28 +110,22 @@ auto ActionPointDistancesCache::GetRaw(
auto cacheKey =
MakeCacheKey(mapId, battalionType, includeBravingWater, braveWaterActionPointCost);
// Check the persistent map first
if (persistentCache.contains(cacheKey)) {
#if CACHE_STATS_LOGGING_
cacheStats.persistentHits++;
MaybePrintCacheStats();
#endif
// Return directly from persistent cache without TLS insertion
// This avoids the overhead of thread-local storage operations on hot path
return persistentCache.at(cacheKey).rawPtr;
}
#if CACHE_STATS_LOGGING_
cacheStats.persistentMisses++;
#endif
// Check thread-local cache first (no locks needed!)
if (tlsCache.contains(cacheKey)) {
auto localIt = tlsCache.find(cacheKey);
if (localIt != tlsCache.end()) {
#if CACHE_STATS_LOGGING_
cacheStats.localHits++;
MaybePrintCacheStats();
// Print stats every 100 requests to monitor effectiveness
if ((cacheStats.localHits + cacheStats.localMisses) % 100 == 0) {
printf("Thread cache stats: %d local hits, %d misses, %d shared accesses, %d eviction "
"events\n",
cacheStats.localHits,
cacheStats.localMisses,
cacheStats.sharedAccesses,
cacheStats.evictionEvents);
}
#endif
return tlsCache.at(cacheKey).rawPtr; // Raw pointer - zero overhead access!
return localIt->second.rawPtr; // Raw pointer - zero overhead access!
}
#if CACHE_STATS_LOGGING_
@@ -163,18 +133,15 @@ auto ActionPointDistancesCache::GetRaw(
#endif
// Check shared cache before expensive ice-clearing operation
auto& vec = includeBravingWater ? bravingDistances : noBravingDistances;
auto& distancesMap = vec[battalionType->typeId];
shared_ptr<ActionPointDistances> sharedResult;
if (sharedDistances.if_contains(cacheKey, [&sharedResult](const auto& kv) {
if (distancesMap.if_contains(mapId, [&sharedResult](const auto& kv) {
sharedResult = kv.second;
})) {
#if CACHE_STATS_LOGGING_
cacheStats.sharedAccesses++;
#endif
// Cache hit in shared cache - store in thread-local cache and return
tlsCache.emplace(cacheKey, CacheEntry(sharedResult));
#if CACHE_STATS_LOGGING_
MaybePrintCacheStats();
#endif
return sharedResult.get();
}
@@ -182,11 +149,9 @@ auto ActionPointDistancesCache::GetRaw(
const bool hasIce = HasIceOnMap(map);
// Declaring here to keep the copied map in scope
fb::HexMapW iceClearedMap;
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
@@ -194,8 +159,8 @@ auto ActionPointDistancesCache::GetRaw(
mapToUse = iceClearedMap.Get();
}
// Create new pathfinding result using factory method
auto creationResult = FixedActionPointDistances::Create(
// Create new pathfinding result
auto result = std::make_shared<FixedActionPointDistances>(
mapToUse,
mapId.terrainTypesId,
mapId.modifierId,
@@ -203,22 +168,11 @@ auto ActionPointDistancesCache::GetRaw(
includeBravingWater,
braveWaterActionPointCost);
#if CACHE_STATS_LOGGING_
// Track whether this was loaded from file or generated fresh
if (creationResult.loadedFromFile) {
cacheStats.apdLoadedFromFile++;
} else {
cacheStats.apdGeneratedFresh++;
}
#endif
auto result = creationResult.apd;
// Store in shared cache
sharedDistances.lazy_emplace_l(
cacheKey,
[](const auto& /*kv*/) { /* already checked above */ },
[=](const auto& ctor) { ctor(cacheKey, result); });
distancesMap.lazy_emplace_l(
mapId,
[](const auto& kv) { /* already checked above */ },
[=](const auto& ctor) { ctor(mapId, result); });
// Cache result locally for future lookups by this thread
// Store both shared_ptr and raw pointer for hybrid access
@@ -23,12 +23,18 @@ struct MapId {
uint64_t terrainTypesId;
uint64_t modifierId;
friend size_t hash_value(const MapId& id) {
return gtl::HashState::combine(0, id.terrainTypesId, id.modifierId);
}
auto operator==(const MapId& other) const -> bool {
return terrainTypesId == other.terrainTypesId && modifierId == other.modifierId;
}
};
// Unified cache key for both thread-safe and thread-local caches
using APDKey = MapId;
// Extended key for thread-local cache that includes battalion type
struct FullCacheKey {
MapId mapId;
int battalionTypeId;
@@ -45,48 +51,41 @@ struct FullCacheKey {
// Hash function for FullCacheKey
struct FullCacheKeyHash {
size_t operator()(const FullCacheKey& key) const {
// Pack small fields into a single 64-bit value
uint64_t packed = (static_cast<uint64_t>(key.battalionTypeId) << 32) |
(static_cast<uint64_t>(key.braveWaterCost) << 1) |
(key.includeBravingWater ? 1 : 0);
// Hash MapId fields directly instead of going through hash_value(MapId)
return gtl::HashState::combine(0, key.mapId.terrainTypesId, key.mapId.modifierId, packed);
return gtl::HashState::combine(
hash_value(key.mapId),
key.battalionTypeId,
key.includeBravingWater,
key.braveWaterCost);
}
};
class ActionPointDistancesCache {
private:
using APDMap = gtl::parallel_flat_hash_map<
APDKey,
shared_ptr<ActionPointDistances>,
gtl::priv::hash_default_hash<APDKey>,
gtl::priv::hash_default_eq<APDKey>,
std::allocator<std::pair<const APDKey, shared_ptr<ActionPointDistances>>>,
6,
std::mutex>;
vector<APDMap> noBravingDistances;
vector<APDMap> bravingDistances;
// Thread-local cache storing both shared_ptr and raw pointer for hybrid access
// Lifetime guaranteed by shared cache ownership
struct CacheEntry {
shared_ptr<ActionPointDistances> sharedPtr;
const ActionPointDistances* rawPtr;
explicit CacheEntry(shared_ptr<ActionPointDistances> ptr)
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,
@@ -95,11 +94,7 @@ private:
int braveWaterActionPointCost) -> FullCacheKey;
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);
}
explicit ActionPointDistancesCache();
// Returns raw pointer for zero overhead access
// Lifetime guaranteed by shared cache ownership
@@ -112,11 +107,6 @@ public:
static auto GetMapId(const HexMap* map) -> MapId;
// Consolidate the thread-safe cache into the persistent cache and clear
// the current thread's local cache. This is only safe if we know reads
// are not happening from other threads.
void ConsolidateThreadLocalCache_Racy();
// Cache management methods
static void ClearThreadLocalCache();
static size_t GetThreadLocalCacheSize();
@@ -26,24 +26,14 @@ void FixedActionPointDistances::SetCacheDirectory(const string& newDir) {
static thread_local byte_vector _scratch;
FixedActionPointDistances::FixedActionPointDistances(const HexMap* /*map*/, int columnCount)
: ActionPointDistances(columnCount) {}
auto FixedActionPointDistances::Create(
FixedActionPointDistances::FixedActionPointDistances(
const HexMap* map,
int64_t terrainTypesHash,
int64_t modifierHash,
const BattalionTypeSPtr& battalionType,
bool includeBravingWater,
int braveWaterActionPointCost) -> CreationResult {
// Create the object using private constructor
auto apd = std::shared_ptr<FixedActionPointDistances>(
new FixedActionPointDistances(map, map->column_count()));
CreationResult result;
result.apd = apd;
result.loadedFromFile = false;
int braveWaterActionPointCost)
: ActionPointDistances(map->column_count()) {
string path = "";
if (!cacheDirectory.empty()) {
@@ -65,26 +55,22 @@ auto FixedActionPointDistances::Create(
const int indexCount = map->row_count() * map->column_count();
if (!path.empty() && FilesystemUtils::FileExistsAtPath(path)) {
apd->distances.resize(indexCount);
distances.resize(indexCount);
// load from file
const auto& bytes = _scratch.ReplaceWithPath(path);
const auto* ptr = reinterpret_cast<const DIST_T*>(bytes.data());
for (int fromIndex = 0; fromIndex < indexCount; fromIndex++) {
apd->distances[fromIndex].insert(
apd->distances[fromIndex].end(),
&(ptr[0]),
&(ptr[indexCount]));
distances[fromIndex].insert(distances[fromIndex].end(), &(ptr[0]), &(ptr[indexCount]));
ptr += indexCount;
}
result.loadedFromFile = true;
} else {
_scratch.reserve(indexCount * indexCount * sizeof(DIST_T));
vector<std::future<vector<vector<DIST_T>>>> futures(indexCount);
auto braveWaterPossibleCoords =
includeBravingWater ? apd->BraveWaterPossibleCoords(map) : nullptr;
includeBravingWater ? BraveWaterPossibleCoords(map) : nullptr;
int chunkSize = (indexCount + ASYNC_COUNT - 1) / ASYNC_COUNT;
// Break into chunks for async
@@ -97,7 +83,7 @@ auto FixedActionPointDistances::Create(
for (int i = 0; i < chunkSize; i++) {
const auto fromIndex = chunkStartIndex + i;
if (fromIndex >= indexCount) { continue; }
chunkVec.push_back(ActionPointDistances::GenerateDistances(
chunkVec.push_back(GenerateDistances(
fromIndex,
map,
includeBravingWater,
@@ -109,20 +95,18 @@ auto FixedActionPointDistances::Create(
});
}
apd->distances.reserve(indexCount);
distances.reserve(indexCount);
_scratch.clear();
_scratch.reserve(indexCount * indexCount * sizeof(DIST_T));
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
auto resultsVec = futures[chunkIdx].get();
apd->distances.insert(apd->distances.end(), resultsVec.begin(), resultsVec.end());
distances.insert(distances.end(), resultsVec.begin(), resultsVec.end());
for (const auto& r : resultsVec) { _scratch.append(r); }
}
if (!path.empty()) { FilesystemUtils::AtomicallySaveToPath(path, _scratch); }
}
return result;
}
} // namespace shardok
} // namespace shardok
@@ -17,43 +17,31 @@ using std::vector;
using BattalionTypeSPtr = std::shared_ptr<const BattalionType>;
class FixedActionPointDistances final : public ActionPointDistances {
public:
struct CreationResult {
std::shared_ptr<FixedActionPointDistances> apd;
bool loadedFromFile;
};
private:
vector<vector<DIST_T>> distances;
inline static string cacheDirectory = "";
// Private constructor - use Create factory method instead
explicit FixedActionPointDistances(const HexMap *map, int columnCount);
public:
static void SetCacheDirectory(const string &newDir);
// Factory method to create FixedActionPointDistances with metadata
static auto Create(
explicit FixedActionPointDistances(
const HexMap *map,
int64_t terrainTypesHash,
int64_t modifierHash,
const BattalionTypeSPtr &battalionType,
bool includeBravingWater,
int braveWaterActionPointCost = -1) -> CreationResult;
int braveWaterActionPointCost = -1);
~FixedActionPointDistances() override = default;
[[nodiscard]] auto Distance(const int fromIndex, const int toIndex) const -> DIST_T override {
auto Distance(const int fromIndex, const int toIndex) const -> DIST_T override {
return distances[fromIndex][toIndex];
}
[[nodiscard]] auto Distance(const Coords &from, const Coords &to) const -> DIST_T override {
auto Distance(const Coords &from, const Coords &to) const -> DIST_T override {
return Distance(ToIndex(from), ToIndex(to));
}
friend struct CreationResult;
};
} // namespace shardok
@@ -130,8 +130,8 @@ void ApplyResolvedUnit(
}
}
std::erase_if(inoutState.units, [unitId](const auto &unit) {
return unit.unit_id() == unitId;
common::FilterInPlace(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 (static_cast<unsigned int>(unit->unit_id()) >= mutatingState->units()->size()) {
if (unit->unit_id() >= mutatingState->units()->size()) {
// Unit ID beyond vector size - must expand
needsVectorExpansion = true;
break; // No point checking further
@@ -338,18 +338,10 @@ void MutatingApplyResult(
settings);
}
// Capture old position before applying changes
auto *mutableUnit = mutatingGameState->units()->GetMutableObject(changedUnit->unit_id());
const auto oldLocation = mutableUnit->location();
fb::ApplyUnit(mutableUnit, changedUnit, status);
// Update occupied tiles bitfield if position changed
const auto &newLocation = changedUnit->location();
if (oldLocation.row() != newLocation.row() ||
oldLocation.column() != newLocation.column()) {
mutatingGameState.UpdateOccupiedTile(oldLocation, newLocation);
}
fb::ApplyUnit(
mutatingGameState->units()->GetMutableObject(changedUnit->unit_id()),
changedUnit,
status);
if (battalionSizeBefore != battalionSizeAfter) {
if (changedUnit->battalion().type() ==
@@ -400,6 +392,9 @@ void MutatingApplyResult(
mutatingGameState->mutable_possible_chargee_ids()->Mutate(i, -1);
}
// Rebuild the occupants cache since units may have moved or been destroyed
mutatingGameState.RebuildOccupantsCache();
return;
}
@@ -4,8 +4,6 @@
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/GameStateCopier.hpp"
#include <cstring>
namespace shardok {
using Unit = net::eagle0::shardok::storage::fb::Unit;
@@ -34,41 +32,6 @@ auto CopyWithExtraUnits(const GameStateW& original, int additionalCount) -> Game
endGST.units.push_back(unit);
}
// Copy occupied tiles bitfield from original GameState (much faster than O(n) rebuild)
if (startGS->occupied_tiles() && startGS->hex_map()) {
const size_t originalBitfieldSize = startGS->occupied_tiles()->size();
endGST.occupied_tiles.resize(originalBitfieldSize);
// Fast O(bitfield_bytes) copy instead of O(units) rebuild
std::memcpy(
endGST.occupied_tiles.data(),
startGS->occupied_tiles()->data(),
originalBitfieldSize);
} else if (endGST.hex_map) {
// Fallback: create new bitfield only if original doesn't have one
const int16_t rowCount = endGST.hex_map->row_count;
const int16_t columnCount = endGST.hex_map->column_count;
const size_t mapSize = static_cast<size_t>(rowCount) * static_cast<size_t>(columnCount);
const size_t bitfieldSize = (mapSize + 7) / 8; // Ceiling division
endGST.occupied_tiles.resize(bitfieldSize, 0); // Initialize all bits to 0 (empty)
// Populate bitfield based on unit positions (O(n) fallback)
for (const auto& unit : endGST.units) {
if (unit.status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
const auto& location = unit.location();
if (location.row() >= 0 && location.row() < rowCount && location.column() >= 0 &&
location.column() < columnCount) {
const size_t tileIndex =
static_cast<size_t>(location.row()) * static_cast<size_t>(columnCount) +
static_cast<size_t>(location.column());
const size_t byteIndex = tileIndex / 8;
const size_t bitOffset = tileIndex % 8;
endGST.occupied_tiles[byteIndex] |= (1 << bitOffset); // Set the bit
}
}
}
}
flatbuffers::FlatBufferBuilder newFbb;
newFbb.ForceDefaults(true);
newFbb.Finish(net::eagle0::shardok::storage::fb::GameState::Pack(newFbb, &endGST));
@@ -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),
@@ -71,14 +71,15 @@ auto ToVector(const Units &units) -> vector<const Unit *> {
auto FallIntoWaterAction::AdjacentsWithTerrain(
const Coords &location,
const GameState *gameState,
const GameStateW &gameState,
const PlayerId playerId) -> vector<AdjacentWithTerrain> {
const CoordsSet adjacentCoords = HexMapUtils::GetAdjacentCoords(gameState->hex_map(), location);
vector<AdjacentWithTerrain> vec{};
for (const Coords &adjCoords : adjacentCoords) {
const auto *possibleOccupant = Occupant(gameState->units(), adjCoords);
// Note: This function takes GameState*, not GameStateW, so cannot use cache
const auto *possibleOccupant = gameState.GetOccupant(adjCoords);
if (possibleOccupant &&
(possibleOccupant->player_id() == playerId || !possibleOccupant->hidden())) {
continue;
@@ -35,7 +35,7 @@ private:
static auto AdjacentsWithTerrain(
const Coords& location,
const GameState* gameState,
const GameStateW& gameState,
PlayerId playerId) -> vector<AdjacentWithTerrain>;
[[nodiscard]] auto InternalExecute(
@@ -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,9 +4,6 @@
#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"
@@ -68,7 +65,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 (!std::ranges::contains(winningIds, unit->player_id()) && unit->hidden() &&
if (!common::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 =
@@ -83,7 +80,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
@@ -151,7 +148,7 @@ auto UpdateGameStatusAction::InternalExecute(
for (const PlayerId pid2 : survivors) {
if (pid1 == pid2) continue;
if (!std::ranges::any_of(
if (!common::ContainsWhere(
*p1Info->allies(),
[pid2](const net::eagle0::shardok::storage::fb::AlliedPlayer*
alliedPlayer) {
@@ -232,7 +229,7 @@ auto UpdateGameStatusAction::InternalExecute(
// Check for attacker occupying castles & towns
bool foundUncontrolledCriticalTile = false;
for (const auto& criticalTile : criticalTileLocations) {
const auto* possibleOccupant = currentState.GetOccupant(criticalTile);
const auto* possibleOccupant = gameState.GetOccupant(criticalTile);
if (!possibleOccupant) {
foundUncontrolledCriticalTile = true;
@@ -23,7 +23,7 @@ private:
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const GameState* gameState;
const GameStateW& gameState;
const CoordsSet criticalTileLocations;
const SettingsGetter settingsGetter;
const bool atEndOfRound;
@@ -34,7 +34,7 @@ private:
public:
UpdateGameStatusAction(
const GameState* gameState,
const GameStateW& gameState,
const CoordsSet& criticalTileLocations,
const SettingsGetter& settingsGetter,
const bool atEndOfRound)
@@ -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 (static_cast<unsigned int>(pid) >= playerCount) continue;
if (pid >= playerCount) continue;
int bump = PlayerIsDefender(currentState, pid) ? defenderKnowledgeGain
: attackerKnowledgeGain;
MutatingBumpOpponentKnowledge(&unitAfter, pid, bump);
@@ -8,9 +8,6 @@
#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"
@@ -44,7 +41,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 (std::ranges::contains(allyPids, occupant->player_id())) {
if (common::Contains(allyPids, occupant->player_id())) {
adjacentFriendliesMod += perAdjacentFriendly;
} else {
adjacentEnemiesMod += perAdjacentEnemy;
@@ -58,7 +55,7 @@ void FleeCommandFactory::AddAvailableFleeCommands(
for (const auto &twoAwayCoords : TilesWithExactDistance(map, unit->location(), 2)) {
const auto *occupant = Occupant(allUnits, twoAwayCoords);
if (occupant && !occupant->hidden()) {
if (std::ranges::contains(allyPids, occupant->player_id())) {
if (common::Contains(allyPids, occupant->player_id())) {
adjacentFriendliesMod += int32_t(perAdjacentFriendly * adjustmentForTwoAway);
} else {
adjacentEnemiesMod += int32_t(perAdjacentEnemy * adjustmentForTwoAway);
@@ -4,9 +4,6 @@
#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"
@@ -29,7 +26,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,
@@ -39,7 +36,7 @@ auto HideCommandFactory::PositionIsHideable(
const auto occupant = Occupant(units, target);
if (occupant) {
if (occupant->player_id() == actor->player_id()) return false;
if (std::ranges::contains(allyPids, occupant->player_id())) return false;
if (common::Contains(allyPids, occupant->player_id())) return false;
if (!occupant->hidden()) return false;
}
@@ -8,6 +8,7 @@
#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"
@@ -45,7 +46,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() ==
@@ -44,6 +44,8 @@ auto ReduceCommandFactory::AddAvailableReduceCommands(
const auto *const occupant = Occupant(units, reduceCoords);
if (occupant && occupant->player_id() == playerId) continue;
// This function takes a Units*, not a GameStateW, so we can't use the cache method
// yet
const auto *const enemyOccupant =
KnownEnemyOccupant(playerId, units, allyPids, reduceCoords);
@@ -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 (size_t i = 0; i < beforeFireUnitsCount; i++) {
const auto *beforeUnit = runningGameState->units()->Get(static_cast<unsigned int>(i));
for (int i = 0; i < beforeFireUnitsCount; i++) {
const auto *beforeUnit = runningGameState->units()->Get(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 (size_t i = 0; i < unitsAtEndOfRoundCount; i++) {
for (int 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(static_cast<unsigned int>(i));
const auto *eorUnit = runningGameState->units()->Get(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,8 +4,6 @@
#include "HideCommand.hpp"
#include <algorithm>
#include <ranges>
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
@@ -31,7 +29,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);
@@ -62,7 +60,7 @@ auto HideCommand::InternalExecute(
if (!oneOverOccupant) continue;
PlayerId occupantPid = oneOverOccupant->player_id();
if (occupantPid == GetPlayerId()) continue;
if (std::ranges::contains(alliedPids, occupantPid)) continue;
if (common::Contains(alliedPids, occupantPid)) continue;
if (!oneOverOccupant->has_attached_hero()) continue;
if (oneOverOccupant->attached_hero().profession_info().profession() ==
@@ -9,7 +9,6 @@
#include "HolyWaveCommand.hpp"
#include <algorithm>
#include <ranges>
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"
@@ -51,7 +50,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);
@@ -132,7 +131,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);
@@ -198,7 +197,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);
@@ -238,7 +237,7 @@ auto HolyWaveCommand::InternalExecute(
undeadIds.push_back(occupant->unit_id());
} else if (
occupant->player_id() == GetPlayerId() ||
std::ranges::contains(allyPids, GetPlayerId())) {
common::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,8 +4,6 @@
#include "MoveCommand.hpp"
#include <algorithm>
#include <ranges>
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"
@@ -65,7 +63,9 @@ auto shardok::MoveCommand::InternalExecute(
bool isEligibleCharger = false;
bool wasHidden = mover.hidden();
const auto* occ = currentState.GetOccupant(destination);
// Check for occupant using the original state (we don't want to see our own unit that just
// moved)
const auto* occ = Occupant(allUnits, destination);
if (occ) {
// Tile is occupied -- ambush!
// Reconstruct the current state by applying results generated so far
@@ -96,10 +96,10 @@ auto shardok::MoveCommand::InternalExecute(
}
for (const auto& adj : HexMapUtils::GetAdjacentCoords(map, destination)) {
const auto* occupant = currentState.GetOccupant(adj);
const auto* occupant = Occupant(allUnits, adj);
if (!occupant) continue;
if (occupant->player_id() == mover.player_id()) continue;
if (std::ranges::contains(allyPids, occupant->player_id())) continue;
if (common::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) {
@@ -112,10 +112,10 @@ auto shardok::MoveCommand::InternalExecute(
mover.attached_hero().profession_info().profession() ==
net::eagle0::shardok::storage::fb::Profession_RANGER) {
for (const auto& adj : HexMapUtils::GetAdjacentCoords(map, destination)) {
auto occupant = currentState.GetOccupant(adj);
auto occupant = Occupant(allUnits, adj);
if (!occupant) continue;
if (occupant->player_id() == mover.player_id()) continue;
if (std::ranges::contains(allyPids, occupant->player_id())) continue;
if (common::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());
}

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