mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 05:15:44 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47481c2de6 | ||
|
|
4810e8e9a5 | ||
|
|
d46423bea8 | ||
|
|
3a2f9c0c8c | ||
|
|
c0accd9d6b | ||
|
|
c3c80e3c41 |
@@ -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
@@ -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")
|
||||
|
||||
|
||||
Generated
+35
-61
@@ -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": {},
|
||||
|
||||
@@ -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.
|
||||
@@ -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"],
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 ¤tView, 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 ¤tGameState = 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
|
||||
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+38
-84
@@ -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
|
||||
|
||||
+28
-38
@@ -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();
|
||||
|
||||
+10
-26
@@ -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
|
||||
+4
-16
@@ -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
|
||||
|
||||
+10
-15
@@ -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 ¤tState,
|
||||
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 ¤tState,
|
||||
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 ¤tState,
|
||||
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 ¤tState,
|
||||
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 ¤tState,
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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 ¤tState,
|
||||
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 ¤tState,
|
||||
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 ¤tState,
|
||||
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 ¤tState,
|
||||
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 ¤tState,
|
||||
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 ¤tState,
|
||||
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 ¤tState,
|
||||
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
Reference in New Issue
Block a user