mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 00:35:41 +00:00
Compare commits
434
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e79e8fc61c | ||
|
|
2571f65ac4 | ||
|
|
f52fd34879 | ||
|
|
3610c34b4a | ||
|
|
30390816df | ||
|
|
9c77f2f843 | ||
|
|
c7fa4c5180 | ||
|
|
66d544fb0a | ||
|
|
7f62f07d0b | ||
|
|
2829ec1aae | ||
|
|
97ffb0633d | ||
|
|
f378429786 | ||
|
|
f755d89b2a | ||
|
|
a8e200ab03 | ||
|
|
68bec95772 | ||
|
|
fde9529a31 | ||
|
|
0d044a7468 | ||
|
|
ee4b9c1d8f | ||
|
|
a7bd3deb76 | ||
|
|
9e69b213b0 | ||
|
|
f75ac01959 | ||
|
|
3eafc2b31c | ||
|
|
b39f48e8d6 | ||
|
|
4346e2dc6f | ||
|
|
85122128bf | ||
|
|
5a58ea3ffb | ||
|
|
e1237da088 | ||
|
|
b4eebf5ead | ||
|
|
922a825648 | ||
|
|
3602d439a5 | ||
|
|
1718d8e885 | ||
|
|
f58f3ac31a | ||
|
|
6e65b556b4 | ||
|
|
d86597b92e | ||
|
|
ce667c5784 | ||
|
|
13a939f88e | ||
|
|
52ae7cad7f | ||
|
|
eacad46c9b | ||
|
|
1bd8fc7154 | ||
|
|
50ccb1b7d8 | ||
|
|
c693553365 | ||
|
|
6c3e4bc202 | ||
|
|
92d9a2dbd8 | ||
|
|
5df24d23c0 | ||
|
|
1215b5044c | ||
|
|
b1263ec4df | ||
|
|
19e85bde8b | ||
|
|
cbe71f4fd4 | ||
|
|
52afd836ac | ||
|
|
095a29a4a0 | ||
|
|
49ff9f8b09 | ||
|
|
457c1e933a | ||
|
|
f5e56f0da9 | ||
|
|
0d8df5e97c | ||
|
|
92af09660c | ||
|
|
435e7e08f3 | ||
|
|
2516c42c08 | ||
|
|
19dc4fd1b1 | ||
|
|
0305cc7506 | ||
|
|
5854929085 | ||
|
|
232e560724 | ||
|
|
994de5cdc2 | ||
|
|
dcc303ad37 | ||
|
|
84ecc4d13d | ||
|
|
eb338b1d0b | ||
|
|
1b1261f4f6 | ||
|
|
3fff156344 | ||
|
|
3d39238045 | ||
|
|
705b176992 | ||
|
|
1c2e150899 | ||
|
|
8af820c58e | ||
|
|
940a4836a7 | ||
|
|
1ef2fa0f84 | ||
|
|
7efdf90100 | ||
|
|
be930740cf | ||
|
|
241b49ac23 | ||
|
|
77499dbdad | ||
|
|
a4c688f126 | ||
|
|
5ee753e989 | ||
|
|
77c6c16096 | ||
|
|
2105ccc791 | ||
|
|
e03af03acb | ||
|
|
041a43be97 | ||
|
|
53180baddd | ||
|
|
b88613a6f1 | ||
|
|
dbc5455de0 | ||
|
|
17357eba33 | ||
|
|
ff9ab1907b | ||
|
|
0b9ebb04c8 | ||
|
|
10a3b3a046 | ||
|
|
7cf852ab52 | ||
|
|
2341df8998 | ||
|
|
190e71d3c4 | ||
|
|
c0a9b90715 | ||
|
|
6d13774fd0 | ||
|
|
41252ccaa6 | ||
|
|
ff05a35991 | ||
|
|
8b7e9eaeb6 | ||
|
|
7f80eb0ef2 | ||
|
|
ef151c2ef4 | ||
|
|
e7cad8dd36 | ||
|
|
c4edb2f6a0 | ||
|
|
11709e6d48 | ||
|
|
0b0fcc411d | ||
|
|
1aadcc863f | ||
|
|
9ac167e66c | ||
|
|
1530873df5 | ||
|
|
75a5b4fa21 | ||
|
|
b6c45da7bc | ||
|
|
5e30673281 | ||
|
|
d38c611a18 | ||
|
|
570bc557f5 | ||
|
|
c1f4152cae | ||
|
|
0472362abc | ||
|
|
b732b60520 | ||
|
|
fc1f988654 | ||
|
|
7ef93c4a11 | ||
|
|
247aa3e685 | ||
|
|
aa0292bad6 | ||
|
|
08cd608ac2 | ||
|
|
f85a32dc08 | ||
|
|
b5deb69ef7 | ||
|
|
872883f710 | ||
|
|
3d402e7bdd | ||
|
|
862480c470 | ||
|
|
98b5d7f034 | ||
|
|
e5c5a929ec | ||
|
|
d39ee392b9 | ||
|
|
b574c6c950 | ||
|
|
074c984f3b | ||
|
|
2817ca529d | ||
|
|
4f932d0a56 | ||
|
|
2dd931dc8f | ||
|
|
c7ab2ecb44 | ||
|
|
8466ab6ed9 | ||
|
|
7b74905973 | ||
|
|
ecfb5a0a94 | ||
|
|
967cb74aa5 | ||
|
|
34daf8f38d | ||
|
|
e12359821c | ||
|
|
eea41e6c05 | ||
|
|
36d1e75719 | ||
|
|
35e93c8ce1 | ||
|
|
d603b01733 | ||
|
|
6c66b5df82 | ||
|
|
3b59e21407 | ||
|
|
d0e74ac4cb | ||
|
|
c04aa3d753 | ||
|
|
f3fade7ed2 | ||
|
|
79d8e837e8 | ||
|
|
c39795fa9f | ||
|
|
4a7c55a382 | ||
|
|
7e4396552e | ||
|
|
40219f2a67 | ||
|
|
ce35c176ee | ||
|
|
a852f03214 | ||
|
|
7c8b43d91b | ||
|
|
ecf763b24b | ||
|
|
8ad4fc52e3 | ||
|
|
7e48f6fd2b | ||
|
|
5c81832cde | ||
|
|
569458d691 | ||
|
|
c825bf4606 | ||
|
|
567846156c | ||
|
|
14f1ab992c | ||
|
|
42cb9291fc | ||
|
|
aa2bf97078 | ||
|
|
6976db961b | ||
|
|
1823543706 | ||
|
|
c452b8be69 | ||
|
|
53612938b6 | ||
|
|
ccb64480a8 | ||
|
|
4f0a172036 | ||
|
|
ca45eba76b | ||
|
|
8f58543a4c | ||
|
|
453242e06a | ||
|
|
249a564ac7 | ||
|
|
701c822e1a | ||
|
|
2ca4b6e8d4 | ||
|
|
fe060e4489 | ||
|
|
6dc29ec746 | ||
|
|
8b9ac89205 | ||
|
|
74bbd490d7 | ||
|
|
7a5f89ea87 | ||
|
|
bdcddb88e4 | ||
|
|
cccc7971c1 | ||
|
|
82d2ca915d | ||
|
|
319dc0b19e | ||
|
|
2573f0f2af | ||
|
|
641244103f | ||
|
|
1b7909d914 | ||
|
|
9d7dfd8f5f | ||
|
|
42edef2313 | ||
|
|
7ad6677b90 | ||
|
|
ce19c34b8b | ||
|
|
d675e3781f | ||
|
|
2164a98abe | ||
|
|
b61ce58f4c | ||
|
|
7a11c8fa9a | ||
|
|
62438ec4f6 | ||
|
|
afcc6a9a3e | ||
|
|
b6a094064a | ||
|
|
70b8eb6513 | ||
|
|
e4c7a0c98b | ||
|
|
67573e7a8a | ||
|
|
c60dce4504 | ||
|
|
8250d3d42f | ||
|
|
5407c8aa65 | ||
|
|
d282634264 | ||
|
|
fff0e6dd30 | ||
|
|
a1ccdc672c | ||
|
|
6802da55ed | ||
|
|
57dae50cf7 | ||
|
|
4bce809fab | ||
|
|
2ff2cdab79 | ||
|
|
76839dcd59 | ||
|
|
effd5451b6 | ||
|
|
00ac74f944 | ||
|
|
728f1969ce | ||
|
|
fab757f9dd | ||
|
|
fa006ba947 | ||
|
|
2b61fb0fcf | ||
|
|
62bcd3b669 | ||
|
|
0fc394174c | ||
|
|
14015aebc7 | ||
|
|
5acdcea0b0 | ||
|
|
709d3b9219 | ||
|
|
0f028e73d3 | ||
|
|
f0fa1f1a1d | ||
|
|
0a52e41c3d | ||
|
|
9ef750956c | ||
|
|
b70d541224 | ||
|
|
a8299738d2 | ||
|
|
e789309827 | ||
|
|
f8066454a8 | ||
|
|
a80c8e6d1b | ||
|
|
3dc8541aee | ||
|
|
b7cd003b4c | ||
|
|
bae90633a5 | ||
|
|
56eff84f30 | ||
|
|
375ae7cecb | ||
|
|
ac8a9358ff | ||
|
|
8bc900eef9 | ||
|
|
845a85ed1f | ||
|
|
a1a7a8abf8 | ||
|
|
9cc4dca5b2 | ||
|
|
ea244506d3 | ||
|
|
9827414e92 | ||
|
|
549cf37127 | ||
|
|
237d5ce7c3 | ||
|
|
05fc72c611 | ||
|
|
5d889b71aa | ||
|
|
8641d15fc7 | ||
|
|
e4fc742990 | ||
|
|
a27dfe895f | ||
|
|
098d4b4dcc | ||
|
|
765ca02255 | ||
|
|
186dec0ad9 | ||
|
|
14a6d143bc | ||
|
|
ade5a4fa10 | ||
|
|
85d3ae8fce | ||
|
|
fb200278ae | ||
|
|
34da29bef0 | ||
|
|
f164fbb150 | ||
|
|
fe1537bb12 | ||
|
|
69cc336cef | ||
|
|
d05030f99a | ||
|
|
9699d095c5 | ||
|
|
6ca2d7a37c | ||
|
|
b0c4944a6c | ||
|
|
d25925d525 | ||
|
|
07a6834216 | ||
|
|
69f1f51460 | ||
|
|
8b30a2c5f2 | ||
|
|
65a534ebae | ||
|
|
ac0a154fc4 | ||
|
|
1b510007fd | ||
|
|
692c363264 | ||
|
|
cf5d88c69d | ||
|
|
109f49e522 | ||
|
|
53e3f7a1b0 | ||
|
|
c7d3e162b9 | ||
|
|
7b2bb34a36 | ||
|
|
c7b1082954 | ||
|
|
aa18d0dd1a | ||
|
|
711d0382f3 | ||
|
|
7b02c39172 | ||
|
|
b178f11bea | ||
|
|
42da8a7d15 | ||
|
|
bd9b62d7de | ||
|
|
98fcd94614 | ||
|
|
06e7112db1 | ||
|
|
9ba023b0fd | ||
|
|
ff63c0fff2 | ||
|
|
421dbcae80 | ||
|
|
4c321967f4 | ||
|
|
b3c0138074 | ||
|
|
4b7c862ac8 | ||
|
|
cf4b99710c | ||
|
|
d25e0121a6 | ||
|
|
213a339d34 | ||
|
|
d2205dc361 | ||
|
|
21e0586add | ||
|
|
cbbe200c98 | ||
|
|
fbeddc7b19 | ||
|
|
1ed6c85b5d | ||
|
|
c7f10dbcbc | ||
|
|
81f747c75b | ||
|
|
a76dc0515c | ||
|
|
bb77ecbd73 | ||
|
|
5d35e7d78b | ||
|
|
e20c4e7027 | ||
|
|
3977df70f4 | ||
|
|
7f72a8bb39 | ||
|
|
de27e9fcb8 | ||
|
|
76bd94ddb5 | ||
|
|
651e209f07 | ||
|
|
448ec1e0ee | ||
|
|
6e2afeee3c | ||
|
|
edce49467c | ||
|
|
2934027437 | ||
|
|
b2250cda79 | ||
|
|
96d69b349f | ||
|
|
4a6ae0380a | ||
|
|
e339ddd227 | ||
|
|
afaf6c9e19 | ||
|
|
e21eaf8606 | ||
|
|
a43a650d17 | ||
|
|
17cb742964 | ||
|
|
9d8231b253 | ||
|
|
5bf2ea95e5 | ||
|
|
2f48330d74 | ||
|
|
b0b516a990 | ||
|
|
1057271163 | ||
|
|
296d5df16f | ||
|
|
b2abe00b14 | ||
|
|
793d665d80 | ||
|
|
bbbe596e72 | ||
|
|
c3c148f5db | ||
|
|
4b8f1d2d20 | ||
|
|
8a3222ed54 | ||
|
|
9f82af2e12 | ||
|
|
2536e5146c | ||
|
|
98b0502e20 | ||
|
|
25c1198c0a | ||
|
|
a6e31596d6 | ||
|
|
c9b3cd6fc0 | ||
|
|
2c18d1d39b | ||
|
|
28895e3e04 | ||
|
|
f8ef044226 | ||
|
|
b8da5750c7 | ||
|
|
fbdc0f79a8 | ||
|
|
ea7f666e36 | ||
|
|
48ff8a7e1b | ||
|
|
033a378e14 | ||
|
|
a3d19f246e | ||
|
|
ec67215bc2 | ||
|
|
f207bef4a8 | ||
|
|
41bb944c26 | ||
|
|
38eb33367d | ||
|
|
5ae59006fb | ||
|
|
e66e7d4a03 | ||
|
|
34bbfaf256 | ||
|
|
abd323a232 | ||
|
|
97ba1d0be1 | ||
|
|
deb211a420 | ||
|
|
9481eed7b8 | ||
|
|
50c457d877 | ||
|
|
a544a0d15e | ||
|
|
8c26ac5fca | ||
|
|
d56e0de9fd | ||
|
|
5132152334 | ||
|
|
ef6ba7e585 | ||
|
|
695a9d19d4 | ||
|
|
45682dff9b | ||
|
|
47c5653bc0 | ||
|
|
033ebc802a | ||
|
|
14e0fb66fb | ||
|
|
18d9756fc5 | ||
|
|
f976548fa1 | ||
|
|
a3fed6836d | ||
|
|
78d89f83da | ||
|
|
66b93b0c83 | ||
|
|
fe06ead2e2 | ||
|
|
71e5b1b866 | ||
|
|
35ac2d6297 | ||
|
|
ff33d13822 | ||
|
|
ffb02d1d6c | ||
|
|
86f7e85ce9 | ||
|
|
cd6318ba84 | ||
|
|
7385b8ced2 | ||
|
|
a36ccd4157 | ||
|
|
29a3459649 | ||
|
|
8dd7df989d | ||
|
|
d40b9a9ba6 | ||
|
|
dd5d34f7aa | ||
|
|
5685c8430f | ||
|
|
723a195d89 | ||
|
|
68a20cba27 | ||
|
|
27802e54e6 | ||
|
|
3e91ca3bc3 | ||
|
|
5a4e98bd71 | ||
|
|
225fe66afa | ||
|
|
619f0ca3e9 | ||
|
|
fa3f4cd06b | ||
|
|
7694d2e676 | ||
|
|
ea6792f21b | ||
|
|
5e4dace755 | ||
|
|
276dbf5d42 | ||
|
|
0ca2685bf3 | ||
|
|
4bab97ec62 | ||
|
|
47bd5f6291 | ||
|
|
7ee1441b8e | ||
|
|
034270a119 | ||
|
|
9e4bff03d9 | ||
|
|
29b9954954 | ||
|
|
3aa62b9eeb | ||
|
|
d8bd976204 | ||
|
|
3ccb8f5dc8 | ||
|
|
cb05990260 | ||
|
|
8a2c21c88c | ||
|
|
204cd81462 | ||
|
|
71505b5419 | ||
|
|
50e958e2e3 | ||
|
|
3ffab58cbf | ||
|
|
febb9e2fe0 | ||
|
|
b1af1b7254 | ||
|
|
32a4da1826 | ||
|
|
cc44eef174 | ||
|
|
c608a5890a | ||
|
|
52253908eb | ||
|
|
a50956cdb0 | ||
|
|
ee55e4b216 | ||
|
|
9d041926d7 |
@@ -14,6 +14,7 @@ common --worker_sandboxing
|
||||
common --local_test_jobs=64
|
||||
common --jobs=64
|
||||
|
||||
# Keep these C++ standard flags in sync with tools/copts.bzl and scripts/shardok_cpp_config.sh.
|
||||
common --cxxopt="--std=c++23"
|
||||
common --cxxopt="-Wno-deprecated-non-prototype"
|
||||
common --per_file_copt=src/test/cpp/.*@-Wno-character-conversion
|
||||
@@ -37,6 +38,8 @@ build:ubsan --strip=never
|
||||
build:ubsan --copt=-fno-omit-frame-pointer
|
||||
build:ubsan --copt=-Wno-macro-redefined
|
||||
build:ubsan --copt=-fsanitize=undefined
|
||||
# Protobuf's arena implementation trips UBSan alignment checks in external code on macOS.
|
||||
build:ubsan --copt=-fno-sanitize=alignment
|
||||
build:ubsan --linkopt=-fsanitize=undefined
|
||||
test:ubsan --test_env=UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
|
||||
|
||||
|
||||
+10
-18
@@ -2,25 +2,17 @@ Checks: >
|
||||
-*,
|
||||
bugprone-*,
|
||||
performance-*,
|
||||
readability-*,
|
||||
modernize-*,
|
||||
cppcoreguidelines-*,
|
||||
modernize-use-nullptr,
|
||||
modernize-use-override,
|
||||
modernize-use-auto,
|
||||
modernize-use-nodiscard,
|
||||
readability-braces-around-statements,
|
||||
readability-qualified-auto,
|
||||
readability-redundant-member-init,
|
||||
cppcoreguidelines-pro-type-cstyle-cast,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-pro-type-const-cast,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-union-access,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-modernize-use-trailing-return-type,
|
||||
-readability-convert-member-functions-to-static,
|
||||
-readability-function-cognitive-complexity,
|
||||
-readability-identifier-length,
|
||||
-readability-magic-numbers
|
||||
-bugprone-branch-clone,
|
||||
-bugprone-signed-char-misuse
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: 'src/(main|test)/cpp/net/eagle0/(shardok|common)/.*'
|
||||
FormatStyle: file
|
||||
|
||||
@@ -12,6 +12,9 @@ on:
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/summarize_bazel_bep.py'
|
||||
- '.clang-tidy'
|
||||
- 'scripts/run-clang-tidy.sh'
|
||||
- 'scripts/check_shardok_clang_tidy.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
@@ -26,6 +29,9 @@ on:
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/summarize_bazel_bep.py'
|
||||
- '.clang-tidy'
|
||||
- 'scripts/run-clang-tidy.sh'
|
||||
- 'scripts/check_shardok_clang_tidy.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
@@ -78,6 +84,8 @@ jobs:
|
||||
node-version: 'lts/*'
|
||||
- name: Check JavaScript syntax
|
||||
run: node --check src/main/go/net/eagle0/admin_server/static/map_editor.js
|
||||
- name: Check Shardok clang-tidy gate
|
||||
run: ./scripts/check_shardok_clang_tidy.sh
|
||||
|
||||
test:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
@@ -255,7 +255,7 @@ jobs:
|
||||
echo "=== All images pushed successfully ==="
|
||||
|
||||
deploy:
|
||||
runs-on: [self-hosted, bazel]
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-all]
|
||||
if: needs.build-all.outputs.skip_build != 'true' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true'))
|
||||
environment: production
|
||||
@@ -334,25 +334,11 @@ jobs:
|
||||
ssh-keyscan -H "$DO_DROPLET_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Download warmup binary
|
||||
id: download-warmup
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: warmup-binary
|
||||
path: scripts/bin/
|
||||
|
||||
- name: Build warmup binary fallback
|
||||
if: steps.download-warmup.outcome != 'success'
|
||||
run: |
|
||||
echo "::warning::warmup-binary artifact was unavailable; rebuilding warmup binary before deploy"
|
||||
./ci/github_actions/ensure_bazel_installed.sh
|
||||
bazel build \
|
||||
--platforms=//:linux_x86_64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux//:cc-toolchain-x86_64-linux \
|
||||
//ci:warmup_tar
|
||||
mkdir -p scripts/bin
|
||||
tar -xf bazel-bin/ci/warmup_tar.tar -C scripts/bin --strip-components=1
|
||||
|
||||
- name: Copy config files to droplet
|
||||
run: |
|
||||
# Create directory structure on remote
|
||||
|
||||
@@ -196,7 +196,9 @@ jobs:
|
||||
echo "Also tagged as: registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
|
||||
|
||||
deploy-hetzner:
|
||||
runs-on: [self-hosted, bazel]
|
||||
# Keep this on a self-hosted Mac runner for IPv6 reachability to Hetzner,
|
||||
# but do not require a Bazel runner slot for artifact transport/deploy work.
|
||||
runs-on: [self-hosted, macOS, ARM64]
|
||||
needs: [build-shardok-arm64]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Shardok Sanitizer Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "src/main/cpp/net/eagle0/shardok/**"
|
||||
- "src/test/cpp/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/common/**"
|
||||
- "WORKSPACE"
|
||||
- "MODULE.bazel"
|
||||
- "MODULE.bazel.lock"
|
||||
- "BUILD.bazel"
|
||||
- ".bazelrc"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- ".github/workflows/shardok_sanitizer_test.yml"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- "ci/github_actions/summarize_bazel_bep.py"
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/main/cpp/net/eagle0/shardok/**"
|
||||
- "src/test/cpp/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/common/**"
|
||||
- "WORKSPACE"
|
||||
- "MODULE.bazel"
|
||||
- "MODULE.bazel.lock"
|
||||
- "BUILD.bazel"
|
||||
- ".bazelrc"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- ".github/workflows/shardok_sanitizer_test.yml"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- "ci/github_actions/summarize_bazel_bep.py"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
shardok-sanitizer-test:
|
||||
name: Shardok ${{ matrix.name }}
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: ASan
|
||||
config: asan
|
||||
- name: UBSan
|
||||
config: ubsan
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Run Shardok sanitizer tests
|
||||
run: bazel test --config=${{ matrix.config }} --build_event_json_file=${{ matrix.config }}.json //src/test/cpp/net/eagle0/shardok/...
|
||||
- name: Summarize Bazel build metrics
|
||||
if: always()
|
||||
run: python3 ci/github_actions/summarize_bazel_bep.py ${{ matrix.config }}.json
|
||||
- name: Archive sanitizer test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shardok-${{ matrix.config }}.json
|
||||
path: ${{ matrix.config }}.json
|
||||
retention-days: 3
|
||||
@@ -11,6 +11,11 @@
|
||||
**NEVER kill Unity without asking the user first.** The user might be actively using the Unity Editor. Do not kill,
|
||||
force-quit, terminate, or otherwise stop Unity processes unless the user explicitly approves that specific action.
|
||||
|
||||
**NEVER change Unity's default/open scene as part of running checks.** Unity batchmode or editor validation may write
|
||||
scene-selection churn such as `ProjectSettings/EditorBuildSettings.asset`, `ProjectSettings/SceneTemplateSettings.json`,
|
||||
or scene files simply because a different scene was open or loaded. Treat those as unintended local environment changes:
|
||||
do not stage them, and restore them before committing unless the user explicitly asked to change scenes/build settings.
|
||||
|
||||
## CRITICAL GIT RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER use `git -C`.** Just run `git` directly from the cwd — it finds the repo via `.git` discovery. Do not `cd` to the repo root either (see the no-`cd` bash rule above).
|
||||
@@ -29,6 +34,16 @@ If you catch yourself about to run `git push origin main` or `git push origin <b
|
||||
|
||||
If you catch yourself about to run `gh pr merge`, STOP. Only the user merges PRs.
|
||||
|
||||
## Commit History During Review
|
||||
|
||||
Prefer new follow-up commits over amending existing commits once a PR is open and the user is actively reviewing or
|
||||
testing it. This preserves working interim states so the user can inspect, compare, or return to a known-good point
|
||||
while improvements continue.
|
||||
|
||||
Use `git commit --amend` and force-push only when the user explicitly asks for history cleanup, when fixing metadata
|
||||
before review has started, or when repairing a local commit that has not been pushed. If you believe squashing is the
|
||||
right choice despite an open review, explain why and ask first.
|
||||
|
||||
## Worktree Usage
|
||||
|
||||
Prefer using the current worktree for small requested fixes. Create a separate worktree only when the user asks for one,
|
||||
@@ -58,6 +73,15 @@ For PRs expected to trigger substantial Bazel work in CI, run the relevant Bazel
|
||||
workstation is fast, and local Bazel runs can help hydrate the remote cache for the CI builders while still giving
|
||||
earlier signal on failures.
|
||||
|
||||
## Cleanup PR Batching
|
||||
|
||||
When making mechanical cleanup changes, group 3-5 files per PR when the files are receiving the same kind of change
|
||||
and can be reviewed as one pattern. Do not open one PR per file for nearly identical few-line cleanups, such as replacing
|
||||
the same unsafe accessor pattern with the same contextual failure pattern across multiple Scala files.
|
||||
|
||||
Keep separate PRs for changes that involve different semantics, materially different risk, unrelated subsystems, or
|
||||
files whose tests/validation strategy makes them better reviewed independently.
|
||||
|
||||
## GitHub CLI PR Bodies
|
||||
|
||||
When creating or editing PR descriptions with multiline bodies, write the body to a temporary markdown file and pass it
|
||||
|
||||
@@ -40,19 +40,6 @@ if [ -f "$INFO_PLIST" ]; then
|
||||
/usr/libexec/PlistBuddy -c "Set :ITSAppUsesNonExemptEncryption false" "$INFO_PLIST" 2>/dev/null \
|
||||
|| /usr/libexec/PlistBuddy -c "Add :ITSAppUsesNonExemptEncryption bool false" "$INFO_PLIST"
|
||||
echo "Set ITSAppUsesNonExemptEncryption=false in Info.plist"
|
||||
|
||||
# Remove UIApplicationSceneManifest if present.
|
||||
# Unity 6 generates this key, which opts the app into the iOS scene-based
|
||||
# lifecycle. This breaks Application.deepLinkActivated because iOS routes
|
||||
# deep link URLs through UISceneDelegate instead of UIApplicationDelegate,
|
||||
# and Unity doesn't implement the scene delegate path.
|
||||
# See: https://issuetracker.unity3d.com/issues/in-135632
|
||||
if /usr/libexec/PlistBuddy -c "Print :UIApplicationSceneManifest" "$INFO_PLIST" 2>/dev/null; then
|
||||
echo "Found UIApplicationSceneManifest in Info.plist — removing to fix deep link handling"
|
||||
/usr/libexec/PlistBuddy -c "Delete :UIApplicationSceneManifest" "$INFO_PLIST"
|
||||
else
|
||||
echo "No UIApplicationSceneManifest in Info.plist (deep links should work)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Unity always generates "Unity-iPhone" as the main app scheme
|
||||
|
||||
@@ -71,7 +71,7 @@ LLM settings can be changed at runtime via the admin console:
|
||||
1. Navigate to Admin Console → Settings
|
||||
2. Change `LlmProvider` to select vendor (gemini, openai, claude)
|
||||
3. Change the corresponding model name setting:
|
||||
- `GeminiModelName` (default: gemini-3.1-flash-lite-preview)
|
||||
- `GeminiModelName` (default: gemini-3.1-flash-lite)
|
||||
- `OpenAiModelName` (default: gpt-4.1-mini)
|
||||
- `ClaudeModelName` (default: claude-3-5-haiku-20241022)
|
||||
|
||||
|
||||
Binary file not shown.
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# Runs the curated Shardok clang-tidy gate used by CI.
|
||||
#
|
||||
# This intentionally checks representative files instead of the whole C++ tree.
|
||||
# Keep this list low-noise; expand it as cleanup PRs make more areas tidy-clean.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
source "$repo_root/scripts/shardok_cpp_config.sh"
|
||||
|
||||
files=(
|
||||
"src/main/cpp/net/eagle0/shardok/library/ActionCost.hpp"
|
||||
"src/main/cpp/net/eagle0/shardok/library/CombatDamage.hpp"
|
||||
)
|
||||
|
||||
compiler_args=(
|
||||
"-I$repo_root"
|
||||
"$SHARDOK_CXX_STANDARD_ARG"
|
||||
)
|
||||
|
||||
if command -v xcrun >/dev/null 2>&1; then
|
||||
sdk_path="$(xcrun --show-sdk-path)"
|
||||
compiler_args+=("-isysroot" "$sdk_path")
|
||||
fi
|
||||
|
||||
"$repo_root/scripts/run-clang-tidy.sh" --verify-config
|
||||
|
||||
"$repo_root/scripts/run-clang-tidy.sh" --quiet --warnings-as-errors="*" "${files[@]}" -- "${compiler_args[@]}"
|
||||
@@ -10,13 +10,14 @@ set -e
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
source "$repo_root/scripts/shardok_cpp_config.sh"
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "Usage: scripts/run-clang-tidy.sh [clang-tidy args] <files> [-- <compiler args>]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " scripts/run-clang-tidy.sh --verify-config"
|
||||
echo " scripts/run-clang-tidy.sh src/main/cpp/net/eagle0/shardok/library/CombatDamage.hpp -- -I$repo_root -std=c++23"
|
||||
echo " scripts/run-clang-tidy.sh src/main/cpp/net/eagle0/shardok/library/CombatDamage.hpp -- -I$repo_root $SHARDOK_CXX_STANDARD_ARG"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Shared C++ settings for shell scripts that invoke Shardok tooling outside Bazel.
|
||||
|
||||
SHARDOK_CXX_STANDARD="c++23"
|
||||
SHARDOK_CXX_STANDARD_ARG="-std=${SHARDOK_CXX_STANDARD}"
|
||||
@@ -28,7 +28,7 @@ static inline auto HashBuffer(const uint8_t* data, size_t size) -> uint64_t {
|
||||
|
||||
// Process 8 bytes at a time
|
||||
while (data + 8 <= end) {
|
||||
uint64_t word;
|
||||
uint64_t word = 0;
|
||||
// Use memcpy to avoid alignment issues and let compiler optimize
|
||||
__builtin_memcpy(&word, data, sizeof(word));
|
||||
hash ^= word;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iterator>
|
||||
|
||||
#define ITERABLE_BITSET_INDEX_CHECKS false
|
||||
|
||||
@@ -104,9 +105,11 @@ public:
|
||||
|
||||
class Iter {
|
||||
public:
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using value_type = size_t;
|
||||
using pointer = size_t*;
|
||||
using reference = size_t&;
|
||||
using pointer = void;
|
||||
using reference = size_t;
|
||||
|
||||
private:
|
||||
size_t storeIndex;
|
||||
@@ -142,13 +145,20 @@ public:
|
||||
auto operator!=(const Iter& rhs) const -> bool {
|
||||
return storeIndex != rhs.storeIndex || bitIndex != rhs.bitIndex;
|
||||
}
|
||||
void operator++() {
|
||||
auto operator++() -> Iter& {
|
||||
bitIndex++;
|
||||
if (bitIndex >= STORE_BITS) {
|
||||
storeIndex++;
|
||||
bitIndex = 0;
|
||||
}
|
||||
ToNext();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto operator++(int) -> Iter {
|
||||
Iter previous = *this;
|
||||
++(*this);
|
||||
return previous;
|
||||
}
|
||||
|
||||
auto operator*() const -> size_t { return (storeIndex << BIT_SHIFT) + bitIndex; }
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
#include "RandomGenerator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <stdexcept>
|
||||
|
||||
constexpr double kOpenEndedHighThreshold = 95.0;
|
||||
constexpr double kOpenEndedLowThreshold = 5.0;
|
||||
@@ -67,7 +69,8 @@ auto RandomGenerator::ReverseOpenEndedPercentile() -> double {
|
||||
}
|
||||
|
||||
auto RandomGenerator::IntBetween(const int min, const int max) -> int {
|
||||
return int(DoubleZeroToOne() * (max - min)) + min;
|
||||
if (min >= max) { throw std::invalid_argument("random integer range must be non-empty"); }
|
||||
return std::min(int(DoubleZeroToOne() * (max - min)) + min, max - 1);
|
||||
}
|
||||
|
||||
auto RandomGenerator::IntBelow(const int max) -> int { return IntBetween(0, max); }
|
||||
@@ -94,6 +97,7 @@ StdLibraryGenerator::StdLibraryGenerator(const std::mt19937_64::result_type seed
|
||||
}
|
||||
|
||||
auto StdLibraryGenerator::IntBetween(const int min, const int max) -> int {
|
||||
if (min >= max) { throw std::invalid_argument("random integer range must be non-empty"); }
|
||||
std::uniform_int_distribution<int> unifInt(min, max - 1);
|
||||
return unifInt(engine);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
class RandomGenerator;
|
||||
@@ -41,6 +42,7 @@ public:
|
||||
|
||||
template<class T>
|
||||
auto RandomElement(const std::vector<T>& vec) -> T {
|
||||
if (vec.empty()) { throw std::invalid_argument("cannot choose from an empty vector"); }
|
||||
return vec.at(static_cast<std::size_t>(IntBelow(static_cast<int>(vec.size()))));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#ifndef EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
|
||||
#define EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -29,14 +31,24 @@ private:
|
||||
size_t position = 0;
|
||||
|
||||
auto DoubleZeroToOne() -> double override {
|
||||
if (position >= sequence.size()) { position = 0; }
|
||||
const double nextVal = sequence.at(position);
|
||||
position++;
|
||||
if (position >= sequence.size()) { position = 0; }
|
||||
return nextVal;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)) {}
|
||||
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)) {
|
||||
if (sequence.empty()) {
|
||||
throw std::invalid_argument("sequence random generator requires at least one value");
|
||||
}
|
||||
for (const double value : sequence) {
|
||||
if (!std::isfinite(value) || value < 0.0 || value > 1.0) {
|
||||
throw std::invalid_argument(
|
||||
"sequence random generator values must be between 0 and 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
static auto WithSequence(const std::vector<double>& s)
|
||||
-> std::shared_ptr<SequenceRandomGenerator> {
|
||||
return std::make_shared<SequenceRandomGenerator>(s);
|
||||
|
||||
@@ -9,18 +9,24 @@
|
||||
#ifndef EAGLE0_COMMON_BYTE_VECTOR_HPP
|
||||
#define EAGLE0_COMMON_BYTE_VECTOR_HPP
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <initializer_list>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using std::string;
|
||||
using std::vector;
|
||||
|
||||
class byte_vector : public vector<uint8_t> {
|
||||
class byte_vector {
|
||||
private:
|
||||
std::vector<uint8_t> bytes_;
|
||||
|
||||
public:
|
||||
class HexException : public std::exception {
|
||||
const std::string description;
|
||||
@@ -35,60 +41,78 @@ public:
|
||||
};
|
||||
|
||||
private:
|
||||
static auto HexNibbleToValue(const char nibble) -> uint8_t {
|
||||
if (nibble >= '0' && nibble <= '9') {
|
||||
return uint8_t(nibble - '0');
|
||||
} else if (nibble >= 'A' && nibble <= 'F') {
|
||||
return uint8_t(nibble - 'A' + 10);
|
||||
} else if (nibble >= 'a' && nibble <= 'f') {
|
||||
return uint8_t(nibble - 'a' + 10);
|
||||
static auto HexCharsToByte(const std::string_view digits) -> uint8_t {
|
||||
unsigned int parsedByte = 0;
|
||||
const auto [ptr, ec] =
|
||||
std::from_chars(digits.data(), digits.data() + digits.size(), parsedByte, 16);
|
||||
if (ec != std::errc() || ptr != digits.data() + digits.size()) {
|
||||
throw HexException(ptr == digits.data() + digits.size() ? digits.back() : *ptr);
|
||||
}
|
||||
throw HexException(nibble);
|
||||
}
|
||||
|
||||
static auto HexCharsToByte(std::istringstream& str) -> uint8_t {
|
||||
uint8_t b = 0;
|
||||
char c = 0;
|
||||
str >> c;
|
||||
b |= (HexNibbleToValue(c) << 4u);
|
||||
str >> c;
|
||||
b |= HexNibbleToValue(c);
|
||||
return b;
|
||||
return static_cast<uint8_t>(parsedByte);
|
||||
}
|
||||
|
||||
public:
|
||||
byte_vector() = default;
|
||||
byte_vector(const std::initializer_list<uint8_t> list) : vector<uint8_t>(list) {}
|
||||
byte_vector(const std::initializer_list<uint8_t> list) : bytes_(list) {}
|
||||
template<class InputIt>
|
||||
byte_vector(InputIt begin, InputIt end) : vector<uint8_t>(begin, end) {}
|
||||
byte_vector(InputIt begin, InputIt end) : bytes_(begin, end) {}
|
||||
byte_vector(const byte_vector& rhs) = default;
|
||||
explicit byte_vector(const std::string& s) : byte_vector(s.begin(), s.end()) {}
|
||||
explicit byte_vector(const size_t size) : vector<uint8_t>(size) {}
|
||||
explicit byte_vector(void* data, const size_t size) : vector<uint8_t>(size) {
|
||||
this->assign((uint8_t*)data, (uint8_t*)data + size);
|
||||
byte_vector(byte_vector&& rhs) noexcept = default;
|
||||
explicit byte_vector(const std::string& s) : bytes_(s.begin(), s.end()) {}
|
||||
explicit byte_vector(std::vector<uint8_t> bytes) : bytes_(std::move(bytes)) {}
|
||||
explicit byte_vector(const size_t size) : bytes_(size) {}
|
||||
explicit byte_vector(const void* data, const size_t size) {
|
||||
auto* bytes = static_cast<const uint8_t*>(data);
|
||||
bytes_.assign(bytes, bytes + size);
|
||||
}
|
||||
|
||||
auto operator==(const vector<uint8_t>& vec) -> bool {
|
||||
if (this->size() != vec.size()) return false;
|
||||
for (size_t i = 0; i < size(); i++) {
|
||||
if ((*this)[i] != vec[i]) return false;
|
||||
}
|
||||
return true;
|
||||
auto operator=(const byte_vector& rhs) -> byte_vector& = default;
|
||||
auto operator=(byte_vector&& rhs) noexcept -> byte_vector& = default;
|
||||
|
||||
[[nodiscard]] auto operator==(const std::vector<uint8_t>& vec) const -> bool {
|
||||
return bytes_ == vec;
|
||||
}
|
||||
[[nodiscard]] auto operator==(const byte_vector& vec) const -> bool {
|
||||
return bytes_ == vec.bytes_;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto data() -> uint8_t* { return bytes_.data(); }
|
||||
[[nodiscard]] auto data() const -> const uint8_t* { return bytes_.data(); }
|
||||
[[nodiscard]] auto size() const -> size_t { return bytes_.size(); }
|
||||
[[nodiscard]] auto empty() const -> bool { return bytes_.empty(); }
|
||||
void reserve(const size_t count) { bytes_.reserve(count); }
|
||||
void resize(const size_t count) { bytes_.resize(count); }
|
||||
void swap(byte_vector& other) noexcept { bytes_.swap(other.bytes_); }
|
||||
|
||||
[[nodiscard]] auto begin() -> std::vector<uint8_t>::iterator { return bytes_.begin(); }
|
||||
[[nodiscard]] auto begin() const -> std::vector<uint8_t>::const_iterator {
|
||||
return bytes_.begin();
|
||||
}
|
||||
[[nodiscard]] auto end() -> std::vector<uint8_t>::iterator { return bytes_.end(); }
|
||||
[[nodiscard]] auto end() const -> std::vector<uint8_t>::const_iterator { return bytes_.end(); }
|
||||
[[nodiscard]] auto cbegin() const -> std::vector<uint8_t>::const_iterator {
|
||||
return bytes_.cbegin();
|
||||
}
|
||||
[[nodiscard]] auto cend() const -> std::vector<uint8_t>::const_iterator {
|
||||
return bytes_.cend();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto operator[](const size_t index) -> uint8_t& { return bytes_[index]; }
|
||||
[[nodiscard]] auto operator[](const size_t index) const -> uint8_t { return bytes_[index]; }
|
||||
|
||||
auto append(const char* bytes, const size_t length) {
|
||||
const auto previousSize = this->size();
|
||||
this->resize(previousSize + length);
|
||||
std::memcpy(this->data() + previousSize, bytes, length);
|
||||
const auto previousSize = size();
|
||||
resize(previousSize + length);
|
||||
std::memcpy(data() + previousSize, bytes, length);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
auto append(const T* elts, const size_t count) {
|
||||
append((char*)elts, count * sizeof(T));
|
||||
append(reinterpret_cast<const char*>(elts), count * sizeof(T));
|
||||
}
|
||||
|
||||
template<class T>
|
||||
auto append(const vector<T> elts) {
|
||||
auto append(const vector<T>& elts) {
|
||||
append(elts.data(), elts.size());
|
||||
}
|
||||
|
||||
@@ -98,7 +122,7 @@ public:
|
||||
}
|
||||
|
||||
auto operator+=(const uint8_t c) -> byte_vector& {
|
||||
this->insert(end(), c);
|
||||
bytes_.push_back(c);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -111,7 +135,7 @@ public:
|
||||
|
||||
template<class Iterable>
|
||||
auto operator+=(const Iterable& rhs) -> byte_vector& {
|
||||
this->insert(end(), rhs.cbegin(), rhs.cend());
|
||||
bytes_.insert(end(), rhs.cbegin(), rhs.cend());
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -119,7 +143,7 @@ public:
|
||||
if (size() < 8) {
|
||||
uint64_t total = 0;
|
||||
for (size_t i = 0; i < size(); i++) {
|
||||
total <<= 8u;
|
||||
total <<= 8U;
|
||||
total += (*this)[i];
|
||||
}
|
||||
return total;
|
||||
@@ -128,9 +152,9 @@ public:
|
||||
uint64_t firstBytes = 0;
|
||||
uint64_t lastBytes = 0;
|
||||
for (unsigned i = 0; i < 8; i++) {
|
||||
firstBytes <<= 8u;
|
||||
firstBytes <<= 8U;
|
||||
firstBytes += (*this)[i];
|
||||
lastBytes <<= 8u;
|
||||
lastBytes <<= 8U;
|
||||
lastBytes += (*this)[i];
|
||||
}
|
||||
|
||||
@@ -141,8 +165,8 @@ public:
|
||||
std::ifstream inputFileStream(path, std::ios::binary | std::ios::ate);
|
||||
const std::streamsize size = inputFileStream.tellg();
|
||||
inputFileStream.seekg(0, std::ios::beg);
|
||||
this->resize((size_t(size)));
|
||||
inputFileStream.read((char*)this->data(), size);
|
||||
this->resize(static_cast<size_t>(size));
|
||||
inputFileStream.read(reinterpret_cast<char*>(this->data()), size);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -153,34 +177,41 @@ public:
|
||||
}
|
||||
const std::streamsize size = inputFileStream.tellg();
|
||||
inputFileStream.seekg(0, std::ios::beg);
|
||||
byte_vector serializedGame((size_t(size)));
|
||||
inputFileStream.read((char*)serializedGame.data(), size);
|
||||
byte_vector serializedGame(static_cast<size_t>(size));
|
||||
inputFileStream.read(reinterpret_cast<char*>(serializedGame.data()), size);
|
||||
return serializedGame;
|
||||
}
|
||||
|
||||
static auto FromHex(const char* hexString) -> byte_vector { return FromHex(string(hexString)); }
|
||||
|
||||
static auto FromHex(string hexString) -> byte_vector {
|
||||
if (hexString[0] == '0' && (hexString[1] == 'x' || hexString[1] == 'X')) {
|
||||
hexString = string(hexString.begin() + 1, hexString.end());
|
||||
static auto FromHex(const string& hexString) -> byte_vector {
|
||||
std::string_view digits(hexString);
|
||||
if (digits.starts_with("0x") || digits.starts_with("0X")) {
|
||||
digits.remove_prefix(2);
|
||||
} else if (digits.starts_with("x") || digits.starts_with("X")) {
|
||||
digits.remove_prefix(1);
|
||||
}
|
||||
if (hexString[0] == 'x' || hexString[0] == 'X') {
|
||||
hexString = string(hexString.begin() + 1, hexString.end());
|
||||
}
|
||||
byte_vector vec;
|
||||
const size_t len = hexString.size();
|
||||
vec.reserve(len / 2);
|
||||
std::istringstream hexStream(hexString);
|
||||
|
||||
while (size_t(hexStream.tellg()) < len) { vec += HexCharsToByte(hexStream); }
|
||||
if (digits.size() % 2 != 0) { throw HexException(digits.empty() ? '\0' : digits.back()); }
|
||||
|
||||
byte_vector vec;
|
||||
vec.reserve(digits.size() / 2);
|
||||
|
||||
for (size_t index = 0; index < digits.size(); index += 2) {
|
||||
vec += HexCharsToByte(digits.substr(index, 2));
|
||||
}
|
||||
return vec;
|
||||
}
|
||||
|
||||
explicit operator std::string() const { return std::string(begin(), end()); }
|
||||
};
|
||||
|
||||
inline auto operator<<(std::ostream& ostr, byte_vector vec) -> std::ostream& {
|
||||
ostr.write((char*)vec.data(), (std::streamsize)vec.size());
|
||||
inline auto operator==(const std::vector<uint8_t>& lhs, const byte_vector& rhs) -> bool {
|
||||
return rhs == lhs;
|
||||
}
|
||||
|
||||
inline auto operator<<(std::ostream& ostr, const byte_vector& vec) -> std::ostream& {
|
||||
ostr.write(reinterpret_cast<const char*>(vec.data()), static_cast<std::streamsize>(vec.size()));
|
||||
return ostr;
|
||||
}
|
||||
|
||||
|
||||
@@ -209,8 +209,8 @@ auto AbstractMCTSAI::BuildMCTSTree(
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
for (auto& future : futures) { future.wait(); }
|
||||
// Wait for all threads to complete and propagate worker exceptions.
|
||||
for (auto& future : futures) { future.get(); }
|
||||
} else {
|
||||
// Single-threaded MCTS
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
|
||||
@@ -37,7 +37,7 @@ int main(const int argc, char** argv) {
|
||||
|
||||
void ModifyMap(HexMapProto& map) {
|
||||
const int terrainCount = map.terrain_size();
|
||||
for (int i = 0; i < terrainCount; i++) {
|
||||
for (int i = 0; i < terrainCount; ++i) {
|
||||
const auto existingTerrainType = map.terrain(i).type();
|
||||
const auto newTerrain =
|
||||
static_cast<net::eagle0::shardok::common::Terrain_Type>(existingTerrainType + 2);
|
||||
|
||||
@@ -37,16 +37,17 @@ auto CalculateMap(
|
||||
.name = mapName,
|
||||
.positionsRequiringCrossing = {}};
|
||||
|
||||
for (std::size_t i = 0; i < hexMap->attacker_starting_positions()->size(); i++) {
|
||||
for (std::size_t i = 0; i < hexMap->attacker_starting_positions()->size(); ++i) {
|
||||
const auto* positionList = hexMap->attacker_starting_positions()->Get(i);
|
||||
if (positionList->positions()->empty()) continue;
|
||||
if (positionList->positions()->size() != 10) {
|
||||
const auto* positions = positionList->positions();
|
||||
if (positions->empty()) continue;
|
||||
if (positions->size() != 10) {
|
||||
std::cerr << "Should have 10 starting positions!" << '\n';
|
||||
std::abort();
|
||||
}
|
||||
|
||||
int positionsRequiringCrossing = 0;
|
||||
for (const auto* startingPosition : *positionList->positions()) {
|
||||
for (const auto* startingPosition : *positions) {
|
||||
for (const shardok::Coords& castlePosition : criticalTileLocations) {
|
||||
if (!shardok::CanReach(
|
||||
*startingPosition,
|
||||
@@ -57,7 +58,7 @@ auto CalculateMap(
|
||||
battalionType)) {
|
||||
++positionsRequiringCrossing;
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
info.positionsRequiringCrossing[static_cast<int>(i)] = positionsRequiringCrossing;
|
||||
|
||||
@@ -18,7 +18,7 @@ struct alignas(64) OneMapInfo {
|
||||
std::map<int, int> positionsRequiringCrossing;
|
||||
};
|
||||
|
||||
auto CalculateMap(
|
||||
[[nodiscard]] auto CalculateMap(
|
||||
const std::string& mapName,
|
||||
const std::shared_ptr<shardok::ActionPointDistancesCache>& apdCache,
|
||||
const shardok::BattalionTypeSPtr& battalionType) -> OneMapInfo;
|
||||
|
||||
@@ -32,8 +32,10 @@ auto main(const int argc, char** argv) -> int {
|
||||
const shardok::BattalionTypeSPtr& battalionType = gameSettings->GetGetter().GetBattalionType(
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_INFANTRY);
|
||||
|
||||
const std::vector<std::string> mapNames = GetMapNames();
|
||||
std::vector<OneMapInfo> mapInfos{};
|
||||
for (const std::string& mapName : GetMapNames()) {
|
||||
mapInfos.reserve(mapNames.size());
|
||||
for (const std::string& mapName : mapNames) {
|
||||
std::shared_ptr<shardok::ActionPointDistancesCache> apdCache =
|
||||
std::make_shared<shardok::ActionPointDistancesCache>();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <climits>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
@@ -10,6 +11,8 @@
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/UnitConversions.hpp"
|
||||
@@ -44,25 +47,27 @@ void runBattle(const std::vector<shardok::Unit>& units) {
|
||||
if (fileNames.empty()) { throw std::runtime_error("No map files found"); }
|
||||
if (units.size() < 2) { throw std::runtime_error("Need at least two units to run a battle"); }
|
||||
|
||||
auto mapName = fileNames[randInt(0, (int)fileNames.size() - 1)];
|
||||
auto month = randInt(1, 12);
|
||||
const auto mapName = fileNames[randInt(0, static_cast<int>(fileNames.size()) - 1)];
|
||||
const auto month = randInt(1, 12);
|
||||
|
||||
const auto maxP1UnitCount = std::min<size_t>(20, units.size() - 1);
|
||||
auto p1UnitCount = randInt(1, static_cast<int>(maxP1UnitCount));
|
||||
const auto p1UnitCount = randInt(1, static_cast<int>(maxP1UnitCount));
|
||||
std::vector<shardok::Unit> p1Units{};
|
||||
for (int i = 0; i < p1UnitCount; i++) {
|
||||
p1Units.reserve(p1UnitCount);
|
||||
for (int i = 0; i < p1UnitCount; ++i) {
|
||||
auto unit = units[i];
|
||||
unit.mutate_player_id(0);
|
||||
p1Units.push_back(unit);
|
||||
p1Units.push_back(std::move(unit));
|
||||
}
|
||||
|
||||
const auto maxP2UnitCount = std::min<size_t>(20, units.size() - p1UnitCount);
|
||||
auto p2UnitCount = randInt(1, static_cast<int>(maxP2UnitCount));
|
||||
const auto p2UnitCount = randInt(1, static_cast<int>(maxP2UnitCount));
|
||||
std::vector<shardok::Unit> p2Units{};
|
||||
for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; i++) {
|
||||
p2Units.reserve(p2UnitCount);
|
||||
for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; ++i) {
|
||||
auto unit = units[i];
|
||||
unit.mutate_player_id(1);
|
||||
p2Units.push_back(unit);
|
||||
p2Units.push_back(std::move(unit));
|
||||
}
|
||||
|
||||
std::cout << "map name is " << mapName << ", " << p1UnitCount << " attacking " << p2UnitCount
|
||||
@@ -75,32 +80,34 @@ void runBattle(const std::vector<shardok::Unit>& units) {
|
||||
VictoryConditionProto::VICTORY_CONDITION_LAST_PLAYER_STANDING,
|
||||
VictoryConditionProto::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS};
|
||||
|
||||
auto p1 = shardok::PlayerInfoWithUnits(
|
||||
std::vector<shardok::PlayerInfoWithUnits> playerInfos;
|
||||
playerInfos.reserve(2);
|
||||
playerInfos.emplace_back(
|
||||
0,
|
||||
true,
|
||||
5,
|
||||
false,
|
||||
10000,
|
||||
attVictoryConditions,
|
||||
{},
|
||||
std::vector<shardok::PlayerId>{},
|
||||
p1Units,
|
||||
false);
|
||||
auto p2 = shardok::PlayerInfoWithUnits(
|
||||
playerInfos.emplace_back(
|
||||
1,
|
||||
true,
|
||||
7,
|
||||
true,
|
||||
10000,
|
||||
defVictoryConditions,
|
||||
{},
|
||||
std::vector<shardok::PlayerId>{},
|
||||
p2Units,
|
||||
false);
|
||||
|
||||
std::string gameId = std::to_string(randInt(INT_MIN, INT_MAX));
|
||||
const std::string gameId = std::to_string(randInt(INT_MIN, INT_MAX));
|
||||
|
||||
manager->UnlockedCreateSpecifiedGame(
|
||||
gameId,
|
||||
{p1, p2},
|
||||
playerInfos,
|
||||
mapName,
|
||||
"",
|
||||
month,
|
||||
@@ -135,10 +142,7 @@ auto LoadUnits() -> std::vector<shardok::Unit> {
|
||||
|
||||
auto main(int /*argc*/, char** argv) -> int {
|
||||
#if FIXED_SEED
|
||||
std::srand(4564564);
|
||||
g.seed(378473);
|
||||
#else
|
||||
std::srand(std::time(nullptr));
|
||||
#endif
|
||||
|
||||
FilesystemUtils::SetExecPath(argv[0]);
|
||||
@@ -152,9 +156,9 @@ auto main(int /*argc*/, char** argv) -> int {
|
||||
|
||||
manager = std::make_unique<shardok::ShardokGamesManager>(std::vector<std::string>());
|
||||
|
||||
std::thread threads[THREAD_COUNT];
|
||||
std::array<std::thread, THREAD_COUNT> threads;
|
||||
|
||||
for (int i = 0; i < THREAD_COUNT; i++) {
|
||||
for (int i = 0; i < THREAD_COUNT; ++i) {
|
||||
threads[i] = std::thread(runBattlesThread, randomUnits);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,25 +25,44 @@ DIST_T NormalizedCostWhenBraving(const DIST_T cost) {
|
||||
|
||||
struct TargetAndDistance {
|
||||
Coords target;
|
||||
size_t targetIndex;
|
||||
CoordsSet attackLocations;
|
||||
double targetPower;
|
||||
DIST_T distance;
|
||||
|
||||
TargetAndDistance(const Coords& t, const CoordsSet& al, const double tp, const DIST_T d)
|
||||
TargetAndDistance(
|
||||
const Coords& t,
|
||||
const size_t ti,
|
||||
const CoordsSet& al,
|
||||
const double tp,
|
||||
const DIST_T d)
|
||||
: target(t),
|
||||
targetIndex(ti),
|
||||
attackLocations(al),
|
||||
targetPower(tp),
|
||||
distance(d) {}
|
||||
};
|
||||
|
||||
auto CoordsIndex(const Coords& coords, const int columnCount) {
|
||||
return coords.row() * columnCount + coords.column();
|
||||
}
|
||||
|
||||
auto CoordsFromIndex(const size_t index, const int columnCount) -> Coords {
|
||||
return Coords(
|
||||
static_cast<int8_t>(index / static_cast<size_t>(columnCount)),
|
||||
static_cast<int8_t>(index % static_cast<size_t>(columnCount)));
|
||||
}
|
||||
|
||||
auto MinDistance(
|
||||
const Coords& start,
|
||||
const CoordsSet& destinations,
|
||||
const ActionPointDistances* apd) -> DIST_T {
|
||||
DIST_T minDistance = ActionPointDistances::IMPOSSIBLE;
|
||||
const int startIndex = CoordsIndex(start, destinations.ColumnCount());
|
||||
|
||||
for (const Coords& dest : destinations) {
|
||||
if (const DIST_T distance = apd->Distance(start, dest); distance < minDistance) {
|
||||
for (const auto destIndex : destinations.indexIterator()) {
|
||||
if (const DIST_T distance = apd->Distance(startIndex, static_cast<int>(destIndex));
|
||||
distance < minDistance) {
|
||||
minDistance = distance;
|
||||
}
|
||||
}
|
||||
@@ -113,10 +132,6 @@ auto EffectiveDistance(
|
||||
|
||||
auto Power(const Unit* unit) -> double { return unit->battalion().size(); }
|
||||
|
||||
auto CoordsIndex(const Coords& coords, const int columnCount) {
|
||||
return coords.row() * columnCount + coords.column();
|
||||
}
|
||||
|
||||
// Chooses a list of targets in priority order for each unit.
|
||||
auto GenerateTargetPriorities(
|
||||
const vector<const Unit*>& occupants,
|
||||
@@ -153,6 +168,7 @@ auto GenerateTargetPriorities(
|
||||
vector<TargetAndAttackLocations>{});
|
||||
|
||||
vector<TargetAndDistance> targetsWithDistance;
|
||||
targetsWithDistance.reserve(targets.size());
|
||||
|
||||
// Get APDs directly from cache (now with built-in thread-local optimization)
|
||||
const auto& battType = battalionTypeGetter(unit->battalion().type());
|
||||
@@ -162,10 +178,10 @@ auto GenerateTargetPriorities(
|
||||
bravingApd = apdCache->GetRaw(map, mapId, battType, true, braveWaterCost);
|
||||
}
|
||||
|
||||
for (const Coords& targetLocation : targets) {
|
||||
const auto coordsIndex = CoordsIndex(targetLocation, cc);
|
||||
const auto& occupant = occupants[coordsIndex];
|
||||
for (const auto targetIndex : targets.indexIterator()) {
|
||||
const auto& occupant = occupants[targetIndex];
|
||||
|
||||
const Coords targetLocation = CoordsFromIndex(targetIndex, cc);
|
||||
const auto& attackLocations = alCache->CachedLocations(targetLocation, isLateGame);
|
||||
|
||||
if (occupant && occupant->player_id() != attackerId) {
|
||||
@@ -178,12 +194,14 @@ auto GenerateTargetPriorities(
|
||||
|
||||
targetsWithDistance.emplace_back(
|
||||
targetLocation,
|
||||
targetIndex,
|
||||
attackLocsForUnit,
|
||||
occupantPower,
|
||||
distance);
|
||||
} else {
|
||||
targetsWithDistance.emplace_back(
|
||||
targetLocation,
|
||||
targetIndex,
|
||||
attackLocations.LocationsWithEnemyInRange(unit),
|
||||
occupantPower,
|
||||
ActionPointDistances::IMPOSSIBLE);
|
||||
@@ -194,13 +212,11 @@ auto GenerateTargetPriorities(
|
||||
// Sort by distance
|
||||
std::ranges::sort(
|
||||
targetsWithDistance,
|
||||
[&powerAttackingEachTarget,
|
||||
cc](const TargetAndDistance& left, const TargetAndDistance& right) {
|
||||
const auto leftIndex = CoordsIndex(left.target, cc);
|
||||
const auto rightIndex = CoordsIndex(right.target, cc);
|
||||
|
||||
const double powerAttackingLeft = powerAttackingEachTarget[leftIndex];
|
||||
const double powerAttackingRight = powerAttackingEachTarget[rightIndex];
|
||||
[&powerAttackingEachTarget](
|
||||
const TargetAndDistance& left,
|
||||
const TargetAndDistance& right) {
|
||||
const double powerAttackingLeft = powerAttackingEachTarget[left.targetIndex];
|
||||
const double powerAttackingRight = powerAttackingEachTarget[right.targetIndex];
|
||||
|
||||
const bool leftAlreadyOverwhelmed =
|
||||
powerAttackingLeft >= kOverpowerRatio * left.targetPower;
|
||||
@@ -214,8 +230,7 @@ auto GenerateTargetPriorities(
|
||||
});
|
||||
|
||||
if (!targetsWithDistance.empty()) {
|
||||
powerAttackingEachTarget[CoordsIndex(targetsWithDistance.front().target, cc)] +=
|
||||
Power(unit);
|
||||
powerAttackingEachTarget[targetsWithDistance.front().targetIndex] += Power(unit);
|
||||
}
|
||||
|
||||
tpl.priorityOrder.reserve(targetsWithDistance.size());
|
||||
|
||||
@@ -27,7 +27,7 @@ struct TargetAndAttackLocations {
|
||||
Coords target;
|
||||
CoordsSet attackLocations;
|
||||
|
||||
auto operator==(const TargetAndAttackLocations& rhs) const -> bool = default;
|
||||
[[nodiscard]] auto operator==(const TargetAndAttackLocations& rhs) const -> bool = default;
|
||||
};
|
||||
|
||||
struct TargetPriorityList {
|
||||
@@ -39,7 +39,7 @@ struct TargetPriorityList {
|
||||
priorityOrder(po) {}
|
||||
};
|
||||
|
||||
auto EffectiveDistance(
|
||||
[[nodiscard]] auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
const HexMap* map,
|
||||
const AttackLocations& attackLocations,
|
||||
@@ -47,7 +47,7 @@ auto EffectiveDistance(
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> DIST_T;
|
||||
|
||||
auto EffectiveDistance(
|
||||
[[nodiscard]] auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
const HexMap* map,
|
||||
const CoordsSet& locations,
|
||||
@@ -55,14 +55,14 @@ auto EffectiveDistance(
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> DIST_T;
|
||||
|
||||
auto EffectiveDistance(
|
||||
[[nodiscard]] auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
const ActionPointDistances* notBravingApd,
|
||||
const ActionPointDistances* bravingApd,
|
||||
const CoordsSet& locations) -> DIST_T;
|
||||
|
||||
// Chooses a list of targets in priority order for each unit.
|
||||
auto GenerateTargetPriorities(
|
||||
[[nodiscard]] auto GenerateTargetPriorities(
|
||||
const std::vector<const Unit*>& occupants,
|
||||
const HexMap* map,
|
||||
const CoordsSet& targets,
|
||||
|
||||
@@ -53,8 +53,8 @@ AttackLocationsCache::AttackLocationsCache(const HexMap *hexMap, const SettingsG
|
||||
|
||||
auto emptySet = CoordsSet(columnCount, rowCount);
|
||||
|
||||
for (MapIndex r = 0; r < hexMap->row_count(); r++) {
|
||||
for (MapIndex c = 0; c < hexMap->column_count(); c++) {
|
||||
for (MapIndex r = 0; r < hexMap->row_count(); ++r) {
|
||||
for (MapIndex c = 0; c < hexMap->column_count(); ++c) {
|
||||
auto location = Coords(r, c);
|
||||
const auto terrain = GetTerrain(hexMap, location);
|
||||
|
||||
@@ -114,8 +114,7 @@ auto AttackLocationsCache::CachedLocations(const CoordsSet &targets, const bool
|
||||
-> AttackLocations {
|
||||
AttackLocations al(rowCount, columnCount);
|
||||
|
||||
for (const auto &target : targets) {
|
||||
const int index = target.row() * columnCount + target.column();
|
||||
for (const auto index : targets.indexIterator()) {
|
||||
if (lateGame) {
|
||||
al += lateGameLocations[index];
|
||||
} else {
|
||||
@@ -137,4 +136,4 @@ auto AttackLocationsCache::CachedLocations(const vector<const Unit *> &enemies,
|
||||
return CachedLocations(targets, lateGame);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
@@ -33,6 +33,7 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
bool canFlee = false;
|
||||
|
||||
vector<const Unit*> attackerUnits{};
|
||||
attackerUnits.reserve(gameState->units()->size());
|
||||
CoordsSet defenderPositions(gameState->hex_map());
|
||||
for (const Unit* unit : *gameState->units()) {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
@@ -131,4 +132,4 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
return chosenStrategy;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "AICommandFilter.hpp"
|
||||
#include "TranspositionTable.hpp"
|
||||
@@ -21,16 +23,49 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// No need to forward declare internal functions - use the public interface instead
|
||||
|
||||
// Helper constants and static variables
|
||||
static const std::vector<double> kAverageSequence = {0.5};
|
||||
static const auto kAverageGenerator = std::make_shared<SequenceRandomGenerator>(kAverageSequence);
|
||||
|
||||
constexpr bool kMultithread = true;
|
||||
constexpr bool kLogging = false;
|
||||
|
||||
// Helper function to determine if a command type is deterministic
|
||||
[[nodiscard]] static auto AverageGenerator() -> std::shared_ptr<SequenceRandomGenerator> {
|
||||
return std::make_shared<SequenceRandomGenerator>(std::vector{0.5});
|
||||
}
|
||||
|
||||
[[nodiscard]] static auto DescribeCommandForLog(const CommandSPtr& command) -> std::string {
|
||||
if (!command) { return "<null command>"; }
|
||||
|
||||
std::ostringstream out;
|
||||
out << "type=" << command->GetCommandType()
|
||||
<< " player=" << static_cast<int>(command->GetPlayerId())
|
||||
<< " actor=" << command->GetActorUnitId() << " target=("
|
||||
<< static_cast<int>(command->GetTargetRow()) << ','
|
||||
<< static_cast<int>(command->GetTargetColumn()) << ')';
|
||||
|
||||
if (command->HasOdds()) { out << " odds_percentile=" << command->GetOddsPercentile(); }
|
||||
|
||||
return out.str();
|
||||
}
|
||||
|
||||
[[nodiscard]] static auto DescribeCommandAt(
|
||||
const CommandListSPtr& commands,
|
||||
const size_t commandIndex) -> std::string {
|
||||
if (!commands) { return "command=<null command list>"; }
|
||||
if (commandIndex >= commands->size()) {
|
||||
std::ostringstream out;
|
||||
out << "command=<index " << commandIndex << " out of " << commands->size() << ">";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::ostringstream out;
|
||||
out << "command={" << DescribeCommandForLog(commands->at(commandIndex)) << '}';
|
||||
return out.str();
|
||||
}
|
||||
|
||||
[[nodiscard]] static auto WithExceptionContext(
|
||||
const std::string& context,
|
||||
const std::exception& exception) -> std::runtime_error {
|
||||
return std::runtime_error(context + ": " + exception.what());
|
||||
}
|
||||
|
||||
static auto IsDeterministic(const CommandType type) -> bool {
|
||||
switch (type) {
|
||||
case net::eagle0::shardok::common::MOVE_COMMAND:
|
||||
@@ -43,13 +78,11 @@ static auto IsDeterministic(const CommandType type) -> bool {
|
||||
case net::eagle0::shardok::common::PLACE_HIDDEN_UNIT_COMMAND:
|
||||
case net::eagle0::shardok::common::UNIT_STOP_COMMAND:
|
||||
case net::eagle0::shardok::common::UNIT_REST_COMMAND:
|
||||
case net::eagle0::shardok::common::FLEE_COMMAND:
|
||||
case net::eagle0::shardok::common::REINFORCE_COMMAND:
|
||||
case net::eagle0::shardok::common::RETREAT_COMMAND:
|
||||
case net::eagle0::shardok::common::END_PLAYER_SETUP_COMMAND:
|
||||
case net::eagle0::shardok::common::HIDE_COMMAND:
|
||||
case net::eagle0::shardok::common::FORTIFY_COMMAND:
|
||||
case net::eagle0::shardok::common::BECOME_OUTLAW_COMMAND:
|
||||
case net::eagle0::shardok::common::HOLY_WAVE_COMMAND:
|
||||
case net::eagle0::shardok::common::EVACUATE_PRISONERS_COMMAND:
|
||||
case net::eagle0::shardok::common::REPAIR_COMMAND: return true;
|
||||
@@ -63,7 +96,14 @@ static auto RandomnessSampleForRepeat(const int repeatIteration, const int maxRe
|
||||
return static_cast<double>(repeatIteration) / static_cast<double>(maxRepeatCount - 1);
|
||||
}
|
||||
|
||||
// Helper function to sort commands by score
|
||||
static auto SuccessSampleForOdds(const double successChance) -> double {
|
||||
return successChance / 2.0;
|
||||
}
|
||||
|
||||
static auto FailureSampleForOdds(const double successChance) -> double {
|
||||
return successChance + ((1.0 - successChance) / 2.0);
|
||||
}
|
||||
|
||||
static auto CommandSorter(
|
||||
const AICommandEvaluator::IndexAndScore& l,
|
||||
const AICommandEvaluator::IndexAndScore& r) -> bool {
|
||||
@@ -83,7 +123,7 @@ AICommandEvaluator::AICommandEvaluator(
|
||||
BattalionTypeGetter battalionTypeGetter)
|
||||
: scorer_(scorer),
|
||||
apdCache_(apdCache),
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)) {} // Move the function object
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)) {}
|
||||
|
||||
auto AICommandEvaluator::PerformLookahead(
|
||||
const PlayerId pid,
|
||||
@@ -138,17 +178,34 @@ auto AICommandEvaluator::PerformLookahead(
|
||||
std::launch::deferred,
|
||||
[bestCommandFuture = std::move(bestCommandFuture),
|
||||
innerEngine,
|
||||
nextCommands,
|
||||
pid,
|
||||
nextUtility,
|
||||
remainingLookahead]() mutable -> EvaluationResult {
|
||||
const auto bestCommand = bestCommandFuture.get();
|
||||
IndexAndScore bestCommand;
|
||||
try {
|
||||
bestCommand = bestCommandFuture.get();
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI PerformLookahead failed while resolving best command"
|
||||
<< " player=" << static_cast<int>(pid)
|
||||
<< " remaining_lookahead=" << remainingLookahead
|
||||
<< " next_command_count=" << nextCommands->size();
|
||||
throw WithExceptionContext(context.str(), e);
|
||||
}
|
||||
if (!bestCommand.completed) {
|
||||
return EvaluationResult{.score = nextUtility, .completed = false};
|
||||
}
|
||||
if (bestCommand.index >= nextCommands->size()) {
|
||||
std::cerr << "AI lookahead produced command index " << bestCommand.index
|
||||
<< " with only " << nextCommands->size()
|
||||
<< " available commands for player " << static_cast<int>(pid)
|
||||
<< " at remaining lookahead " << remainingLookahead << "\n";
|
||||
return EvaluationResult{.score = nextUtility, .completed = false};
|
||||
}
|
||||
|
||||
ScoreValue resultScore;
|
||||
if (auto& nextCommand = innerEngine->GetAvailableCommandsForAIPlayer(pid)->at(
|
||||
bestCommand.index);
|
||||
if (const auto& nextCommand = nextCommands->at(bestCommand.index);
|
||||
nextCommand->GetCommandType() !=
|
||||
net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
resultScore = bestCommand.immediateScore;
|
||||
@@ -199,14 +256,36 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
const auto guessedCommands = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
|
||||
auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
|
||||
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
|
||||
try {
|
||||
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI EvaluateWithRandomness failed while posting simulated command"
|
||||
<< " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
|
||||
<< " guessed_command_count=" << guessedCommands->size()
|
||||
<< " remaining_lookahead=" << remainingLookahead << ' '
|
||||
<< DescribeCommandAt(guessedCommands, commandIndex);
|
||||
throw WithExceptionContext(context.str(), e);
|
||||
}
|
||||
|
||||
auto innerUtility = scorer_.GuessedStateScore(
|
||||
isDefender,
|
||||
innerEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords);
|
||||
ScoreValue innerUtility;
|
||||
try {
|
||||
innerUtility = scorer_.GuessedStateScore(
|
||||
isDefender,
|
||||
innerEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords);
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI EvaluateWithRandomness failed while scoring simulated state"
|
||||
<< " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
|
||||
<< " guessed_command_count=" << guessedCommands->size()
|
||||
<< " remaining_lookahead=" << remainingLookahead << ' '
|
||||
<< DescribeCommandAt(guessedCommands, commandIndex);
|
||||
throw WithExceptionContext(context.str(), e);
|
||||
}
|
||||
|
||||
returnValue.immediateScore = innerUtility;
|
||||
returnValue.completed = true;
|
||||
@@ -221,22 +300,33 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
isDefender,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
commandIndex,
|
||||
innerEngine,
|
||||
attackerStrategy,
|
||||
innerUtility,
|
||||
&allCastleCoords,
|
||||
guessedCommands,
|
||||
allCastleCoords,
|
||||
deadline]() -> EvaluationResult {
|
||||
auto lookaheadFuture = PerformLookahead(
|
||||
pid,
|
||||
isDefender,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
innerEngine,
|
||||
innerUtility,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
return lookaheadFuture.get();
|
||||
try {
|
||||
auto lookaheadFuture = PerformLookahead(
|
||||
pid,
|
||||
isDefender,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
innerEngine,
|
||||
innerUtility,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
return lookaheadFuture.get();
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI EvaluateWithRandomness failed during recursive lookahead"
|
||||
<< " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
|
||||
<< " remaining_lookahead=" << remainingLookahead << ' '
|
||||
<< DescribeCommandAt(guessedCommands, commandIndex);
|
||||
throw WithExceptionContext(context.str(), e);
|
||||
}
|
||||
};
|
||||
|
||||
if constexpr (kMultithread) {
|
||||
@@ -327,18 +417,30 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
|
||||
// Structure to hold all command evaluation data
|
||||
struct CommandEvaluation {
|
||||
size_t index;
|
||||
CommandType type;
|
||||
ScoreValue immediateScore;
|
||||
size_t index{};
|
||||
CommandType type{};
|
||||
ScoreValue immediateScore{};
|
||||
bool immediateCompleted = true;
|
||||
std::vector<std::future<EvaluationResult>> lookaheadFutures;
|
||||
};
|
||||
|
||||
std::vector<CommandEvaluation> commandEvaluations(commandCount);
|
||||
|
||||
for (uint32_t index = 0; index < commandCount; index++) {
|
||||
for (uint32_t index = 0; index < commandCount; ++index) {
|
||||
const auto originalIndex = filteredIndices[index];
|
||||
const auto& guessedDescriptor = guessedDescriptors->at(originalIndex);
|
||||
const CommandSPtr guessedDescriptor = [&] {
|
||||
try {
|
||||
return guessedDescriptors->at(originalIndex);
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI FindBestCommand failed while reading filtered command"
|
||||
<< " player=" << static_cast<int>(pid) << " filtered_index=" << index
|
||||
<< " original_index=" << originalIndex
|
||||
<< " guessed_command_count=" << guessedDescriptors->size()
|
||||
<< " filtered_command_count=" << filteredIndices.size();
|
||||
throw WithExceptionContext(context.str(), e);
|
||||
}
|
||||
}();
|
||||
const auto guessedCommandType = guessedDescriptor->GetCommandType();
|
||||
|
||||
commandEvaluations[index].index = originalIndex;
|
||||
@@ -356,7 +458,7 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
originalIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
kAverageGenerator,
|
||||
AverageGenerator(),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
@@ -370,7 +472,7 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
|
||||
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
|
||||
|
||||
// Success attempt uses 1.0 - (successChance / 2) as the roll
|
||||
// Success attempt uses the midpoint of the successful percentile range.
|
||||
auto successEvaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
@@ -378,13 +480,13 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(
|
||||
std::vector{1.0 - successChance / 2.0}),
|
||||
std::vector{SuccessSampleForOdds(successChance)}),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
// Failure attempt uses the average of (1 - successChance) and 0 as the roll
|
||||
// Failure attempt uses the midpoint of the failing percentile range.
|
||||
auto failureEvaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
@@ -392,7 +494,7 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(
|
||||
std::vector{(1.0 - successChance) / 2.0}),
|
||||
std::vector{FailureSampleForOdds(successChance)}),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
@@ -422,7 +524,8 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
} else {
|
||||
ScoreValue sum = 0.0;
|
||||
const int sampleCount = std::max(1, maxRepeatCount);
|
||||
for (int repeatIteration = 0; repeatIteration < sampleCount; repeatIteration++) {
|
||||
commandEvaluations[index].lookaheadFutures.reserve(sampleCount);
|
||||
for (int repeatIteration = 0; repeatIteration < sampleCount; ++repeatIteration) {
|
||||
// In each iteration, use a double from [0, 1] as the random roll
|
||||
auto sequence =
|
||||
std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
|
||||
@@ -432,7 +535,7 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
originalIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(sequence),
|
||||
std::make_shared<SequenceRandomGenerator>(std::move(sequence)),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
@@ -459,7 +562,17 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
ScoreValue totalLookaheadScore = 0.0;
|
||||
bool completed = eval.immediateCompleted;
|
||||
for (auto& future : eval.lookaheadFutures) {
|
||||
const auto lookahead = future.get();
|
||||
EvaluationResult lookahead;
|
||||
try {
|
||||
lookahead = future.get();
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI FindBestCommand failed while resolving lookahead future"
|
||||
<< " command_index=" << eval.index
|
||||
<< " command_type=" << eval.type
|
||||
<< " lookahead_future_count=" << eval.lookaheadFutures.size();
|
||||
throw WithExceptionContext(context.str(), e);
|
||||
}
|
||||
if (!lookahead.completed) { completed = false; }
|
||||
totalLookaheadScore += lookahead.score;
|
||||
}
|
||||
@@ -511,26 +624,30 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
}
|
||||
|
||||
const auto& guessedDescriptor = guessedDescriptors->at(commandIndex);
|
||||
const auto guessedCommandType = guessedDescriptor->GetCommandType();
|
||||
|
||||
if (const auto guessedCommandType = guessedDescriptor->GetCommandType();
|
||||
guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
if (guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
std::promise<EvaluationResult> p;
|
||||
p.set_value(EvaluationResult{.score = currentUtility, .completed = true});
|
||||
return p.get_future();
|
||||
} else if (IsDeterministic(guessedCommandType)) {
|
||||
}
|
||||
|
||||
if (IsDeterministic(guessedCommandType)) {
|
||||
auto evaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
kAverageGenerator,
|
||||
AverageGenerator(),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
return std::move(evaluation.lookaheadScore);
|
||||
} else if (guessedDescriptor->HasOdds()) {
|
||||
}
|
||||
|
||||
if (guessedDescriptor->HasOdds()) {
|
||||
const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
|
||||
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
|
||||
|
||||
@@ -541,7 +658,8 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(std::vector{1.0 - successChance / 2.0}),
|
||||
std::make_shared<SequenceRandomGenerator>(
|
||||
std::vector{SuccessSampleForOdds(successChance)}),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
@@ -554,7 +672,8 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(std::vector{(1.0 - successChance) / 2.0}),
|
||||
std::make_shared<SequenceRandomGenerator>(
|
||||
std::vector{FailureSampleForOdds(successChance)}),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
@@ -575,47 +694,47 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
.score = std::lerp(failure.score, success.score, successChance),
|
||||
.completed = true};
|
||||
});
|
||||
} else {
|
||||
// For non-deterministic commands without odds, use multiple attempts
|
||||
std::vector<std::future<EvaluationResult>> lookaheadFutures;
|
||||
const int sampleCount = std::max(1, maxRepeatCount);
|
||||
lookaheadFutures.reserve(sampleCount);
|
||||
|
||||
for (int repeatIteration = 0; repeatIteration < sampleCount; repeatIteration++) {
|
||||
auto sequence = std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
|
||||
auto evaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(sequence),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
lookaheadFutures.push_back(std::move(evaluation.lookaheadScore));
|
||||
}
|
||||
|
||||
// Return a future that computes the average when needed
|
||||
return std::async(
|
||||
std::launch::deferred,
|
||||
[lookaheadFutures = std::move(lookaheadFutures),
|
||||
sampleCount]() mutable -> EvaluationResult {
|
||||
ScoreValue total = 0.0;
|
||||
bool completed = true;
|
||||
for (auto& future : lookaheadFutures) {
|
||||
const auto result = future.get();
|
||||
if (!result.completed) { completed = false; }
|
||||
total += result.score;
|
||||
}
|
||||
if (!completed) { return EvaluationResult{.score = 0.0, .completed = false}; }
|
||||
return EvaluationResult{
|
||||
.score = total / static_cast<ScoreValue>(sampleCount),
|
||||
.completed = true};
|
||||
});
|
||||
}
|
||||
|
||||
// For non-deterministic commands without odds, use multiple attempts
|
||||
std::vector<std::future<EvaluationResult>> lookaheadFutures;
|
||||
const int sampleCount = std::max(1, maxRepeatCount);
|
||||
lookaheadFutures.reserve(sampleCount);
|
||||
|
||||
for (int repeatIteration = 0; repeatIteration < sampleCount; ++repeatIteration) {
|
||||
auto sequence = std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
|
||||
auto evaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(std::move(sequence)),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
lookaheadFutures.push_back(std::move(evaluation.lookaheadScore));
|
||||
}
|
||||
|
||||
// Return a future that computes the average when needed
|
||||
return std::async(
|
||||
std::launch::deferred,
|
||||
[lookaheadFutures = std::move(lookaheadFutures),
|
||||
sampleCount]() mutable -> EvaluationResult {
|
||||
ScoreValue total = 0.0;
|
||||
bool completed = true;
|
||||
for (auto& future : lookaheadFutures) {
|
||||
const auto result = future.get();
|
||||
if (!result.completed) { completed = false; }
|
||||
total += result.score;
|
||||
}
|
||||
if (!completed) { return EvaluationResult{.score = 0.0, .completed = false}; }
|
||||
return EvaluationResult{
|
||||
.score = total / static_cast<ScoreValue>(sampleCount),
|
||||
.completed = true};
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -57,11 +57,11 @@ public:
|
||||
|
||||
/// Find the best command among all available commands at the given depth.
|
||||
struct IndexAndScore {
|
||||
size_t index;
|
||||
CommandType type;
|
||||
ScoreValue lookaheadScore;
|
||||
ScoreValue immediateScore;
|
||||
bool completed;
|
||||
size_t index{};
|
||||
CommandType type{};
|
||||
ScoreValue lookaheadScore{};
|
||||
ScoreValue immediateScore{};
|
||||
bool completed{};
|
||||
};
|
||||
|
||||
[[nodiscard]] auto FindBestCommand(
|
||||
@@ -78,11 +78,11 @@ public:
|
||||
private:
|
||||
const AIScoreCalculator& scorer_;
|
||||
const APDCache& apdCache_;
|
||||
BattalionTypeGetter battalionTypeGetter_; // Store by value
|
||||
BattalionTypeGetter battalionTypeGetter_;
|
||||
|
||||
struct ImmediateAndLookaheadScore {
|
||||
ScoreValue immediateScore;
|
||||
bool completed;
|
||||
ScoreValue immediateScore{};
|
||||
bool completed{};
|
||||
std::future<EvaluationResult> lookaheadScore;
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
bool canFlee = false;
|
||||
|
||||
vector<UnitId> attackerUnitIdsRequiringWaterCrossing{};
|
||||
attackerUnitIdsRequiringWaterCrossing.reserve(gameState->units()->size());
|
||||
for (const auto& player : *gameState->player_infos()) {
|
||||
if (!player->is_defender()) {
|
||||
const auto& unitIdsRequiringWaterCrossing = UnitIdsRequiringWaterCrossing(
|
||||
|
||||
@@ -104,23 +104,21 @@ auto DefenderDistanceBuf(
|
||||
pointCostToDesiredTargetWithBraving);
|
||||
}
|
||||
|
||||
std::sort(
|
||||
begin(pointCosts),
|
||||
end(pointCosts),
|
||||
[](const WithoutAndWith &left, const WithoutAndWith &right) {
|
||||
return left.without == right.without ? left.with < right.with
|
||||
: left.without < right.without;
|
||||
});
|
||||
std::ranges::sort(pointCosts, [](const WithoutAndWith &left, const WithoutAndWith &right) {
|
||||
return left.without == right.without ? left.with < right.with
|
||||
: left.without < right.without;
|
||||
});
|
||||
|
||||
// experiment: add dummy values to the end with very high cost, so there's never an advantage
|
||||
// to leaving a defender unit alive in a castle.
|
||||
for (size_t i = attackerUnits.size(); i < 20; i++) { pointCosts.emplace_back(999, 999); }
|
||||
for (size_t i = attackerUnits.size(); i < 20; ++i) { pointCosts.emplace_back(999, 999); }
|
||||
|
||||
double sum = 0.0;
|
||||
double decr = kPerUnitDebufDecay;
|
||||
for (const WithoutAndWith &pointCost : pointCosts) {
|
||||
const int effectiveCost =
|
||||
(pointCost.without == 999) ? int(1.2 * pointCost.with) : pointCost.without;
|
||||
const int effectiveCost = (pointCost.without == 999)
|
||||
? static_cast<int>(1.2 * pointCost.with)
|
||||
: pointCost.without;
|
||||
sum += decr / (effectiveCost + 1);
|
||||
decr *= kPerUnitDebufDecay;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
auto DefenderDistanceBuf(
|
||||
[[nodiscard]] auto DefenderDistanceBuf(
|
||||
const Coords &defenderLocation,
|
||||
const HexMap *hexMap,
|
||||
const MapId &mapId,
|
||||
|
||||
@@ -5,18 +5,32 @@
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_EXPERIMENT_CONFIG_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_EXPERIMENT_CONFIG_HPP
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
namespace shardok {
|
||||
|
||||
inline auto CurrentAIExperimentId() -> int {
|
||||
[[nodiscard]] inline auto CurrentAIExperimentId() -> int {
|
||||
const char* rawValue = std::getenv("SHARDOK_SCORING_EXPERIMENT_ID");
|
||||
if (rawValue == nullptr || std::string(rawValue).empty()) { return 0; }
|
||||
return std::atoi(rawValue);
|
||||
if (rawValue == nullptr) { return 0; }
|
||||
|
||||
const std::string_view value(rawValue);
|
||||
if (value.empty()) { return 0; }
|
||||
|
||||
int experimentId = 0;
|
||||
const auto parseResult =
|
||||
std::from_chars(value.data(), value.data() + value.size(), experimentId);
|
||||
if (parseResult.ec != std::errc{} || parseResult.ptr != value.data() + value.size()) {
|
||||
return 0;
|
||||
}
|
||||
return experimentId;
|
||||
}
|
||||
|
||||
inline auto IsAIExperiment(const int id) -> bool { return CurrentAIExperimentId() == id; }
|
||||
[[nodiscard]] inline auto IsAIExperiment(const int id) -> bool {
|
||||
return CurrentAIExperimentId() == id;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace shardok {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double kCertainCombatWinChance = 0.95;
|
||||
|
||||
void WritePercent(std::ostream& output, const double value) {
|
||||
const auto flags = output.flags();
|
||||
const auto precision = output.precision();
|
||||
@@ -154,8 +156,8 @@ auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
const CommandListSPtr& availableCommands,
|
||||
const CommandList::const_iterator& fleeCommand,
|
||||
int maxRounds,
|
||||
int minimumFleeOddsThreshold,
|
||||
int desperateFleeThreshold,
|
||||
int /*minimumFleeOddsThreshold*/,
|
||||
int /*desperateFleeThreshold*/,
|
||||
bool enableDebugLogging) -> FleeDecision {
|
||||
// Get flee success odds
|
||||
const int fleeSuccessChance = (*fleeCommand)->GetOddsPercentile();
|
||||
@@ -164,67 +166,31 @@ auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
std::cout << "AI FinalRound: Evaluating flee (odds=" << fleeSuccessChance << "%)...\n";
|
||||
}
|
||||
|
||||
// Check if flee odds are good enough to attempt
|
||||
if (fleeSuccessChance >= minimumFleeOddsThreshold) {
|
||||
if (enableDebugLogging) {
|
||||
std::cout << "AI FinalRound: Good flee odds (" << fleeSuccessChance
|
||||
<< "% >= " << minimumFleeOddsThreshold << "%), choosing flee\n";
|
||||
}
|
||||
return FleeDecision{
|
||||
true,
|
||||
GetFleeCommandIndex(fleeCommand, availableCommands),
|
||||
"Good flee odds"};
|
||||
}
|
||||
|
||||
// Low flee odds - evaluate if fighting might be better
|
||||
const double combatWinChance = EstimateCombatSuccess(playerId, guessedState, maxRounds);
|
||||
|
||||
// If combat situation is hopeless, even bad flee odds are better than certain death
|
||||
if (combatWinChance <= 0.05 && fleeSuccessChance >= desperateFleeThreshold) {
|
||||
if (combatWinChance < kCertainCombatWinChance) {
|
||||
if (enableDebugLogging) {
|
||||
std::cout << "AI FinalRound: Combat hopeless (";
|
||||
std::cout << "AI FinalRound: Combat not certain (";
|
||||
WritePercent(std::cout, combatWinChance * 100);
|
||||
std::cout << "%), desperate flee attempt (" << fleeSuccessChance << "%)\n";
|
||||
std::cout << "% < ";
|
||||
WritePercent(std::cout, kCertainCombatWinChance * 100);
|
||||
std::cout << "%), choosing flee\n";
|
||||
}
|
||||
return FleeDecision{
|
||||
true,
|
||||
GetFleeCommandIndex(fleeCommand, availableCommands),
|
||||
"Combat hopeless, desperate flee"};
|
||||
"Last round combat not certain"};
|
||||
}
|
||||
|
||||
// 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) {
|
||||
std::cout << "AI FinalRound: Flee=" << fleeSuccessChance << "%, Combat=";
|
||||
std::cout << "AI FinalRound: Combat effectively certain (";
|
||||
WritePercent(std::cout, combatWinChance * 100);
|
||||
std::cout << "%, Threshold=";
|
||||
WritePercent(std::cout, adjustedCombatThreshold * 100);
|
||||
std::cout << "% -> ";
|
||||
}
|
||||
|
||||
if (fleeChance > adjustedCombatThreshold) {
|
||||
if (enableDebugLogging) { std::cout << "FLEE (better odds)\n"; }
|
||||
return FleeDecision{
|
||||
true,
|
||||
GetFleeCommandIndex(fleeCommand, availableCommands),
|
||||
"Flee has better expected outcome"};
|
||||
} else {
|
||||
if (enableDebugLogging) { std::cout << "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"};
|
||||
std::cout << "%), fighting\n";
|
||||
}
|
||||
return FleeDecision{
|
||||
false,
|
||||
0, // Will be replaced by StandardChooseCommandIndex
|
||||
"Combat effectively certain"};
|
||||
}
|
||||
|
||||
auto AIFleeDecisionCalculator::ShouldConsiderFleeing(
|
||||
|
||||
@@ -5,15 +5,34 @@
|
||||
#include "AIMinimumDistanceAndTarget.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
auto CoordsIndex(const Coords &coords, const int columnCount) -> int {
|
||||
return coords.row() * columnCount + coords.column();
|
||||
}
|
||||
|
||||
auto CoordsFromIndex(const size_t index, const int columnCount) -> Coords {
|
||||
return Coords(
|
||||
static_cast<int8_t>(index / static_cast<size_t>(columnCount)),
|
||||
static_cast<int8_t>(index % static_cast<size_t>(columnCount)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto MinimumDistanceAndTarget(
|
||||
const ActionPointDistances *apd,
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations) -> CoordsAndDistance {
|
||||
CoordsAndDistance min{Coords(-1, -1), ActionPointDistances::IMPOSSIBLE};
|
||||
for (const auto &dest : destinations) {
|
||||
const DIST_T dist = apd->Distance(origin, dest);
|
||||
const int columnCount = destinations.ColumnCount();
|
||||
const int originIndex = CoordsIndex(origin, columnCount);
|
||||
|
||||
for (const auto destIndex : destinations.indexIterator()) {
|
||||
const DIST_T dist = apd->Distance(originIndex, static_cast<int>(destIndex));
|
||||
if (dist == ActionPointDistances::IMPOSSIBLE) continue;
|
||||
if (dist < min.distance) min = CoordsAndDistance{dest, dist};
|
||||
if (dist < min.distance) {
|
||||
min = CoordsAndDistance{CoordsFromIndex(destIndex, columnCount), dist};
|
||||
}
|
||||
}
|
||||
|
||||
return min;
|
||||
@@ -25,4 +44,4 @@ auto MinimumDistance(
|
||||
const CoordsSet &destinations) -> int {
|
||||
return MinimumDistanceAndTarget(apd, origin, destinations).distance;
|
||||
}
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
@@ -18,12 +18,12 @@ struct CoordsAndDistance {
|
||||
int distance;
|
||||
};
|
||||
|
||||
auto MinimumDistanceAndTarget(
|
||||
[[nodiscard]] auto MinimumDistanceAndTarget(
|
||||
const ActionPointDistances *apd,
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations) -> CoordsAndDistance;
|
||||
|
||||
auto MinimumDistance(
|
||||
[[nodiscard]] auto MinimumDistance(
|
||||
const ActionPointDistances *apd,
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations) -> int;
|
||||
|
||||
@@ -20,12 +20,13 @@ using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
using PlayerInfo = net::eagle0::shardok::storage::fb::PlayerInfo;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
auto HasAttachedHeroWithProfession(
|
||||
[[nodiscard]] 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 *;
|
||||
[[nodiscard]] auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int;
|
||||
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &gameState, PlayerId pid)
|
||||
-> const PlayerInfo *;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -26,11 +26,13 @@ struct AIStrategy {
|
||||
};
|
||||
|
||||
extern AIStrategy FleeStrategy;
|
||||
auto AttackUnitsStrategy(const vector<TargetPriorityList>& targetPriorities) -> AIStrategy;
|
||||
auto AttackCastlesStrategy(const vector<TargetPriorityList>& targetPriorities) -> AIStrategy;
|
||||
[[nodiscard]] auto AttackUnitsStrategy(const vector<TargetPriorityList>& targetPriorities)
|
||||
-> AIStrategy;
|
||||
[[nodiscard]] auto AttackCastlesStrategy(const vector<TargetPriorityList>& targetPriorities)
|
||||
-> AIStrategy;
|
||||
extern AIStrategy HoldCastlesStrategy;
|
||||
extern AIStrategy ScatterStrategy;
|
||||
auto CrossRiversStrategy(const CoordsSet& targetLocations) -> AIStrategy;
|
||||
[[nodiscard]] auto CrossRiversStrategy(const CoordsSet& targetLocations) -> AIStrategy;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -25,7 +25,12 @@ class AIEvaluationCounter {
|
||||
public:
|
||||
AIEvaluationCounter();
|
||||
~AIEvaluationCounter();
|
||||
static int GetCurrentCount();
|
||||
AIEvaluationCounter(const AIEvaluationCounter&) = delete;
|
||||
auto operator=(const AIEvaluationCounter&) -> AIEvaluationCounter& = delete;
|
||||
AIEvaluationCounter(AIEvaluationCounter&&) = delete;
|
||||
auto operator=(AIEvaluationCounter&&) -> AIEvaluationCounter& = delete;
|
||||
|
||||
[[nodiscard]] static int GetCurrentCount();
|
||||
};
|
||||
|
||||
// Configuration structure for iterative deepening time budget
|
||||
@@ -39,10 +44,10 @@ struct AITimeBudget {
|
||||
// Time budget is calculated dynamically based on number of available commands:
|
||||
// budget = msPerCommand × numCommands (clamped to 200-5000ms)
|
||||
// If isAllAiBattle is true, uses allAiBattleTimeBudgetMaximum instead of the normal maximum.
|
||||
auto CalculateTimeBudget(
|
||||
[[nodiscard]] auto CalculateTimeBudget(
|
||||
PlayerId playerId,
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &state,
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
size_t numCommands,
|
||||
bool isAllAiBattle) -> AITimeBudget;
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using std::begin;
|
||||
using std::end;
|
||||
using std::shared_ptr;
|
||||
|
||||
constexpr double kProfessionValue = 200;
|
||||
constexpr double kVigorScoreMultiplier = 5.0;
|
||||
constexpr double kCastleMultiplierBonus = 1.0;
|
||||
@@ -92,8 +88,8 @@ auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue {
|
||||
break;
|
||||
}
|
||||
|
||||
const double battalionValue = battalionTypeMultiplier * (1.0 + armament / 100.0) *
|
||||
(1.0 + training / 100.0) * (0.5 + morale / 100.0) *
|
||||
const double battalionValue = battalionTypeMultiplier * (1.0 + (armament / 100.0)) *
|
||||
(1.0 + (training / 100.0)) * (0.5 + (morale / 100.0)) *
|
||||
unit->battalion().size();
|
||||
|
||||
const double heroValue =
|
||||
@@ -124,15 +120,16 @@ auto archeryValue(const vector<const Unit *> &enemyUnits, const bool canShootNow
|
||||
|
||||
auto reduceValue(const Unit *unit, const Terrain *unitTerrain) -> double {
|
||||
// TODO: make this depend on value of reducing castle too
|
||||
if (unit->fortified() || unitTerrain->modifier().castle().present())
|
||||
if (unit->fortified() || unitTerrain->modifier().castle().present()) {
|
||||
return kReduceFortifiedPossibleValue;
|
||||
else if (AllowsFortification(unitTerrain))
|
||||
return kReduceUnfortifiedPossibleValue;
|
||||
else
|
||||
return 0.0;
|
||||
}
|
||||
if (AllowsFortification(unitTerrain)) { return kReduceUnfortifiedPossibleValue; }
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
auto fearValue(const vector<const Unit *> &) -> double { return kFearPossibleValue; }
|
||||
auto fearValue([[maybe_unused]] const vector<const Unit *> &enemyUnits) -> double {
|
||||
return kFearPossibleValue;
|
||||
}
|
||||
|
||||
auto lightningValue(const Unit *unit) -> double {
|
||||
// TODO: make this depend on the value of the targets
|
||||
@@ -239,9 +236,8 @@ auto meteorValue(
|
||||
multiplier = kMeteorNoneRoundMultiplier * vigorFactor;
|
||||
}
|
||||
return maxValue * multiplier;
|
||||
} else {
|
||||
return 0.0;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
case net::eagle0::shardok::storage::fb::MultiroundMagicState_TARGET:
|
||||
@@ -251,9 +247,8 @@ auto meteorValue(
|
||||
map,
|
||||
enemyOccupants,
|
||||
friendlyOccupants);
|
||||
} else {
|
||||
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
|
||||
}
|
||||
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
|
||||
case net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST:
|
||||
return kMeteorCastRoundMultiplier *
|
||||
meteorDropRawValue(
|
||||
@@ -280,7 +275,7 @@ auto GetRangedAttackBonus(
|
||||
const int meteorRange,
|
||||
const double minVigorToCast,
|
||||
const ActionPoints archeryActionPointCost) -> double {
|
||||
vector<double> rangedAttackValues{};
|
||||
double bestRangedAttackValue = 0.0;
|
||||
|
||||
const auto &enemyUnits = isAttacker ? defenderUnits : attackerUnits;
|
||||
const auto &friendlyUnits = isAttacker ? attackerUnits : defenderUnits;
|
||||
@@ -296,7 +291,7 @@ auto GetRangedAttackBonus(
|
||||
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE)) {
|
||||
if (unit->attached_hero().profession_info().meteor_cast_state() ==
|
||||
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE) {
|
||||
rangedAttackValues.push_back(lightningValue(unit));
|
||||
bestRangedAttackValue = std::max(bestRangedAttackValue, lightningValue(unit));
|
||||
} else {
|
||||
const double mv = meteorValue(
|
||||
unit,
|
||||
@@ -306,14 +301,14 @@ auto GetRangedAttackBonus(
|
||||
friendlyUnits,
|
||||
meteorRange,
|
||||
minVigorToCast);
|
||||
rangedAttackValues.push_back(mv);
|
||||
bestRangedAttackValue = std::max(bestRangedAttackValue, mv);
|
||||
}
|
||||
}
|
||||
|
||||
if (attackLocations.EngineerLocations().Contains(unitLocation) && unit->has_attached_hero() &&
|
||||
unit->attached_hero().profession_info().profession() ==
|
||||
net::eagle0::shardok::storage::fb::Profession_ENGINEER) {
|
||||
rangedAttackValues.push_back(reduceValue(unit, terrain));
|
||||
bestRangedAttackValue = std::max(bestRangedAttackValue, reduceValue(unit, terrain));
|
||||
}
|
||||
|
||||
if (attackLocations.NecromancerLocations().Contains(unitLocation) &&
|
||||
@@ -321,23 +316,24 @@ auto GetRangedAttackBonus(
|
||||
unit->attached_hero().profession_info().profession() ==
|
||||
net::eagle0::shardok::storage::fb::Profession_NECROMANCER &&
|
||||
unit->attached_hero().control_info().controlled_unit_id() != -1) {
|
||||
rangedAttackValues.push_back(fearValue(enemyUnits));
|
||||
bestRangedAttackValue = std::max(bestRangedAttackValue, fearValue(enemyUnits));
|
||||
}
|
||||
|
||||
if (attackLocations.ArcheryLocations().Contains(unitLocation) &&
|
||||
unit->volleys_remaining() > 0) {
|
||||
rangedAttackValues.push_back(archeryValue(enemyUnits, canShootNow));
|
||||
bestRangedAttackValue =
|
||||
std::max(bestRangedAttackValue, archeryValue(enemyUnits, canShootNow));
|
||||
}
|
||||
|
||||
// Longbowmen in castle can archer adjacent
|
||||
if (attackLocations.AdjacentLocations().Contains(unitLocation) &&
|
||||
unit->volleys_remaining() > 0 && terrain->modifier().castle().present() &&
|
||||
unit->battalion().type() == net::eagle0::shardok::storage::fb::BattalionTypeId_LONGBOWMEN) {
|
||||
rangedAttackValues.push_back(archeryValue(enemyUnits, canShootNow));
|
||||
bestRangedAttackValue =
|
||||
std::max(bestRangedAttackValue, archeryValue(enemyUnits, canShootNow));
|
||||
}
|
||||
|
||||
if (rangedAttackValues.empty()) return 0.0;
|
||||
return *std::max_element(begin(rangedAttackValues), end(rangedAttackValues));
|
||||
return bestRangedAttackValue;
|
||||
}
|
||||
|
||||
auto UnitValue(
|
||||
|
||||
@@ -17,9 +17,9 @@ using net::eagle0::shardok::storage::fb::Unit;
|
||||
using ScoreValue = double;
|
||||
class AttackLocations;
|
||||
|
||||
auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue;
|
||||
[[nodiscard]] auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue;
|
||||
|
||||
auto GetRangedAttackBonus(
|
||||
[[nodiscard]] auto GetRangedAttackBonus(
|
||||
const Unit *unit,
|
||||
bool isAttacker,
|
||||
const HexMap *map,
|
||||
@@ -32,7 +32,7 @@ auto GetRangedAttackBonus(
|
||||
double minVigorToCast,
|
||||
ActionPoints archeryActionPointCost = 0) -> double;
|
||||
|
||||
auto UnitValue(
|
||||
[[nodiscard]] auto UnitValue(
|
||||
const Unit *unit,
|
||||
bool isAttacker,
|
||||
const std::vector<const Unit *> &attackerUnits,
|
||||
|
||||
@@ -18,7 +18,7 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
|
||||
// Put out all the fires, except on bridges
|
||||
fb::HexMapW mapCopy = fb::CopyHexMap(gameState->hex_map());
|
||||
for (uint32_t index = 0; index < mapCopy->terrain()->size(); index++) {
|
||||
for (uint32_t index = 0; index < mapCopy->terrain()->size(); ++index) {
|
||||
if (IsWater(mapCopy->terrain()->Get(index)->type())) continue;
|
||||
// const_cast is safe because we own the mutable buffer (mapCopy)
|
||||
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
|
||||
@@ -155,7 +155,7 @@ auto WaterCrossingTiles(
|
||||
// 1) the tile is water
|
||||
// 2) the tile does not already have a bridge (that is not on fire)
|
||||
// 3) the tile is not already frozen
|
||||
for (int index = 0; index < mapSize; index++) {
|
||||
for (int index = 0; index < mapSize; ++index) {
|
||||
const auto startTerrain = *hexMap->terrain()->Get(index);
|
||||
|
||||
if (!IsWater(startTerrain.type())) continue;
|
||||
@@ -169,6 +169,7 @@ auto WaterCrossingTiles(
|
||||
auto *terr = const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
|
||||
mapCopy->mutable_terrain()->GetMutableObject(index));
|
||||
terr->mutable_modifier().mutable_bridge().mutate_present(true);
|
||||
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
|
||||
terr->mutable_modifier().mutable_fire().mutate_present(false);
|
||||
|
||||
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
|
||||
|
||||
@@ -31,7 +31,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(
|
||||
[[nodiscard]] auto UnitIdsRequiringWaterCrossing(
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const CoordsSet& destinations,
|
||||
@@ -39,14 +39,14 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
|
||||
|
||||
// Units belonging to the player that are capable of creating water crossings
|
||||
auto UnitIdsToCreateWaterCrossing(
|
||||
[[nodiscard]] auto UnitIdsToCreateWaterCrossing(
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
|
||||
|
||||
// Whether a unit of the given type can reach destination from origin, given the current state
|
||||
// of the map
|
||||
auto CanReach(
|
||||
[[nodiscard]] auto CanReach(
|
||||
const Coords& origin,
|
||||
const Coords& destination,
|
||||
const HexMap* hexMap,
|
||||
@@ -56,11 +56,12 @@ auto CanReach(
|
||||
|
||||
// Returns the *non*-water tiles from which you could bridge/freeze water to allow a unit to cross
|
||||
// from origin to destination
|
||||
auto CrossingStartLocations(const HexMap* hexMap, const CoordsSet& waterCrossingTiles) -> CoordsSet;
|
||||
[[nodiscard]] auto CrossingStartLocations(const HexMap* hexMap, const CoordsSet& waterCrossingTiles)
|
||||
-> CoordsSet;
|
||||
|
||||
// Returns the water tiles that, if they were bridged/frozen, would allow a unit to cross from
|
||||
// origin to destination
|
||||
auto WaterCrossingTiles(
|
||||
[[nodiscard]] auto WaterCrossingTiles(
|
||||
const Coords& origin,
|
||||
const Coords& destination,
|
||||
const HexMap* hexMap,
|
||||
@@ -68,7 +69,7 @@ auto WaterCrossingTiles(
|
||||
const BattalionTypeSPtr& battalionType) -> CoordsSet;
|
||||
|
||||
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
|
||||
auto IntendedCrossingStarts(
|
||||
[[nodiscard]] auto IntendedCrossingStarts(
|
||||
const GameStateW& gameState,
|
||||
const vector<UnitId>& unitIdsCreatingCrossing,
|
||||
const CoordsSet& tilesToStartCrossingFrom,
|
||||
@@ -76,7 +77,7 @@ auto IntendedCrossingStarts(
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> CoordsSet;
|
||||
|
||||
// Calculate score based on water crossing strategy
|
||||
auto WaterCrossingScore(
|
||||
[[nodiscard]] auto WaterCrossingScore(
|
||||
PlayerId playerId,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
const GameStateW& gameState,
|
||||
|
||||
@@ -51,7 +51,7 @@ IterativeDeepeningAI::IterativeDeepeningAI(
|
||||
castleCoords(castleCoords),
|
||||
scorer(scorer),
|
||||
apdCache(apdCache),
|
||||
battalionTypeGetter(std::move(battalionTypeGetter)) {} // Move the function object
|
||||
battalionTypeGetter(std::move(battalionTypeGetter)) {}
|
||||
|
||||
auto IterativeDeepeningAI::IterativeSearch(
|
||||
const GameSettingsSPtr& settings,
|
||||
|
||||
@@ -79,7 +79,7 @@ private:
|
||||
CoordsSet castleCoords;
|
||||
const AIScoreCalculator& scorer;
|
||||
const APDCache& apdCache;
|
||||
BattalionTypeGetter battalionTypeGetter; // Store by value, not reference!
|
||||
BattalionTypeGetter battalionTypeGetter;
|
||||
|
||||
// Reusable vectors to reduce memory allocations
|
||||
mutable std::vector<std::vector<ScoreValue>> scoresByDepth;
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
#include <sstream>
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -182,7 +185,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
<< " round=" << guessedState->current_round() << "\n";
|
||||
|
||||
const auto maxDump = std::max(commandCount, realAvailableCommands->size());
|
||||
for (size_t i = 0; i < maxDump; i++) {
|
||||
for (size_t i = 0; i < maxDump; ++i) {
|
||||
const auto realName = i < realAvailableCommands->size()
|
||||
? net::eagle0::shardok::common::CommandType_Name(
|
||||
(*realAvailableCommands)[i]->GetCommandType())
|
||||
@@ -214,13 +217,13 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
// Group by unit to find the missing position per unit
|
||||
std::unordered_map<int, std::set<std::pair<int, int>>> realPositions;
|
||||
std::unordered_map<int, std::set<std::pair<int, int>>> guessedPositions;
|
||||
for (size_t i = 0; i < realAvailableCommands->size(); i++) {
|
||||
for (size_t i = 0; i < realAvailableCommands->size(); ++i) {
|
||||
const auto &cmd = (*realAvailableCommands)[i];
|
||||
realPositions[cmd->GetActorUnitId()].emplace(
|
||||
cmd->GetTargetRow(),
|
||||
cmd->GetTargetColumn());
|
||||
}
|
||||
for (size_t i = 0; i < commandCount; i++) {
|
||||
for (size_t i = 0; i < commandCount; ++i) {
|
||||
const auto &cmd = (*guessedCommands)[i];
|
||||
guessedPositions[cmd->GetActorUnitId()].emplace(
|
||||
cmd->GetTargetRow(),
|
||||
@@ -250,7 +253,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
|
||||
// Dump all guessed state units for full picture
|
||||
std::cerr << " Guessed state units (" << guessedState->units()->size() << " total):\n";
|
||||
for (size_t i = 0; i < guessedState->units()->size(); i++) {
|
||||
for (size_t i = 0; i < guessedState->units()->size(); ++i) {
|
||||
const auto *u = guessedState->units()->Get(static_cast<unsigned int>(i));
|
||||
std::cerr << " unit_id=" << u->unit_id() << " player=" << u->player_id()
|
||||
<< " status=" << static_cast<int>(u->status()) << " loc=("
|
||||
@@ -263,7 +266,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
"AI command count mismatch: guessed=" + std::to_string(commandCount) +
|
||||
" real=" + std::to_string(realAvailableCommands->size()));
|
||||
}
|
||||
for (size_t i = 0; i < commandCount; i++) {
|
||||
for (size_t i = 0; i < commandCount; ++i) {
|
||||
CheckCommand((*realAvailableCommands)[i], (*guessedCommands)[i]);
|
||||
}
|
||||
|
||||
@@ -465,8 +468,10 @@ auto ShardokAIClient::ChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateView &gsv,
|
||||
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
|
||||
static int typeChosenCount[net::eagle0::shardok::common::CommandType_MAX + 1];
|
||||
static int totalChoices = 0;
|
||||
static std::array<std::atomic<int>, net::eagle0::shardok::common::CommandType_MAX + 1>
|
||||
typeChosenCount;
|
||||
static std::atomic<int> totalChoices = 0;
|
||||
static std::mutex reportMutex;
|
||||
|
||||
CommandChoiceResults results{};
|
||||
|
||||
@@ -484,15 +489,18 @@ auto ShardokAIClient::ChooseCommandIndex(
|
||||
}
|
||||
|
||||
const auto chosenType = (*realAvailableCommands)[results.chosenIndex]->GetCommandType();
|
||||
typeChosenCount[static_cast<int>(chosenType)]++;
|
||||
totalChoices++;
|
||||
typeChosenCount[static_cast<size_t>(chosenType)].fetch_add(1, std::memory_order_relaxed);
|
||||
const int updatedTotal = totalChoices.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
|
||||
if (totalChoices % 1000 == 0) {
|
||||
if (updatedTotal % 1000 == 0) {
|
||||
std::lock_guard lock(reportMutex);
|
||||
std::cout << "TYPES CHOSEN:\n";
|
||||
vector<std::pair<int, CommandType>> choices{};
|
||||
for (int i = 0; i <= net::eagle0::shardok::common::CommandType_MAX; i++) {
|
||||
if (typeChosenCount[i] != 0) {
|
||||
choices.emplace_back(typeChosenCount[i], static_cast<CommandType>(i));
|
||||
for (int i = 0; i <= net::eagle0::shardok::common::CommandType_MAX; ++i) {
|
||||
if (const int count =
|
||||
typeChosenCount[static_cast<size_t>(i)].load(std::memory_order_relaxed);
|
||||
count != 0) {
|
||||
choices.emplace_back(count, static_cast<CommandType>(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ TranspositionTable::TranspositionTable() : table(TABLE_SIZE) {
|
||||
clear();
|
||||
}
|
||||
|
||||
uint64_t TranspositionTable::hashGameState(const GameStateW& state) const {
|
||||
auto TranspositionTable::hashGameState(const GameStateW& state) const -> uint64_t {
|
||||
// The FlatBuffer is contiguous in memory and units are sorted by ID,
|
||||
// so we can just hash the raw bytes for order-independent hashing
|
||||
// Use ComputeFNV1aHash to avoid creating a string copy
|
||||
@@ -38,8 +38,10 @@ TranspositionTable::probe(const GameStateW& state, int depth, PlayerId player) {
|
||||
uint64_t stored_hash = entry.hash_full.load(std::memory_order_relaxed);
|
||||
uint8_t stored_depth = entry.depth.load(std::memory_order_relaxed);
|
||||
uint8_t stored_player = entry.player_id.load(std::memory_order_relaxed);
|
||||
uint16_t stored_age = entry.age.load(std::memory_order_relaxed);
|
||||
|
||||
if (stored_hash == hash && stored_depth >= depth && stored_player == player) {
|
||||
if (stored_hash == hash && stored_depth >= depth && stored_player == player &&
|
||||
stored_age == current_age.load(std::memory_order_relaxed)) {
|
||||
stats.hits++;
|
||||
float score = entry.score.load(std::memory_order_relaxed);
|
||||
return static_cast<ScoreValue>(score);
|
||||
|
||||
@@ -28,8 +28,8 @@ public:
|
||||
std::atomic<uint64_t> stores{0};
|
||||
std::atomic<uint64_t> collisions{0};
|
||||
|
||||
double hitRate() const {
|
||||
uint64_t p = probes.load();
|
||||
[[nodiscard]] auto hitRate() const -> double {
|
||||
const uint64_t p = probes.load();
|
||||
return p > 0 ? (100.0 * hits.load() / p) : 0.0;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ private:
|
||||
std::atomic<uint16_t> current_age{0};
|
||||
|
||||
// Hash function for FlatBuffer game state
|
||||
uint64_t hashGameState(const GameStateW& state) const;
|
||||
[[nodiscard]] auto hashGameState(const GameStateW& state) const -> uint64_t;
|
||||
|
||||
public:
|
||||
TranspositionTable();
|
||||
@@ -78,7 +78,7 @@ public:
|
||||
void incrementAge() { current_age++; }
|
||||
|
||||
// Get statistics
|
||||
const Stats& getStats() const { return stats; }
|
||||
[[nodiscard]] auto getStats() const -> const Stats& { return stats; }
|
||||
|
||||
// Print statistics to stdout
|
||||
void printStats() const;
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
[[nodiscard]] PlayerId getPlayer() const { return player_; }
|
||||
[[nodiscard]] int getActorId() const { return actorId_; }
|
||||
[[nodiscard]] std::pair<int, int> getTarget() const { return {targetRow_, targetCol_}; }
|
||||
[[nodiscard]] bool hasOdds() const { return hasOdds_; }
|
||||
|
||||
private:
|
||||
// Store only essential fields (~25 bytes, all POD, cache-friendly)
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
|
||||
#include "ShardokAction.hpp"
|
||||
@@ -32,9 +34,47 @@ auto IsDefenderPlayer(const GameStateW& gameState, const PlayerId playerId) -> b
|
||||
return false;
|
||||
}
|
||||
|
||||
auto LegalActionsCacheKey(const uint64_t stateHash, const bool rootIsDefender) -> uint64_t {
|
||||
auto MixCacheKey(uint64_t seed, const uint64_t value) -> uint64_t {
|
||||
constexpr uint64_t kHashCombineConstant = 0x9e3779b97f4a7c15ULL;
|
||||
seed ^= value + kHashCombineConstant + (seed << 6) + (seed >> 2);
|
||||
return seed;
|
||||
}
|
||||
|
||||
auto PointerIdentityHash(const void* ptr) -> uint64_t {
|
||||
return static_cast<uint64_t>(reinterpret_cast<std::uintptr_t>(ptr));
|
||||
}
|
||||
|
||||
auto LegalActionsCacheKey(
|
||||
const uint64_t stateHash,
|
||||
const bool rootIsDefender,
|
||||
const GameSettings* gameSettings,
|
||||
const APDCache* apdCache) -> uint64_t {
|
||||
constexpr uint64_t kDefenderRoleHash = 0x9e3779b97f4a7c15ULL;
|
||||
return rootIsDefender ? stateHash ^ kDefenderRoleHash : stateHash;
|
||||
uint64_t key = rootIsDefender ? stateHash ^ kDefenderRoleHash : stateHash;
|
||||
|
||||
// The cached action set is the output of AICommandFilter, not just the raw command list.
|
||||
// Include the context objects that can affect filtering so a process-wide cache entry from
|
||||
// one search cannot be reused by a different settings/APD context with the same state hash.
|
||||
key = MixCacheKey(key, PointerIdentityHash(gameSettings));
|
||||
key = MixCacheKey(key, PointerIdentityHash(apdCache != nullptr ? apdCache->get() : nullptr));
|
||||
return key;
|
||||
}
|
||||
|
||||
auto SortActionsByWeight(
|
||||
std::vector<std::unique_ptr<MCTSAction>> actions,
|
||||
const std::vector<double>& weights) -> std::vector<std::unique_ptr<MCTSAction>> {
|
||||
std::vector<size_t> sortedIndices(actions.size());
|
||||
std::iota(sortedIndices.begin(), sortedIndices.end(), 0);
|
||||
|
||||
std::ranges::sort(sortedIndices, [&weights](size_t a, size_t b) {
|
||||
return weights[a] > weights[b];
|
||||
});
|
||||
|
||||
std::vector<std::unique_ptr<MCTSAction>> sortedActions;
|
||||
sortedActions.reserve(actions.size());
|
||||
for (size_t idx : sortedIndices) { sortedActions.push_back(std::move(actions[idx])); }
|
||||
|
||||
return sortedActions;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -55,6 +95,10 @@ std::atomic<uint64_t> ShardokGameEngine::cacheMisses_{0};
|
||||
std::atomic<uint64_t> ShardokGameEngine::timeInHashComputation_{0};
|
||||
std::atomic<uint64_t> ShardokGameEngine::timeInLegalActionsComputation_{0};
|
||||
|
||||
namespace {
|
||||
std::once_flag legalActionsCacheReserveOnce;
|
||||
} // namespace
|
||||
|
||||
ShardokGameEngine::ShardokGameEngine(
|
||||
[[maybe_unused]] const ShardokEngine* engine,
|
||||
const AIScoreCalculator* scoreCalculator,
|
||||
@@ -75,7 +119,9 @@ ShardokGameEngine::ShardokGameEngine(
|
||||
criticalTileCoords_(criticalTileCoords) {
|
||||
// Thread-local cache is automatically initialized per thread
|
||||
// Reserve space to reduce rehashing (based on profiling: ~30-50K unique states per search)
|
||||
legalActionsCache_.reserve(100000);
|
||||
std::call_once(legalActionsCacheReserveOnce, [] {
|
||||
ShardokGameEngine::legalActionsCache_.reserve(100000);
|
||||
});
|
||||
}
|
||||
|
||||
std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
|
||||
@@ -89,27 +135,15 @@ std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
|
||||
|
||||
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
|
||||
|
||||
// Use cached engine if available (avoids recomputing GetAvailableCommands for same state)
|
||||
std::shared_ptr<ShardokEngine> engine;
|
||||
if (auto cachedEngine = shardokState->getCachedEngine()) {
|
||||
// Clone the cached engine to preserve command cache
|
||||
engine = std::make_shared<ShardokEngine>(*cachedEngine);
|
||||
} else {
|
||||
// Create fresh engine and populate command cache
|
||||
engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_,
|
||||
0,
|
||||
false);
|
||||
// Populate command cache (result intentionally unused, just populating cache)
|
||||
[[maybe_unused]] const auto commands =
|
||||
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
// Cache the engine for future use with this state
|
||||
shardokState->setCachedEngine(engine);
|
||||
// Clone it for applying the action (don't mutate the cached engine)
|
||||
engine = std::make_shared<ShardokEngine>(*engine);
|
||||
}
|
||||
auto engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_,
|
||||
0,
|
||||
false);
|
||||
// Populate the AI command list before posting so the MCTS action index maps to the
|
||||
// no-follow-up command ordering used by getLegalActions().
|
||||
[[maybe_unused]] const auto commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
|
||||
// Create deterministic random generator if a specific roll is requested
|
||||
// deterministicRoll of -1.0 (default) means use random generator
|
||||
@@ -170,11 +204,6 @@ std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
|
||||
*alCache_,
|
||||
criticalTileCoords_);
|
||||
|
||||
// Cache the engine on the new state so score() can use it for END_TURN normalization
|
||||
// The engine's command list may be stale after the action was applied, but that's OK -
|
||||
// we'll refresh it when we call GetAvailableCommandsForAIPlayer() in score()
|
||||
newState->setCachedEngine(engine);
|
||||
|
||||
// Don't pre-compute hash - let it be computed lazily on first use
|
||||
// Many states (especially in simulation) never need their hash computed
|
||||
return newState;
|
||||
@@ -194,28 +223,18 @@ void ShardokGameEngine::applyActionMutable(
|
||||
|
||||
const auto currentPlayer = static_cast<PlayerId>(state->currentPlayerId());
|
||||
|
||||
// Use cached engine if available
|
||||
std::shared_ptr<ShardokEngine> engine;
|
||||
if (auto cachedEngine = shardokState->getCachedEngine()) {
|
||||
engine = std::make_shared<ShardokEngine>(*cachedEngine);
|
||||
} else {
|
||||
engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_,
|
||||
0,
|
||||
false);
|
||||
// Populate command cache (result intentionally unused, just populating cache)
|
||||
[[maybe_unused]] const auto commands =
|
||||
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
shardokState->setCachedEngine(engine);
|
||||
engine = std::make_shared<ShardokEngine>(*engine);
|
||||
}
|
||||
auto engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_,
|
||||
0,
|
||||
false);
|
||||
// Populate the AI command list before posting so the MCTS action index maps to the
|
||||
// no-follow-up command ordering used by getLegalActions().
|
||||
[[maybe_unused]] const auto commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
|
||||
engine->PostCommand(currentPlayer, shardokAction->getIndex(), nullptr);
|
||||
shardokState->getMutableShardokState() = engine->GetCurrentGameState();
|
||||
// Clear the cached engine and hash since the state has been mutated
|
||||
shardokState->setCachedEngine(nullptr);
|
||||
shardokState->invalidateHashCache();
|
||||
}
|
||||
|
||||
@@ -241,7 +260,8 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
|
||||
// Time hash computation
|
||||
const auto hashStart = std::chrono::high_resolution_clock::now();
|
||||
const uint64_t stateHash = shardokState->hash();
|
||||
const uint64_t cacheKey = LegalActionsCacheKey(stateHash, isDefender_);
|
||||
const uint64_t cacheKey =
|
||||
LegalActionsCacheKey(stateHash, isDefender_, gameSettings_.get(), apdCache_);
|
||||
const auto hashEnd = std::chrono::high_resolution_clock::now();
|
||||
timeInHashComputation_.fetch_add(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(hashEnd - hashStart).count(),
|
||||
@@ -249,52 +269,49 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
|
||||
|
||||
// Check transposition table for cached legal actions
|
||||
if (auto it = legalActionsCache_.find(cacheKey); it != legalActionsCache_.end()) {
|
||||
cacheHits_.fetch_add(1, std::memory_order_relaxed);
|
||||
const auto engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_);
|
||||
|
||||
// Use cached engine
|
||||
shardokState->setCachedEngine(it->second.engine);
|
||||
const CommandListSPtr commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
|
||||
// Get commands from the cached engine (Engine already caches these internally)
|
||||
const CommandListSPtr commands =
|
||||
it->second.engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
|
||||
if (!commands || commands->empty()) { return {}; }
|
||||
|
||||
// Convert to MCTSActions using stored filtered indices
|
||||
std::vector<std::unique_ptr<MCTSAction>> actions;
|
||||
actions.reserve(it->second.filteredIndices.size());
|
||||
|
||||
for (const size_t origIdx : it->second.filteredIndices) {
|
||||
if (origIdx < commands->size()) {
|
||||
const auto& cmd = commands->at(origIdx);
|
||||
|
||||
// Extract essential fields directly from command (no proto conversion!)
|
||||
actions.push_back(std::make_unique<ShardokAction>(
|
||||
origIdx,
|
||||
cmd->GetCommandType(),
|
||||
cmd->GetPlayerId(),
|
||||
cmd->GetActorUnitId(),
|
||||
cmd->GetTargetRow(),
|
||||
cmd->GetTargetColumn(),
|
||||
cmd->HasOdds()));
|
||||
bool cacheEntryMatches = commands != nullptr && commands->size() == it->second.commandCount;
|
||||
for (const auto& cachedAction : it->second.actions) {
|
||||
const size_t commandIndex = cachedAction.getIndex();
|
||||
if (!cacheEntryMatches || commandIndex >= commands->size()) {
|
||||
cacheEntryMatches = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& command = commands->at(commandIndex);
|
||||
const auto [targetRow, targetColumn] = cachedAction.getTarget();
|
||||
cacheEntryMatches = command->GetCommandType() ==
|
||||
static_cast<net::eagle0::shardok::common::CommandType>(
|
||||
cachedAction.getType()) &&
|
||||
command->GetPlayerId() == cachedAction.getPlayer() &&
|
||||
command->GetActorUnitId() == cachedAction.getActorId() &&
|
||||
command->GetTargetRow() == targetRow &&
|
||||
command->GetTargetColumn() == targetColumn &&
|
||||
command->HasOdds() == cachedAction.hasOdds();
|
||||
if (!cacheEntryMatches) { break; }
|
||||
}
|
||||
|
||||
// Sort actions by weight (descending) to ensure MCTS explores high-value actions first
|
||||
const std::vector<double> weights = getActionWeights(actions, state);
|
||||
if (cacheEntryMatches) {
|
||||
cacheHits_.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
std::vector<size_t> sortedIndices(actions.size());
|
||||
std::iota(sortedIndices.begin(), sortedIndices.end(), 0);
|
||||
// Convert immutable cached action snapshots to MCTS action instances.
|
||||
std::vector<std::unique_ptr<MCTSAction>> actions;
|
||||
actions.reserve(it->second.actions.size());
|
||||
|
||||
std::sort(sortedIndices.begin(), sortedIndices.end(), [&weights](size_t a, size_t b) {
|
||||
return weights[a] > weights[b];
|
||||
});
|
||||
for (const auto& cachedAction : it->second.actions) {
|
||||
actions.push_back(cachedAction.clone());
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<MCTSAction>> sortedActions;
|
||||
sortedActions.reserve(actions.size());
|
||||
for (size_t idx : sortedIndices) { sortedActions.push_back(std::move(actions[idx])); }
|
||||
|
||||
return sortedActions;
|
||||
// Sort actions by weight (descending) to ensure MCTS explores high-value actions first
|
||||
const std::vector<double> weights = getActionWeights(actions, state);
|
||||
return SortActionsByWeight(std::move(actions), weights);
|
||||
}
|
||||
}
|
||||
|
||||
cacheMisses_.fetch_add(1, std::memory_order_relaxed);
|
||||
@@ -302,17 +319,10 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
|
||||
// Time legal actions computation
|
||||
const auto actionsStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Use cached engine if available, otherwise create and cache it
|
||||
std::shared_ptr<ShardokEngine> engine;
|
||||
if (auto cachedEngine = shardokState->getCachedEngine()) {
|
||||
engine = cachedEngine;
|
||||
} else {
|
||||
engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_);
|
||||
shardokState->setCachedEngine(engine);
|
||||
}
|
||||
const auto engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_);
|
||||
|
||||
const CommandListSPtr commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
|
||||
@@ -333,20 +343,24 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
|
||||
// Convert only filtered commands to MCTSActions
|
||||
std::vector<std::unique_ptr<MCTSAction>> actions;
|
||||
actions.reserve(filteredIndices.size());
|
||||
std::vector<ShardokAction> cachedActions;
|
||||
cachedActions.reserve(filteredIndices.size());
|
||||
|
||||
for (const size_t idx : filteredIndices) {
|
||||
if (idx < commands->size()) {
|
||||
const auto& cmd = commands->at(idx);
|
||||
|
||||
// Extract essential fields directly from command (no proto conversion!)
|
||||
actions.push_back(std::make_unique<ShardokAction>(
|
||||
ShardokAction action(
|
||||
idx,
|
||||
cmd->GetCommandType(),
|
||||
cmd->GetPlayerId(),
|
||||
cmd->GetActorUnitId(),
|
||||
cmd->GetTargetRow(),
|
||||
cmd->GetTargetColumn(),
|
||||
cmd->HasOdds()));
|
||||
cmd->HasOdds());
|
||||
actions.push_back(action.clone());
|
||||
cachedActions.push_back(action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,20 +369,7 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
|
||||
// get explored deeply. Original indices are preserved in ShardokAction::getIndex()
|
||||
const std::vector<double> weights = getActionWeights(actions, state);
|
||||
|
||||
// Create index vector for sorting
|
||||
std::vector<size_t> sortedIndices(actions.size());
|
||||
std::iota(sortedIndices.begin(), sortedIndices.end(), 0);
|
||||
|
||||
// Sort indices by weight (descending)
|
||||
std::sort(sortedIndices.begin(), sortedIndices.end(), [&weights](size_t a, size_t b) {
|
||||
return weights[a] > weights[b];
|
||||
});
|
||||
|
||||
// Reorder actions according to sorted indices
|
||||
std::vector<std::unique_ptr<MCTSAction>> sortedActions;
|
||||
sortedActions.reserve(actions.size());
|
||||
for (size_t idx : sortedIndices) { sortedActions.push_back(std::move(actions[idx])); }
|
||||
actions = std::move(sortedActions);
|
||||
actions = SortActionsByWeight(std::move(actions), weights);
|
||||
|
||||
const auto actionsEnd = std::chrono::high_resolution_clock::now();
|
||||
timeInLegalActionsComputation_.fetch_add(
|
||||
@@ -376,20 +377,20 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
|
||||
.count(),
|
||||
std::memory_order_relaxed);
|
||||
|
||||
// Store in transposition table for future lookups
|
||||
// Note: We only store filtered indices and the engine (which caches commands internally)
|
||||
// This avoids duplicating heavy protocol buffer objects
|
||||
// Store immutable action snapshots in the transposition table for future lookups.
|
||||
// Do not store ShardokEngine here: its command cache is mutable and not safe to share
|
||||
// across MCTS worker threads.
|
||||
// Use lazy_emplace_l to ensure thread-safe insertion (locks the bucket during construction)
|
||||
legalActionsCache_.lazy_emplace_l(
|
||||
cacheKey,
|
||||
[&](typename decltype(legalActionsCache_)::value_type& v) {
|
||||
// Update existing entry
|
||||
v.second.filteredIndices = filteredIndices;
|
||||
v.second.engine = engine;
|
||||
v.second.commandCount = commands->size();
|
||||
v.second.actions = cachedActions;
|
||||
},
|
||||
[&](const typename decltype(legalActionsCache_)::constructor& ctor) {
|
||||
// Create new entry
|
||||
ctor(cacheKey, LegalActionsCache{filteredIndices, engine});
|
||||
ctor(cacheKey, LegalActionsCache{commands->size(), cachedActions});
|
||||
});
|
||||
|
||||
return actions;
|
||||
@@ -406,9 +407,8 @@ std::vector<size_t> ShardokGameEngine::filterActions(
|
||||
const MCTSGameState& /*state*/) const {
|
||||
// All filtering is already done in getLegalActions() using AICommandFilter
|
||||
// This method is used by simulation policies and doesn't need additional filtering
|
||||
std::vector<size_t> indices;
|
||||
indices.reserve(actions.size());
|
||||
for (size_t i = 0; i < actions.size(); ++i) { indices.push_back(i); }
|
||||
std::vector<size_t> indices(actions.size());
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
return indices;
|
||||
}
|
||||
|
||||
@@ -423,15 +423,7 @@ std::vector<double> ShardokGameEngine::getActionWeights(
|
||||
"indicates a type mismatch in the MCTS adapter layer");
|
||||
}
|
||||
|
||||
// Get cached engine and command list for looking up command protos
|
||||
auto cachedEngine = shardokState->getCachedEngine();
|
||||
if (!cachedEngine) {
|
||||
throw MCTSInternalError(
|
||||
"ShardokGameEngine::getActionWeights called with state that has no cached engine");
|
||||
}
|
||||
|
||||
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
|
||||
const CommandListSPtr commands = cachedEngine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
|
||||
// Determine if current player is defender (not root player!)
|
||||
// During simulation we need to use the correct perspective for action weighting
|
||||
@@ -450,20 +442,13 @@ std::vector<double> ShardokGameEngine::getActionWeights(
|
||||
"indicates a type mismatch in the MCTS adapter layer");
|
||||
}
|
||||
|
||||
// Look up command proto from cached engine using action's index
|
||||
const size_t cmdIndex = shardokAction->getIndex();
|
||||
if (cmdIndex >= commands->size()) {
|
||||
throw MCTSInternalError(
|
||||
"ShardokGameEngine::getActionWeights: action index out of bounds");
|
||||
}
|
||||
|
||||
const auto& cmd = commands->at(cmdIndex);
|
||||
const auto [targetRow, targetColumn] = shardokAction->getTarget();
|
||||
|
||||
weights.push_back(AIHeuristicWeighting::GetCommandWeight(
|
||||
cmd->GetCommandType(),
|
||||
cmd->GetActorUnitId(),
|
||||
cmd->GetPlayerId(),
|
||||
Coords{cmd->GetTargetRow(), cmd->GetTargetColumn()},
|
||||
static_cast<net::eagle0::shardok::common::CommandType>(shardokAction->getType()),
|
||||
shardokAction->getActorId(),
|
||||
shardokAction->getPlayer(),
|
||||
Coords{static_cast<int8_t>(targetRow), static_cast<int8_t>(targetColumn)},
|
||||
gameState,
|
||||
castleCoords_,
|
||||
apdCache_,
|
||||
@@ -497,7 +482,7 @@ bool ShardokGameEngine::shouldStopSearch(
|
||||
size_t ShardokGameEngine::mapFilteredIndexToOriginal(
|
||||
size_t filteredIndex,
|
||||
const MCTSGameState& state) const {
|
||||
// Get the filtered actions (uses cached engine)
|
||||
// Get the filtered actions.
|
||||
auto actions = getLegalActions(state, state.currentPlayerId(), 0, 0);
|
||||
|
||||
// Check bounds
|
||||
@@ -599,22 +584,12 @@ ChanceOutcomeInfo ShardokGameEngine::getBinaryOutcomeInfo(
|
||||
|
||||
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
|
||||
|
||||
// Get or create the engine for this state
|
||||
std::shared_ptr<ShardokEngine> engine;
|
||||
if (auto cachedEngine = shardokState->getCachedEngine()) {
|
||||
engine = cachedEngine;
|
||||
} else {
|
||||
engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_,
|
||||
0,
|
||||
false);
|
||||
// Populate command cache
|
||||
[[maybe_unused]] const auto commands =
|
||||
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
shardokState->setCachedEngine(engine);
|
||||
}
|
||||
const auto engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_,
|
||||
0,
|
||||
false);
|
||||
|
||||
// Get command descriptors
|
||||
const auto descriptors = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSGameEngine.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/ai/mcts/adapters/ShardokAction.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.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"
|
||||
@@ -99,11 +100,12 @@ public:
|
||||
void resetCacheStatistics();
|
||||
|
||||
private:
|
||||
// Transposition table entry for caching legal actions
|
||||
// Note: We don't store command protos since the Engine already caches them
|
||||
// Transposition table entry for caching legal actions.
|
||||
// Store immutable action snapshots only; ShardokEngine owns mutable command caches and
|
||||
// must remain per-state/per-thread.
|
||||
struct LegalActionsCache {
|
||||
std::vector<size_t> filteredIndices;
|
||||
std::shared_ptr<ShardokEngine> engine; // Engine with populated command cache
|
||||
size_t commandCount;
|
||||
std::vector<ShardokAction> actions;
|
||||
};
|
||||
|
||||
const AIScoreCalculator* scoreCalculator_;
|
||||
|
||||
@@ -98,7 +98,6 @@ std::unique_ptr<MCTSGameState> ShardokGameState::clone() const {
|
||||
apdCache_,
|
||||
alCache_,
|
||||
criticalTileCoords_);
|
||||
// Don't copy the cached engine - each state needs its own
|
||||
return cloned;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSGameState.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/ShardokEngine.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"
|
||||
@@ -52,10 +51,6 @@ public:
|
||||
[[nodiscard]] const GameSettings* getSettings() const { return settings_; }
|
||||
[[nodiscard]] const CoordsSet& getCriticalTileCoords() const { return criticalTileCoords_; }
|
||||
|
||||
// Engine caching for performance (avoids recomputing available commands)
|
||||
void setCachedEngine(std::shared_ptr<ShardokEngine> engine) const { cachedEngine_ = engine; }
|
||||
[[nodiscard]] std::shared_ptr<ShardokEngine> getCachedEngine() const { return cachedEngine_; }
|
||||
|
||||
// Invalidate hash cache when state is mutated
|
||||
void invalidateHashCache() const {
|
||||
hashCached_ = false;
|
||||
@@ -74,7 +69,6 @@ private:
|
||||
mutable uint64_t cachedHash_ = 0;
|
||||
mutable bool hashCached_ = false;
|
||||
const CoordsSet criticalTileCoords_; // Own the data to avoid dangling references
|
||||
mutable std::shared_ptr<ShardokEngine> cachedEngine_; // Engine with cached available commands
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
|
||||
@@ -133,8 +133,7 @@ auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
const auto cc = gameState->hex_map()->column_count();
|
||||
const auto occupants = Occupants(*gameState->units(), rc, cc);
|
||||
|
||||
for (const Coords& criticalTileLocation : criticalTileLocations) {
|
||||
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
|
||||
for (const auto index : criticalTileLocations.indexIterator()) {
|
||||
const auto& occupant = occupants[index];
|
||||
|
||||
if (occupant && occupant->battalion().type() !=
|
||||
@@ -160,6 +159,8 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
ActionPoints braveWaterCost) -> ScoreValue {
|
||||
vector<const Unit*> playerUnits{};
|
||||
vector<const Unit*> claimablePlayerUnits{};
|
||||
playerUnits.reserve(gameState->units()->size());
|
||||
claimablePlayerUnits.reserve(gameState->units()->size());
|
||||
for (const Unit* unit : *gameState->units()) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->player_id() == player->player_id()) {
|
||||
@@ -260,6 +261,8 @@ auto LastPlayerStandingVictoryScore(
|
||||
|
||||
vector<const Unit*> playerUnits{};
|
||||
vector<const Unit*> otherUnits{};
|
||||
playerUnits.reserve(gameState->units()->size());
|
||||
otherUnits.reserve(gameState->units()->size());
|
||||
|
||||
for (const Unit* unit : *gameState->units()) {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
|
||||
|
||||
@@ -23,7 +23,7 @@ using net::eagle0::shardok::storage::fb::PlayerInfo;
|
||||
|
||||
using ScoreValue = double;
|
||||
|
||||
auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
[[nodiscard]] auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileLocations,
|
||||
const PlayerInfo* player,
|
||||
@@ -32,12 +32,12 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> ScoreValue;
|
||||
|
||||
auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
[[nodiscard]] auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileLocations,
|
||||
const PlayerInfo* player) -> ScoreValue;
|
||||
|
||||
auto LastPlayerStandingVictoryScore(
|
||||
[[nodiscard]] auto LastPlayerStandingVictoryScore(
|
||||
const GameStateW& gameState,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
|
||||
@@ -230,7 +230,7 @@ auto MakeMCTSOptimizedAIScoreCalculator(
|
||||
std::vector<BattalionTypeSPtr> battalionTypes(BattalionTypeId::BattalionTypeId_MAX + 1);
|
||||
for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
|
||||
typeId <= BattalionTypeId::BattalionTypeId_MAX;
|
||||
typeId++) {
|
||||
++typeId) {
|
||||
auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
|
||||
battalionTypes[battalionTypeId] = settingsGetter.GetBattalionType(battalionTypeId);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,10 @@ private:
|
||||
|
||||
public:
|
||||
AttackerScoreTimer() : startTime(std::chrono::high_resolution_clock::now()) {}
|
||||
AttackerScoreTimer(const AttackerScoreTimer &) = delete;
|
||||
auto operator=(const AttackerScoreTimer &) -> AttackerScoreTimer & = delete;
|
||||
AttackerScoreTimer(AttackerScoreTimer &&) = delete;
|
||||
auto operator=(AttackerScoreTimer &&) -> AttackerScoreTimer & = delete;
|
||||
|
||||
~AttackerScoreTimer() {
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
@@ -352,7 +356,7 @@ auto MakeStandardAIScoreCalculatorWithExperiment(
|
||||
std::vector<BattalionTypeSPtr> battalionTypes(BattalionTypeId::BattalionTypeId_MAX + 1);
|
||||
for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
|
||||
typeId <= BattalionTypeId::BattalionTypeId_MAX;
|
||||
typeId++) {
|
||||
++typeId) {
|
||||
auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
|
||||
battalionTypes[battalionTypeId] = settingsGetter.GetBattalionType(battalionTypeId);
|
||||
}
|
||||
|
||||
+2
-4
@@ -7,8 +7,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace score_calculator_internal {
|
||||
namespace shardok::score_calculator_internal {
|
||||
|
||||
auto EffectiveDistanceCache::GetOrCompute(
|
||||
const Unit* unit,
|
||||
@@ -125,5 +124,4 @@ auto AttackerMultiplierForTargetDistance(
|
||||
isLateGame);
|
||||
}
|
||||
|
||||
} // namespace score_calculator_internal
|
||||
} // namespace shardok
|
||||
} // namespace shardok::score_calculator_internal
|
||||
|
||||
+8
-9
@@ -24,8 +24,7 @@
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map_generated.h"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit_generated.h"
|
||||
|
||||
namespace shardok {
|
||||
namespace score_calculator_internal {
|
||||
namespace shardok::score_calculator_internal {
|
||||
|
||||
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
@@ -51,13 +50,13 @@ struct EffectiveDistanceCache {
|
||||
struct CacheKey {
|
||||
UnitId unitId;
|
||||
Coords target;
|
||||
bool operator==(const CacheKey& other) const {
|
||||
[[nodiscard]] bool operator==(const CacheKey& other) const {
|
||||
return unitId == other.unitId && target == other.target;
|
||||
}
|
||||
};
|
||||
|
||||
struct CacheKeyHash {
|
||||
size_t operator()(const CacheKey& key) const {
|
||||
[[nodiscard]] size_t operator()(const CacheKey& key) const {
|
||||
return std::hash<UnitId>{}(key.unitId) ^ (std::hash<int>{}(key.target.row()) << 1) ^
|
||||
(std::hash<int>{}(key.target.column()) << 2);
|
||||
}
|
||||
@@ -65,7 +64,7 @@ struct EffectiveDistanceCache {
|
||||
|
||||
mutable gtl::flat_hash_map<CacheKey, DIST_T, CacheKeyHash> cache;
|
||||
|
||||
DIST_T GetOrCompute(
|
||||
[[nodiscard]] DIST_T GetOrCompute(
|
||||
const Unit* unit,
|
||||
const Coords& target,
|
||||
const ActionPointDistances* notBravingApd,
|
||||
@@ -75,11 +74,12 @@ struct EffectiveDistanceCache {
|
||||
|
||||
/// Calculate score for FLEE strategy
|
||||
/// Returns negative score based on fleeing units
|
||||
auto FleeStrategyScoreForState(const GameStateW& gameState, PlayerId playerId) -> ScoreValue;
|
||||
[[nodiscard]] auto FleeStrategyScoreForState(const GameStateW& gameState, PlayerId playerId)
|
||||
-> ScoreValue;
|
||||
|
||||
/// Calculate attacker multiplier based on distance to priority targets
|
||||
/// This is used to weight attacker units by their proximity to objectives
|
||||
auto AttackerMultiplierForTargetDistance(
|
||||
[[nodiscard]] auto AttackerMultiplierForTargetDistance(
|
||||
const Unit* attackingUnit,
|
||||
const std::vector<TargetAndAttackLocations>& priorityList,
|
||||
const std::vector<const Unit*>& occupants,
|
||||
@@ -89,7 +89,6 @@ auto AttackerMultiplierForTargetDistance(
|
||||
const ActionPointDistances* bravingApd,
|
||||
bool isLateGame) -> double;
|
||||
|
||||
} // namespace score_calculator_internal
|
||||
} // namespace shardok
|
||||
} // namespace shardok::score_calculator_internal
|
||||
|
||||
#endif // EAGLE0_SHARDOK_AI_SCORE_PRIVATE_AI_SCORE_CALCULATOR_SHARED_UTILITIES_HPP
|
||||
|
||||
@@ -47,19 +47,19 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
// Memoization cache for EffectiveDistance calls
|
||||
EffectiveDistanceCache distanceCache;
|
||||
|
||||
// Early return for empty game states
|
||||
const size_t estimatedUnitCount = cachedUnits->size();
|
||||
if (estimatedUnitCount == 0) { return UnitsScoreComponents{0.0, 0.0}; }
|
||||
|
||||
std::vector<const Unit *> attackerUnits{};
|
||||
std::vector<const Unit *> defenderUnits{};
|
||||
// Pre-allocate vectors based on estimated unit ratios to avoid reallocations
|
||||
const size_t estimatedUnitCount = cachedUnits->size();
|
||||
attackerUnits.reserve(estimatedUnitCount - 1);
|
||||
defenderUnits.reserve(estimatedUnitCount - 1);
|
||||
attackerUnits.reserve(estimatedUnitCount);
|
||||
defenderUnits.reserve(estimatedUnitCount);
|
||||
|
||||
double attackerUnitsValue = 0;
|
||||
double defenderUnitsValue = 0;
|
||||
|
||||
// Early return for empty game states
|
||||
if (cachedUnits->size() == 0) { return UnitsScoreComponents{0.0, 0.0}; }
|
||||
|
||||
auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);
|
||||
|
||||
for (const Unit *unit : *cachedUnits) {
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_battle_simulator {
|
||||
namespace shardok::ai_battle_simulator {
|
||||
|
||||
using net::eagle0::shardok::ai_battle_simulator::AIAlgorithmType;
|
||||
using net::eagle0::shardok::ai_battle_simulator::PlayerConfig;
|
||||
@@ -110,5 +109,4 @@ std::string AiBattleConfigLoader::ToJsonString(const BattleConfigProto& config)
|
||||
return jsonString;
|
||||
}
|
||||
|
||||
} // namespace ai_battle_simulator
|
||||
} // namespace shardok
|
||||
} // namespace shardok::ai_battle_simulator
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
#include "src/main/protobuf/net/eagle0/shardok/ai_battle_simulator/ai_battle_config.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_battle_simulator {
|
||||
namespace shardok::ai_battle_simulator {
|
||||
|
||||
using BattleConfigProto = net::eagle0::shardok::ai_battle_simulator::BattleConfig;
|
||||
|
||||
@@ -41,7 +40,6 @@ public:
|
||||
[[nodiscard]] static std::string ToJsonString(const BattleConfigProto& config);
|
||||
};
|
||||
|
||||
} // namespace ai_battle_simulator
|
||||
} // namespace shardok
|
||||
} // namespace shardok::ai_battle_simulator
|
||||
|
||||
#endif // EAGLE0_SHARDOK_AI_BATTLE_SIMULATOR_AI_BATTLE_CONFIG_HPP
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
@@ -27,8 +28,7 @@
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_battle_simulator {
|
||||
namespace shardok::ai_battle_simulator {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -330,9 +330,6 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
|
||||
// Load map
|
||||
auto hexMapProto = LoadMap(config_.map_name());
|
||||
|
||||
// Create player info protos
|
||||
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos;
|
||||
|
||||
// Attacker info
|
||||
net::eagle0::shardok::common::PlayerInfo attackerInfo;
|
||||
attackerInfo.set_player_id(ATTACKER_ID);
|
||||
@@ -342,7 +339,6 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
|
||||
attackerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
|
||||
playerInfoProtos.push_back(attackerInfo);
|
||||
|
||||
// Defender info
|
||||
net::eagle0::shardok::common::PlayerInfo defenderInfo;
|
||||
@@ -353,10 +349,14 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
|
||||
defenderInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
|
||||
playerInfoProtos.push_back(defenderInfo);
|
||||
|
||||
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos{
|
||||
std::move(attackerInfo),
|
||||
std::move(defenderInfo)};
|
||||
|
||||
// Create units from config
|
||||
std::vector<net::eagle0::shardok::storage::fb::Unit> units;
|
||||
units.reserve(config_.attacker().units_size() + config_.defender().units_size());
|
||||
|
||||
// Attacker units
|
||||
const int attackerUnitCount = config_.attacker().units_size();
|
||||
@@ -450,7 +450,7 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
|
||||
unit.mutable_opponent_knowledge()->Mutate(0, 0);
|
||||
unit.mutable_opponent_knowledge()->Mutate(1, 0);
|
||||
|
||||
units.push_back(unit);
|
||||
units.push_back(std::move(unit));
|
||||
}
|
||||
|
||||
// Defender units
|
||||
@@ -546,7 +546,7 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
|
||||
unit.mutable_opponent_knowledge()->Mutate(0, 0);
|
||||
unit.mutable_opponent_knowledge()->Mutate(1, 0);
|
||||
|
||||
units.push_back(unit);
|
||||
units.push_back(std::move(unit));
|
||||
}
|
||||
|
||||
// Create game state
|
||||
@@ -914,6 +914,7 @@ BattleResult AiBattleSimulator::CreateResultFromGameState(
|
||||
// Collect surviving units (for winner, or all units if draw)
|
||||
const auto* units = state->units();
|
||||
if (units) {
|
||||
result.survivingUnits.reserve(units->size());
|
||||
for (const auto* unit : *units) {
|
||||
if (!unit) { continue; }
|
||||
|
||||
@@ -957,5 +958,4 @@ BattleResult AiBattleSimulator::CreateResultFromGameState(
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace ai_battle_simulator
|
||||
} // namespace shardok
|
||||
} // namespace shardok::ai_battle_simulator
|
||||
|
||||
@@ -30,7 +30,9 @@ class ShardokEngine;
|
||||
// Type alias for hex map
|
||||
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
|
||||
|
||||
namespace ai_battle_simulator {
|
||||
} // namespace shardok
|
||||
|
||||
namespace shardok::ai_battle_simulator {
|
||||
|
||||
using BattleConfigProto = net::eagle0::shardok::ai_battle_simulator::BattleConfig;
|
||||
|
||||
@@ -146,7 +148,6 @@ private:
|
||||
CreateResultFromGameState(const GameStateW& state, int totalRounds, int totalCommands) const;
|
||||
};
|
||||
|
||||
} // namespace ai_battle_simulator
|
||||
} // namespace shardok
|
||||
} // namespace shardok::ai_battle_simulator
|
||||
|
||||
#endif // EAGLE0_SHARDOK_AI_BATTLE_SIMULATOR_AI_BATTLE_SIMULATOR_HPP
|
||||
|
||||
@@ -88,8 +88,7 @@ void GenerateConfigFile(const std::string& outputPath) {
|
||||
// Output to file
|
||||
std::ofstream outFile(outputPath);
|
||||
if (!outFile.is_open()) {
|
||||
std::cerr << "Error: Failed to open output file: " << outputPath << "\n";
|
||||
std::exit(1);
|
||||
throw std::runtime_error("Failed to open output file: " + outputPath);
|
||||
}
|
||||
outFile << jsonString << "\n";
|
||||
std::cout << "Sample config written to: " << outputPath << "\n";
|
||||
@@ -295,8 +294,7 @@ BattleResult RunBattle(
|
||||
|
||||
if (!config) {
|
||||
restoreOutput();
|
||||
std::cerr << "Error: Failed to load config file\n";
|
||||
std::exit(1);
|
||||
throw std::runtime_error("Failed to load config file");
|
||||
}
|
||||
|
||||
// Create simulator
|
||||
@@ -328,6 +326,8 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
std::vector<std::string> configPaths;
|
||||
std::vector<std::string> statePaths;
|
||||
configPaths.reserve(argc - 1);
|
||||
statePaths.reserve(argc - 1);
|
||||
std::string outputPath;
|
||||
std::string tracePath;
|
||||
std::string runLabel;
|
||||
|
||||
+2
-3
@@ -2,7 +2,6 @@
|
||||
// Converts saved Eagle ShardokBattle/GameState protobuf blobs into AI battle configs.
|
||||
//
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
@@ -146,9 +145,9 @@ int main(int argc, char *argv[]) {
|
||||
} else if (arg.starts_with("--output=")) {
|
||||
outputPath = arg.substr(9);
|
||||
} else if (arg.starts_with("--seed=")) {
|
||||
seed = std::atoi(arg.substr(7).c_str());
|
||||
seed = std::stoi(arg.substr(7));
|
||||
} else if (arg.starts_with("--max-rounds=")) {
|
||||
maxRounds = std::atoi(arg.substr(13).c_str());
|
||||
maxRounds = std::stoi(arg.substr(13));
|
||||
} else if (arg.starts_with("--attacker-scoring=")) {
|
||||
const auto scoring = ParseScoring(arg.substr(19));
|
||||
if (!scoring.has_value()) { throw std::runtime_error("Unknown attacker scoring"); }
|
||||
|
||||
+6
-1
@@ -115,6 +115,7 @@ int main(int argc, char *argv[]) {
|
||||
std::string resultPath;
|
||||
std::string stateFbPath;
|
||||
std::vector<std::string> actionViewPaths;
|
||||
actionViewPaths.reserve(argc - 1);
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string arg = argv[i];
|
||||
@@ -145,6 +146,9 @@ int main(int argc, char *argv[]) {
|
||||
std::vector<int> attackerFactions;
|
||||
std::vector<int> defenderFactions;
|
||||
std::vector<std::string> playerSides;
|
||||
attackerFactions.reserve(battle.players_size());
|
||||
defenderFactions.reserve(battle.players_size());
|
||||
playerSides.reserve(battle.players_size());
|
||||
bool aiVsAi = true;
|
||||
for (const auto &player : battle.players()) {
|
||||
if (player.eagle_fid() == 3 || player.eagle_fid() == 4) { aiVsAi = false; }
|
||||
@@ -187,7 +191,8 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
std::cout << "],\"has_game_status\":" << (hasGameStatus ? "true" : "false") << "}\n";
|
||||
return 0;
|
||||
} else if (!resultPath.empty()) {
|
||||
}
|
||||
if (!resultPath.empty()) {
|
||||
ActionResult result;
|
||||
if (!result.ParseFromString(ReadFile(resultPath))) {
|
||||
throw std::runtime_error("Failed to parse Shardok action result proto");
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
#include "AIPerformanceRunner.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "PerformanceTestGameStateBuilder.hpp"
|
||||
@@ -47,7 +48,7 @@ auto CompletionReasonToString(EvaluationCompletionReason reason) -> std::string
|
||||
/**
|
||||
* Parse command line arguments into a configuration struct.
|
||||
*/
|
||||
auto ParseCommandLineArgs(int argc, char* argv[]) -> PerformanceTestConfig {
|
||||
auto ParseCommandLineArgs(int argc, char* argv[]) -> std::optional<PerformanceTestConfig> {
|
||||
PerformanceTestConfig config;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
@@ -63,7 +64,7 @@ auto ParseCommandLineArgs(int argc, char* argv[]) -> PerformanceTestConfig {
|
||||
<< " --defender=BOOL AI is defender (default: false)\n"
|
||||
<< " --verbose Enable verbose output\n"
|
||||
<< " --help, -h Show this help message\n";
|
||||
std::exit(0);
|
||||
return std::nullopt;
|
||||
} else if (arg.starts_with("--map=")) {
|
||||
config.mapName = arg.substr(6);
|
||||
} else if (arg.starts_with("--turns=")) {
|
||||
@@ -74,9 +75,8 @@ auto ParseCommandLineArgs(int argc, char* argv[]) -> PerformanceTestConfig {
|
||||
} else if (arg == "--verbose") {
|
||||
config.verbose = true;
|
||||
} else {
|
||||
std::cerr << "Unknown argument: " << arg << "\n";
|
||||
std::cerr << "Use --help for usage information.\n";
|
||||
std::exit(1);
|
||||
throw std::invalid_argument(
|
||||
"Unknown argument: " + arg + "\nUse --help for usage information.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,9 @@ int main(int argc, char* argv[]) {
|
||||
std::cout << "==============================\n";
|
||||
|
||||
// Parse command line arguments
|
||||
auto config = ParseCommandLineArgs(argc, argv);
|
||||
const auto parsedConfig = ParseCommandLineArgs(argc, argv);
|
||||
if (!parsedConfig.has_value()) { return 0; }
|
||||
const auto& config = parsedConfig.value();
|
||||
|
||||
if (config.verbose) {
|
||||
std::cout << "Configuration:\n";
|
||||
@@ -163,6 +165,7 @@ int main(int argc, char* argv[]) {
|
||||
std::cout << "Running AI performance test for " << config.numTurns << " turns...\n";
|
||||
|
||||
std::vector<AIPerformanceMetrics> metrics;
|
||||
if (config.numTurns > 0) { metrics.reserve(config.numTurns); }
|
||||
|
||||
for (int turn = 0; turn < config.numTurns; ++turn) {
|
||||
// Check if AI can make a move
|
||||
|
||||
+12
-6
@@ -4,7 +4,9 @@
|
||||
|
||||
#include "PerformanceTestGameStateBuilder.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai_testing_common/GameSettingsFactory.hpp"
|
||||
@@ -53,6 +55,7 @@ auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
|
||||
|
||||
// Create player info protos
|
||||
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos;
|
||||
playerInfoProtos.reserve(2);
|
||||
|
||||
// AI player
|
||||
net::eagle0::shardok::common::PlayerInfo aiPlayerInfo;
|
||||
@@ -68,7 +71,7 @@ auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
|
||||
aiPlayerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
|
||||
}
|
||||
playerInfoProtos.push_back(aiPlayerInfo);
|
||||
playerInfoProtos.push_back(std::move(aiPlayerInfo));
|
||||
|
||||
// Human player
|
||||
net::eagle0::shardok::common::PlayerInfo humanPlayerInfo;
|
||||
@@ -84,13 +87,16 @@ auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
|
||||
humanPlayerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
|
||||
}
|
||||
playerInfoProtos.push_back(humanPlayerInfo);
|
||||
playerInfoProtos.push_back(std::move(humanPlayerInfo));
|
||||
|
||||
// Create units
|
||||
const int clampedAiUnitCount = std::clamp(aiUnitCount, 0, 6);
|
||||
const int clampedHumanUnitCount = std::clamp(humanUnitCount, 0, 6);
|
||||
std::vector<net::eagle0::shardok::storage::fb::Unit> units;
|
||||
units.reserve(clampedAiUnitCount + clampedHumanUnitCount);
|
||||
|
||||
// Create AI units in reserve (location -1, -1)
|
||||
for (int i = 0; i < aiUnitCount && i < 6; ++i) {
|
||||
for (int i = 0; i < clampedAiUnitCount; ++i) {
|
||||
units.push_back(AddGenericUnit(
|
||||
AI_PLAYER_ID,
|
||||
i, // Unit ID
|
||||
@@ -101,10 +107,10 @@ auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
|
||||
}
|
||||
|
||||
// Create human units in reserve (location -1, -1)
|
||||
for (int i = 0; i < humanUnitCount && i < 6; ++i) {
|
||||
for (int i = 0; i < clampedHumanUnitCount; ++i) {
|
||||
units.push_back(AddGenericUnit(
|
||||
HUMAN_PLAYER_ID,
|
||||
aiUnitCount + i, // Unit ID starting aiUnitCount
|
||||
clampedAiUnitCount + i, // Unit ID starting after generated AI units
|
||||
net::eagle0::shardok::storage::fb::Coords(-1, -1), // Reserve location
|
||||
NO_PROFESSION,
|
||||
HEAVY_INFANTRY_BATTALION_TYPE,
|
||||
@@ -235,4 +241,4 @@ auto PerformanceTestGameStateBuilder::AddGenericUnit(
|
||||
return unit;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
+5
-5
@@ -32,7 +32,7 @@ public:
|
||||
* Initialize game settings from the default configuration files.
|
||||
* Must be called before creating game states.
|
||||
*/
|
||||
static auto InitializeGameSettings() -> GameSettingsSPtr;
|
||||
[[nodiscard]] static auto InitializeGameSettings() -> GameSettingsSPtr;
|
||||
|
||||
/**
|
||||
* Create the standard "Perf" test configuration:
|
||||
@@ -44,7 +44,7 @@ public:
|
||||
* @param defenderToggle If true, AI is defender; if false, AI is attacker
|
||||
* @return A GameStateW with the configured battle
|
||||
*/
|
||||
static auto CreatePerfTestGameState(
|
||||
[[nodiscard]] static auto CreatePerfTestGameState(
|
||||
const GameSettingsSPtr& settings,
|
||||
bool defenderToggle = false) -> GameStateW;
|
||||
|
||||
@@ -58,7 +58,7 @@ public:
|
||||
* @param defenderToggle If true, AI is defender; if false, AI is attacker
|
||||
* @return A GameStateW with the configured battle
|
||||
*/
|
||||
static auto CreateCustomTestGameState(
|
||||
[[nodiscard]] static auto CreateCustomTestGameState(
|
||||
const GameSettingsSPtr& settings,
|
||||
const std::string& mapName,
|
||||
int aiUnitCount,
|
||||
@@ -67,11 +67,11 @@ public:
|
||||
|
||||
private:
|
||||
// Helper functions for building game state components
|
||||
static auto
|
||||
[[nodiscard]] static auto
|
||||
AddPlayerInfo(flatbuffers::FlatBufferBuilder& fbb, int playerId, bool isDefender, int food)
|
||||
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
|
||||
|
||||
static auto AddGenericUnit(
|
||||
[[nodiscard]] static auto AddGenericUnit(
|
||||
PlayerId playerId,
|
||||
UnitId unitId,
|
||||
const net::eagle0::shardok::storage::fb::Coords& location,
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_testing_common {
|
||||
namespace shardok::ai_testing_common {
|
||||
|
||||
auto AIClientFactory::Create(
|
||||
PlayerId playerId,
|
||||
@@ -30,5 +29,4 @@ auto AIClientFactory::Create(
|
||||
mctsConfig);
|
||||
}
|
||||
|
||||
} // namespace ai_testing_common
|
||||
} // namespace shardok
|
||||
} // namespace shardok::ai_testing_common
|
||||
|
||||
@@ -54,7 +54,7 @@ public:
|
||||
* @param mctsConfig MCTS settings, used only when algorithmType is MCTS
|
||||
* @return Unique pointer to created ShardokAIClient
|
||||
*/
|
||||
static auto Create(
|
||||
[[nodiscard]] static auto Create(
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
const net::eagle0::shardok::storage::fb::HexMap* hexMap,
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_testing_common {
|
||||
namespace shardok::ai_testing_common {
|
||||
|
||||
auto GamePhaseRunner::RunSetupPhase(
|
||||
ShardokEngine& engine,
|
||||
@@ -58,5 +57,4 @@ auto GamePhaseRunner::RunSingleTurn(ShardokEngine& engine, PlayerId playerId, Sh
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ai_testing_common
|
||||
} // namespace shardok
|
||||
} // namespace shardok::ai_testing_common
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/BattalionTypeRegistrar.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_testing_common {
|
||||
namespace shardok::ai_testing_common {
|
||||
|
||||
auto GameSettingsFactory::CreateDefault() -> GameSettingsSPtr {
|
||||
auto settings = std::make_shared<GameSettings>();
|
||||
@@ -31,5 +30,4 @@ auto GameSettingsFactory::CreateDefault() -> GameSettingsSPtr {
|
||||
return settings;
|
||||
}
|
||||
|
||||
} // namespace ai_testing_common
|
||||
} // namespace shardok
|
||||
} // namespace shardok::ai_testing_common
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_testing_common {
|
||||
namespace shardok::ai_testing_common {
|
||||
|
||||
/**
|
||||
* Factory class for creating GameSettings instances with standard configuration.
|
||||
@@ -30,10 +29,9 @@ public:
|
||||
*
|
||||
* @return Shared pointer to initialized GameSettings
|
||||
*/
|
||||
static auto CreateDefault() -> GameSettingsSPtr;
|
||||
[[nodiscard]] static auto CreateDefault() -> GameSettingsSPtr;
|
||||
};
|
||||
|
||||
} // namespace ai_testing_common
|
||||
} // namespace shardok
|
||||
} // namespace shardok::ai_testing_common
|
||||
|
||||
#endif // EAGLE0_SHARDOK_AI_TESTING_COMMON_GAME_SETTINGS_FACTORY_HPP
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -24,14 +25,80 @@
|
||||
|
||||
namespace shardok {
|
||||
using net::eagle0::common::ScopedShardokLatencyTrace;
|
||||
using std::condition_variable;
|
||||
using std::mutex;
|
||||
using std::scoped_lock;
|
||||
using std::unique_lock;
|
||||
using std::weak_ptr;
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] auto DescribeCommandForLog(const CommandSPtr &command) -> std::string {
|
||||
if (!command) { return "<null command>"; }
|
||||
|
||||
std::ostringstream out;
|
||||
out << "type=" << command->GetCommandType()
|
||||
<< " player=" << static_cast<int>(command->GetPlayerId())
|
||||
<< " actor=" << command->GetActorUnitId() << " target=("
|
||||
<< static_cast<int>(command->GetTargetRow()) << ','
|
||||
<< static_cast<int>(command->GetTargetColumn()) << ')';
|
||||
|
||||
if (command->HasOdds()) { out << " odds_percentile=" << command->GetOddsPercentile(); }
|
||||
|
||||
try {
|
||||
out << " proto={" << command->GetCommandProto().ShortDebugString() << '}';
|
||||
} catch (const std::exception &e) { out << " proto_error={" << e.what() << '}'; }
|
||||
|
||||
return out.str();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto DescribeCommandsForLog(const CommandList &commands) -> std::string {
|
||||
std::ostringstream out;
|
||||
out << "available_command_count=" << commands.size();
|
||||
for (size_t i = 0; i < commands.size(); ++i) {
|
||||
out << "\n [" << i << "] " << DescribeCommandForLog(commands[i]);
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto DescribeCurrentStateForLog(const ShardokEngine &engine) -> std::string {
|
||||
const auto *state = engine.GetCurrentGameState().Get();
|
||||
if (state == nullptr) { return "state=<null>"; }
|
||||
|
||||
const auto vectorSize = [](const auto *vector) -> size_t {
|
||||
return vector == nullptr ? 0 : vector->size();
|
||||
};
|
||||
|
||||
std::ostringstream out;
|
||||
out << "state={round=" << static_cast<int>(state->current_round())
|
||||
<< " current_player=" << static_cast<int>(state->current_player())
|
||||
<< " history_count=" << engine.GetUnfilteredHistoryCount();
|
||||
|
||||
if (const auto *status = state->status(); status != nullptr) {
|
||||
out << " status=" << static_cast<int>(status->state()) << " end_condition=";
|
||||
if (const auto *endCondition = status->end_game_condition(); endCondition != nullptr) {
|
||||
out << "{type=" << static_cast<int>(endCondition->victory_type())
|
||||
<< " victory_details=" << static_cast<int>(endCondition->victory_details())
|
||||
<< " draw_details=" << static_cast<int>(endCondition->draw_details()) << '}';
|
||||
} else {
|
||||
out << "<null>";
|
||||
}
|
||||
} else {
|
||||
out << " status=<null>";
|
||||
}
|
||||
|
||||
out << " units=" << vectorSize(state->units())
|
||||
<< " player_infos=" << vectorSize(state->player_infos());
|
||||
|
||||
if (const auto *hexMap = state->hex_map(); hexMap != nullptr) {
|
||||
out << " map=" << hexMap->row_count() << 'x' << hexMap->column_count();
|
||||
} else {
|
||||
out << " map=<null>";
|
||||
}
|
||||
|
||||
out << '}';
|
||||
return out.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class InvalidTokenException : public std::exception {};
|
||||
|
||||
class MismatchedPlayerIdException : public std::exception {
|
||||
@@ -66,9 +133,9 @@ public:
|
||||
[[nodiscard]] auto what() const noexcept -> const char * override { return message.c_str(); }
|
||||
};
|
||||
|
||||
vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
|
||||
const unique_ptr<ShardokEngine> &e) {
|
||||
vector<shared_ptr<ShardokAIClient>> aic;
|
||||
std::vector<std::shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
|
||||
const std::unique_ptr<ShardokEngine> &e) {
|
||||
std::vector<std::shared_ptr<ShardokAIClient>> aic;
|
||||
|
||||
mcts::MCTSConfig mctsConfig;
|
||||
mctsConfig.backpropagationPolicy = mcts::MCTSBackpropagationPolicy::MINIMAX;
|
||||
@@ -78,6 +145,7 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
|
||||
|
||||
// Check if all players are AI - if so, use faster time budgets
|
||||
const auto &playerInfos = e->GetPlayerInfos();
|
||||
aic.reserve(playerInfos.size());
|
||||
const bool isAllAiBattle =
|
||||
std::ranges::all_of(playerInfos, [](const auto &pi) { return pi.is_ai(); });
|
||||
|
||||
@@ -107,7 +175,7 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
|
||||
void ShardokGameController::LockedNotifyClients() const { updateCondition.notify_all(); }
|
||||
|
||||
auto ShardokGameController::LockedAIClientForPid(PlayerId pid) const
|
||||
-> shared_ptr<ShardokAIClient> {
|
||||
-> std::shared_ptr<ShardokAIClient> {
|
||||
const auto it = std::ranges::find_if(aiClients, [pid](const auto &client) {
|
||||
return client->GetPlayerId() == pid;
|
||||
});
|
||||
@@ -120,7 +188,7 @@ void ShardokGameController::DoAIThread() {
|
||||
|
||||
while (aiThreadKeepGoing) {
|
||||
// Phase 1: Gather data for AI decision (brief lock)
|
||||
shared_ptr<ShardokAIClient> aiClient;
|
||||
std::shared_ptr<ShardokAIClient> aiClient;
|
||||
PlayerId playerId;
|
||||
GameSettingsSPtr settings;
|
||||
net::eagle0::shardok::api::GameStateView gsv;
|
||||
@@ -128,7 +196,7 @@ void ShardokGameController::DoAIThread() {
|
||||
size_t expectedHistoryCount;
|
||||
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
std::unique_lock lk(masterLock);
|
||||
|
||||
if (engine->GameIsOver()) {
|
||||
aiThreadKeepGoing = false;
|
||||
@@ -158,6 +226,8 @@ void ShardokGameController::DoAIThread() {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string aiPhase = "choosing command";
|
||||
int64_t chosenIndex = -1;
|
||||
try {
|
||||
// Phase 2: AI thinks (NO LOCK - this is the slow part)
|
||||
auto results = [&] {
|
||||
@@ -169,10 +239,11 @@ void ShardokGameController::DoAIThread() {
|
||||
{"available_command_count", std::to_string(availableCommands->size())}});
|
||||
return aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
}();
|
||||
chosenIndex = results.chosenIndex;
|
||||
|
||||
// Phase 3: Post the command (brief lock)
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
std::unique_lock lk(masterLock);
|
||||
|
||||
// Verify state hasn't changed while we were thinking
|
||||
if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
|
||||
@@ -187,6 +258,7 @@ void ShardokGameController::DoAIThread() {
|
||||
}
|
||||
|
||||
{
|
||||
aiPhase = "posting chosen command";
|
||||
ScopedShardokLatencyTrace trace(
|
||||
"ai_engine_post_command",
|
||||
cachedGameId,
|
||||
@@ -197,6 +269,7 @@ void ShardokGameController::DoAIThread() {
|
||||
}
|
||||
LockedNotifyClients();
|
||||
{
|
||||
aiPhase = "posting forced follow-up commands";
|
||||
ScopedShardokLatencyTrace trace(
|
||||
"ai_engine_post_forced_commands",
|
||||
cachedGameId,
|
||||
@@ -208,16 +281,32 @@ void ShardokGameController::DoAIThread() {
|
||||
aiThreadKeepGoing = !engine->GameIsOver();
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
std::ostringstream error;
|
||||
error << "AI error in game " << cachedGameId << ", player " << playerId << ", round "
|
||||
<< gsv.current_round() << ": " << e.what();
|
||||
|
||||
std::ostringstream context;
|
||||
context << "AI failure context: phase=" << aiPhase
|
||||
<< ", expected_history_count=" << expectedHistoryCount
|
||||
<< ", current_history_count=" << engine->GetUnfilteredHistoryCount()
|
||||
<< ", chosen_index=" << chosenIndex
|
||||
<< ", current_player=" << static_cast<int>(engine->GetCurrentPlayerId()) << ", "
|
||||
<< DescribeCurrentStateForLog(*engine);
|
||||
|
||||
const std::string commandsContext = availableCommands
|
||||
? DescribeCommandsForLog(*availableCommands)
|
||||
: "available_command_count=<cleared>";
|
||||
|
||||
std::cerr << "AI thread exception in game " << cachedGameId << ", player " << playerId
|
||||
<< ", round " << gsv.current_round() << ": " << e.what() << '\n';
|
||||
std::cerr << context.str() << '\n';
|
||||
std::cerr << commandsContext << '\n';
|
||||
|
||||
void *backtraceArray[20];
|
||||
const int backtraceSize = backtrace(backtraceArray, 20);
|
||||
backtrace_symbols_fd(backtraceArray, backtraceSize, STDERR_FILENO);
|
||||
|
||||
aiThreadErrorMessage = "AI error in game " + cachedGameId + ", player " +
|
||||
std::to_string(playerId) + ", round " +
|
||||
std::to_string(gsv.current_round()) + ": " + e.what();
|
||||
aiThreadErrorMessage = error.str() + "\n" + context.str() + "\n" + commandsContext;
|
||||
aiThreadFailed.store(true);
|
||||
updateCondition.notify_all();
|
||||
break;
|
||||
@@ -228,7 +317,7 @@ void ShardokGameController::DoAIThread() {
|
||||
|
||||
ShardokGameController::~ShardokGameController() {
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
std::unique_lock lk(masterLock);
|
||||
aiThreadKeepGoing = false;
|
||||
aiCondition.notify_one();
|
||||
}
|
||||
@@ -236,7 +325,7 @@ ShardokGameController::~ShardokGameController() {
|
||||
}
|
||||
|
||||
void CheckFactionId(
|
||||
const unique_ptr<ShardokEngine> &engine,
|
||||
const std::unique_ptr<ShardokEngine> &engine,
|
||||
const PlayerId shardokPlayerId,
|
||||
const int eagleFactionId) {
|
||||
bool found = false;
|
||||
@@ -256,7 +345,7 @@ void ShardokGameController::PostCommand(
|
||||
const int eagleFactionId,
|
||||
const int64_t token,
|
||||
const int64_t commandIndex,
|
||||
const optional<int32_t> roll) {
|
||||
const std::optional<int32_t> roll) {
|
||||
ScopedShardokLatencyTrace totalTrace(
|
||||
"controller_post_command_total",
|
||||
cachedGameId,
|
||||
@@ -264,7 +353,7 @@ void ShardokGameController::PostCommand(
|
||||
{"eagle_faction_id", std::to_string(eagleFactionId)},
|
||||
{"token", std::to_string(token)},
|
||||
{"command_index", std::to_string(commandIndex)}});
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
std::scoped_lock<std::mutex> guard(masterLock);
|
||||
|
||||
CheckFactionId(engine, shardokPlayerId, eagleFactionId);
|
||||
|
||||
@@ -303,7 +392,7 @@ void ShardokGameController::PostPlacementCommands(
|
||||
const PlayerId shardokPlayerId,
|
||||
const int eagleFactionId,
|
||||
const int64_t token,
|
||||
const vector<UnitPlacementInfo> &infos) {
|
||||
const std::vector<UnitPlacementInfo> &infos) {
|
||||
ScopedShardokLatencyTrace totalTrace(
|
||||
"controller_post_placement_commands_total",
|
||||
cachedGameId,
|
||||
@@ -312,7 +401,7 @@ void ShardokGameController::PostPlacementCommands(
|
||||
{"token", std::to_string(token)},
|
||||
{"placement_count", std::to_string(infos.size())}});
|
||||
{
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
std::scoped_lock<std::mutex> guard(masterLock);
|
||||
|
||||
CheckFactionId(engine, shardokPlayerId, eagleFactionId);
|
||||
|
||||
@@ -356,7 +445,7 @@ void ShardokGameController::PostPlacementCommands(
|
||||
}
|
||||
|
||||
auto ShardokGameController::GetCurrentGameStateBytes() -> byte_vector {
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
std::scoped_lock<std::mutex> guard(masterLock);
|
||||
return engine->GetCurrentGameStateBytes();
|
||||
}
|
||||
|
||||
@@ -368,9 +457,9 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
AllUpdates updates{};
|
||||
|
||||
incomingRegistrations++;
|
||||
vector<net::eagle0::shardok::storage::ShardokActionWithResultingState> awrs;
|
||||
std::vector<net::eagle0::shardok::storage::ShardokActionWithResultingState> awrs;
|
||||
{
|
||||
unique_lock<mutex> guard(masterLock);
|
||||
std::unique_lock<std::mutex> guard(masterLock);
|
||||
|
||||
updates.newUnfilteredCount = engine->GetUnfilteredHistoryCount();
|
||||
|
||||
@@ -400,7 +489,7 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
|
||||
auto fid = pi.eagle_faction_id();
|
||||
|
||||
vector<net::eagle0::shardok::api::ActionResultView> actionResultViews;
|
||||
std::vector<net::eagle0::shardok::api::ActionResultView> actionResultViews;
|
||||
{
|
||||
ScopedShardokLatencyTrace trace(
|
||||
"engine_filter_new_results",
|
||||
@@ -411,7 +500,7 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
actionResultViews = engine->FilterNewResults(pi.player_id(), startingActionId);
|
||||
}
|
||||
|
||||
shared_ptr<AvailableCommands> acs = nullptr;
|
||||
std::shared_ptr<AvailableCommands> acs = nullptr;
|
||||
|
||||
const auto availableCommands = [&] {
|
||||
ScopedShardokLatencyTrace trace(
|
||||
@@ -430,9 +519,11 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
|
||||
if (!availableCommands.empty() || !previewCommands.empty()) {
|
||||
acs = std::make_shared<AvailableCommands>();
|
||||
acs->mutable_current_command()->Reserve(availableCommands.size());
|
||||
*acs->mutable_current_command() = {
|
||||
std::begin(availableCommands),
|
||||
std::end(availableCommands)};
|
||||
acs->mutable_preview_command()->Reserve(previewCommands.size());
|
||||
*acs->mutable_preview_command() = {
|
||||
std::begin(previewCommands),
|
||||
std::end(previewCommands)};
|
||||
@@ -483,6 +574,7 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
// Extract battle progress data from the FlatBuffer game state
|
||||
const auto *gs = engine->GetCurrentGameState().Get();
|
||||
updates.currentRound = gs->current_round();
|
||||
updates.playerTroopCounts.reserve(gs->player_infos()->size());
|
||||
for (const auto *player : *gs->player_infos()) {
|
||||
int32_t troopCount = 0;
|
||||
for (const auto *unit : *gs->units()) {
|
||||
@@ -504,36 +596,37 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
}
|
||||
|
||||
auto ShardokGameController::GameIsOver() -> bool {
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
std::scoped_lock<std::mutex> guard(masterLock);
|
||||
|
||||
const auto &gameStatus = engine->GetGameStatus();
|
||||
return ShardokEngine::GameIsOver(gameStatus);
|
||||
}
|
||||
|
||||
auto ShardokGameController::GameOverStatus() -> net::eagle0::shardok::common::GameStatus {
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
std::scoped_lock<std::mutex> guard(masterLock);
|
||||
|
||||
return fb::ToProto(engine->GetGameStatus());
|
||||
}
|
||||
|
||||
auto ShardokGameController::EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit> {
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
auto ShardokGameController::EndGameUnits()
|
||||
-> std::vector<net::eagle0::shardok::storage::ResolvedUnit> {
|
||||
std::scoped_lock<std::mutex> guard(masterLock);
|
||||
return engine->EndGameUnits();
|
||||
}
|
||||
|
||||
auto ShardokGameController::ResolvedPlayerInfos()
|
||||
-> vector<net::eagle0::shardok::common::PlayerInfo> {
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
-> std::vector<net::eagle0::shardok::common::PlayerInfo> {
|
||||
std::scoped_lock<std::mutex> guard(masterLock);
|
||||
return engine->GetPlayerInfos();
|
||||
}
|
||||
|
||||
void ShardokGameController::RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber) {
|
||||
scoped_lock<mutex> guard(subscriberLock);
|
||||
std::scoped_lock<std::mutex> guard(subscriberLock);
|
||||
subscribers.push_back(subscriber);
|
||||
}
|
||||
|
||||
void ShardokGameController::UnregisterSubscriber(const StreamSubscriber *subscriber) {
|
||||
scoped_lock<mutex> guard(subscriberLock);
|
||||
std::scoped_lock<std::mutex> guard(subscriberLock);
|
||||
subscribers.erase(
|
||||
std::remove_if(
|
||||
subscribers.begin(),
|
||||
@@ -556,7 +649,7 @@ auto ShardokGameController::WaitForUpdatesAndPush(
|
||||
GameOverInfo gameOverInfo{};
|
||||
|
||||
{
|
||||
unique_lock<mutex> guard(masterLock);
|
||||
std::unique_lock<std::mutex> guard(masterLock);
|
||||
|
||||
// Wait for updates, game over, or AI thread failure
|
||||
updateCondition.wait(guard, [this, lastPushedActionId] {
|
||||
|
||||
@@ -10,9 +10,14 @@
|
||||
#define EAGLE0_SHARDOK_CONTROLLER_SHARDOK_GAME_CONTROLLER_HPP
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -26,31 +31,27 @@
|
||||
namespace shardok {
|
||||
using net::eagle0::shardok::api::ActionResultView;
|
||||
using net::eagle0::shardok::api::AvailableCommands;
|
||||
using std::optional;
|
||||
using std::shared_ptr;
|
||||
using std::unique_ptr;
|
||||
using std::weak_ptr;
|
||||
|
||||
// Forward declaration
|
||||
class ShardokGameController;
|
||||
|
||||
/// Info about a game that has ended, for notifying subscribers
|
||||
struct GameOverInfo {
|
||||
vector<net::eagle0::shardok::common::PlayerInfo> playerInfos;
|
||||
vector<net::eagle0::shardok::storage::ResolvedUnit> endGameUnits;
|
||||
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfos;
|
||||
std::vector<net::eagle0::shardok::storage::ResolvedUnit> endGameUnits;
|
||||
net::eagle0::shardok::common::GameStatus gameStatus;
|
||||
};
|
||||
|
||||
/// Updates for a single player (includes faction ID for client routing)
|
||||
struct OnePlayerUpdates {
|
||||
int32_t eagleFactionId;
|
||||
vector<ActionResultView> resultViews;
|
||||
shared_ptr<AvailableCommands> availableCommands;
|
||||
std::vector<ActionResultView> resultViews;
|
||||
std::shared_ptr<AvailableCommands> availableCommands;
|
||||
|
||||
OnePlayerUpdates(
|
||||
const int32_t fid,
|
||||
const vector<ActionResultView>& arvs,
|
||||
const shared_ptr<AvailableCommands>& acs)
|
||||
const std::vector<ActionResultView>& arvs,
|
||||
const std::shared_ptr<AvailableCommands>& acs)
|
||||
: eagleFactionId(fid),
|
||||
resultViews(arvs),
|
||||
availableCommands(acs) {}
|
||||
@@ -70,12 +71,12 @@ public:
|
||||
|
||||
/// Called when new game updates are available
|
||||
virtual void OnUpdate(
|
||||
const vector<ActionResult>& mainResults,
|
||||
const vector<OnePlayerUpdates>& filteredResults,
|
||||
const std::vector<ActionResult>& mainResults,
|
||||
const std::vector<OnePlayerUpdates>& filteredResults,
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector& currentGameState,
|
||||
int32_t currentRound,
|
||||
const vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
|
||||
const std::vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
|
||||
|
||||
/// Called when the game ends
|
||||
virtual void OnGameOver(const GameOverInfo& info) = 0;
|
||||
@@ -101,51 +102,52 @@ private:
|
||||
mutable std::mutex subscriberLock{};
|
||||
std::vector<std::weak_ptr<StreamSubscriber>> subscribers{};
|
||||
|
||||
string serializedRequest;
|
||||
std::string serializedRequest;
|
||||
|
||||
unique_ptr<ShardokEngine> engine;
|
||||
std::unique_ptr<ShardokEngine> engine;
|
||||
|
||||
// Cached immutable data - safe to access without lock since it never changes after construction
|
||||
const GameId cachedGameId;
|
||||
|
||||
std::atomic_int incomingRegistrations = 0;
|
||||
|
||||
const string mapName;
|
||||
const string logFilePath;
|
||||
const std::string mapName;
|
||||
const std::string logFilePath;
|
||||
|
||||
// AI thread error state - written once by AI thread before setting atomic flag
|
||||
std::atomic<bool> aiThreadFailed{false};
|
||||
std::string aiThreadErrorMessage;
|
||||
|
||||
vector<shared_ptr<ShardokAIClient>> aiClients;
|
||||
std::vector<std::shared_ptr<ShardokAIClient>> aiClients;
|
||||
std::thread aiThread;
|
||||
|
||||
// Each entry maps an Eagle faction id of a non-participant watcher to the
|
||||
// Shardok pids of participants whose allies list named this watcher. Used
|
||||
// when generating per-watcher filtered views in GetUpdates.
|
||||
const vector<std::pair<int32_t, vector<PlayerId>>> watcherAllies;
|
||||
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> watcherAllies;
|
||||
|
||||
static auto MakeLogFilePath() -> string {
|
||||
const time_t timer = time(nullptr);
|
||||
static auto MakeLogFilePath() -> std::string {
|
||||
const std::time_t timer = std::time(nullptr);
|
||||
char buf[255];
|
||||
strftime(buf, 255, "/tmp/playthrough_%Y_%m_%d_%H%M%S.log", localtime(&timer));
|
||||
return string(buf);
|
||||
std::strftime(buf, 255, "/tmp/playthrough_%Y_%m_%d_%H%M%S.log", std::localtime(&timer));
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
void LockedNotifyClients() const;
|
||||
|
||||
auto LockedAIClientForPid(PlayerId pid) const -> shared_ptr<ShardokAIClient>;
|
||||
auto LockedAIClientForPid(PlayerId pid) const -> std::shared_ptr<ShardokAIClient>;
|
||||
|
||||
static vector<shared_ptr<ShardokAIClient>> MakeAIClients(const unique_ptr<ShardokEngine>& e);
|
||||
static std::vector<std::shared_ptr<ShardokAIClient>> MakeAIClients(
|
||||
const std::unique_ptr<ShardokEngine>& e);
|
||||
|
||||
void DoAIThread();
|
||||
|
||||
public:
|
||||
ShardokGameController(
|
||||
unique_ptr<ShardokEngine> e,
|
||||
string mapName,
|
||||
string serializedRequest = "",
|
||||
vector<std::pair<int32_t, vector<PlayerId>>> watcherAllies = {})
|
||||
std::unique_ptr<ShardokEngine> e,
|
||||
std::string mapName,
|
||||
std::string serializedRequest = "",
|
||||
std::vector<std::pair<int32_t, std::vector<PlayerId>>> watcherAllies = {})
|
||||
: serializedRequest(std::move(serializedRequest)),
|
||||
engine(std::move(e)),
|
||||
cachedGameId(engine->GetGameId()),
|
||||
@@ -160,40 +162,40 @@ public:
|
||||
|
||||
~ShardokGameController();
|
||||
|
||||
auto SerializedRequest() const -> string { return serializedRequest; }
|
||||
auto SerializedRequest() const -> std::string { return serializedRequest; }
|
||||
|
||||
void PostCommand(
|
||||
PlayerId shardokPlayerId,
|
||||
int eagleFactionId,
|
||||
int64_t token,
|
||||
int64_t commandIndex,
|
||||
optional<int32_t> roll);
|
||||
std::optional<int32_t> roll);
|
||||
|
||||
void PostPlacementCommands(
|
||||
PlayerId shardokPlayerId,
|
||||
int eagleFactionId,
|
||||
int64_t token,
|
||||
const vector<UnitPlacementInfo>& infos);
|
||||
const std::vector<UnitPlacementInfo>& infos);
|
||||
|
||||
struct AllUpdates {
|
||||
vector<ActionResult> mainResults;
|
||||
vector<OnePlayerUpdates> filteredResults;
|
||||
std::vector<ActionResult> mainResults;
|
||||
std::vector<OnePlayerUpdates> filteredResults;
|
||||
int32_t newUnfilteredCount;
|
||||
byte_vector currentGameState;
|
||||
int32_t currentRound = 0;
|
||||
vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
|
||||
std::vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
|
||||
};
|
||||
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
|
||||
auto GetCurrentGameStateBytes() -> byte_vector;
|
||||
|
||||
auto GameIsOver() -> bool;
|
||||
auto GameOverStatus() -> net::eagle0::shardok::common::GameStatus;
|
||||
auto ResolvedPlayerInfos() -> vector<net::eagle0::shardok::common::PlayerInfo>;
|
||||
auto EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit>;
|
||||
auto ResolvedPlayerInfos() -> std::vector<net::eagle0::shardok::common::PlayerInfo>;
|
||||
auto EndGameUnits() -> std::vector<net::eagle0::shardok::storage::ResolvedUnit>;
|
||||
|
||||
[[nodiscard]] auto GetGameId() const -> GameId { return cachedGameId; }
|
||||
|
||||
[[nodiscard]] auto GetLogFilePath() const -> string { return logFilePath; }
|
||||
[[nodiscard]] auto GetLogFilePath() const -> std::string { return logFilePath; }
|
||||
|
||||
[[nodiscard]] auto HasAIThreadError() const -> bool { return aiThreadFailed.load(); }
|
||||
[[nodiscard]] auto GetAIThreadError() const -> std::string { return aiThreadErrorMessage; }
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "AvailableCommandsFactory.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
|
||||
@@ -31,23 +32,21 @@ private:
|
||||
static auto CannotBecomeOutlaw(const GameStateW &gameState, PlayerId pid) -> bool {
|
||||
if (pid == UNCONTROLLED_PLAYER_ID) return false;
|
||||
|
||||
const auto &player = std::find_if(
|
||||
begin(*gameState->player_infos()),
|
||||
end(*gameState->player_infos()),
|
||||
const auto &player = std::ranges::find_if(
|
||||
*gameState->player_infos(),
|
||||
[pid](const PlayerInfoFb *pi) { return pi->player_id() == pid; });
|
||||
|
||||
return (player != end(*gameState->player_infos()) && player->cannot_become_outlaw());
|
||||
return (player != std::end(*gameState->player_infos()) && player->cannot_become_outlaw());
|
||||
}
|
||||
|
||||
static auto IsAttacker(const GameStateW &gameState, PlayerId pid) -> bool {
|
||||
if (pid == UNCONTROLLED_PLAYER_ID) return false;
|
||||
|
||||
const auto &player = std::find_if(
|
||||
begin(*gameState->player_infos()),
|
||||
end(*gameState->player_infos()),
|
||||
const auto &player = std::ranges::find_if(
|
||||
*gameState->player_infos(),
|
||||
[pid](const PlayerInfoFb *pi) { return pi->player_id() == pid; });
|
||||
|
||||
return (player == end(*gameState->player_infos()) || !player->is_defender());
|
||||
return (player == std::end(*gameState->player_infos()) || !player->is_defender());
|
||||
}
|
||||
|
||||
const std::vector<std::shared_ptr<const CommandFactory>> commandFactories;
|
||||
@@ -116,6 +115,7 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
|
||||
const bool isEligibleCharger = unit->unit_id() == gameState->eligible_charger_id();
|
||||
std::vector<UnitId> possibleChargees{};
|
||||
possibleChargees.reserve(gameState->possible_chargee_ids()->size());
|
||||
for (const UnitId cid : *gameState->possible_chargee_ids()) {
|
||||
if (cid != -1) { possibleChargees.push_back(cid); }
|
||||
}
|
||||
@@ -194,8 +194,6 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> CommandListSPtr {
|
||||
CommandList commands{};
|
||||
|
||||
std::vector<AdjacentTile> adjacentTiles;
|
||||
|
||||
playerSetupCommandFactory
|
||||
.AddAvailablePlayerSetupCommands(commands, playerId, gameState, reinforcementPositions);
|
||||
if (!commands.empty()) return std::make_shared<CommandList>(commands);
|
||||
@@ -238,6 +236,7 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
/* onlyFollowUps=*/true);
|
||||
|
||||
std::unordered_set<CommandType> followUpTypes{};
|
||||
followUpTypes.reserve(followUpCommands.size());
|
||||
for (const auto &followUpCmd : followUpCommands) {
|
||||
if (followUpCmd->CanBeFollowUp()) {
|
||||
followUpTypes.insert(followUpCmd->GetCommandType());
|
||||
|
||||
@@ -189,12 +189,13 @@ struct BattalionType {
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetCostToEnterTerrain(const TerrainProto &terrain) const -> ActionCost {
|
||||
if (HasBridge(terrain.modifier()))
|
||||
return ActionCost(
|
||||
ActionCost::standard,
|
||||
if (HasBridge(terrain.modifier()) && terrain.modifier().bridge().has_integrity() &&
|
||||
terrain.modifier().bridge().integrity().value() > 0.0) {
|
||||
return {ActionCost::standard,
|
||||
ActionPoints(std::ceil(
|
||||
100.0 * minimumCostToEnterBridge /
|
||||
terrain.modifier().bridge().integrity().value())));
|
||||
terrain.modifier().bridge().integrity().value()))};
|
||||
}
|
||||
if (IsFrozen(terrain.modifier())) return costToEnterIce;
|
||||
|
||||
auto terrainCost = GetCostToEnterTerrainType(terrain.type());
|
||||
@@ -208,11 +209,11 @@ struct BattalionType {
|
||||
[[nodiscard]] auto GetCostToEnterTerrain(
|
||||
const net::eagle0::shardok::storage::fb::Terrain_::Type terrainType,
|
||||
const net::eagle0::shardok::storage::fb::TileModifier &tm) const -> ActionCost {
|
||||
if (tm.bridge().present())
|
||||
return ActionCost(
|
||||
ActionCost::standard,
|
||||
if (tm.bridge().present() && tm.bridge().integrity() > 0.0) {
|
||||
return {ActionCost::standard,
|
||||
ActionPoints(
|
||||
std::ceil(100.0 * minimumCostToEnterBridge / tm.bridge().integrity())));
|
||||
std::ceil(100.0 * minimumCostToEnterBridge / tm.bridge().integrity()))};
|
||||
}
|
||||
if (tm.ice().present()) return costToEnterIce;
|
||||
|
||||
auto terrainCost = GetCostToEnterTerrainType(terrainType);
|
||||
@@ -261,13 +262,8 @@ struct BattalionType {
|
||||
trainingDifferenceMultiplierForStunChanceOnCharge * trainingDifference;
|
||||
OtherFactor trainingFactor =
|
||||
MakeOtherFactor(static_cast<int16_t>(trainingValue), "training difference");
|
||||
return MakeOdds(
|
||||
static_cast<int16_t>(baseStunChanceOnCharge),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
{},
|
||||
{trainingFactor});
|
||||
const std::array otherFactors = {trainingFactor};
|
||||
return MakeOdds(static_cast<int16_t>(baseStunChanceOnCharge), 0, 0, 0, {}, otherFactors);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetDailyFoodCostPerTroop() const -> double {
|
||||
|
||||
@@ -44,7 +44,7 @@ inline constexpr DamageType DamageType_aessence = DamageType::DamageType_aessenc
|
||||
inline constexpr DamageType DamageType_impact = DamageType::DamageType_impact;
|
||||
inline constexpr int NUM_DAMAGE_TYPES = static_cast<int>(DamageType::NUM_DAMAGE_TYPES);
|
||||
|
||||
static inline bool DamageTypeGetsTerrainDefense(const DamageType type) {
|
||||
[[nodiscard]] static inline bool DamageTypeGetsTerrainDefense(const DamageType type) {
|
||||
switch (type) {
|
||||
case DamageType_slashing:
|
||||
case DamageType_puncturing:
|
||||
@@ -132,11 +132,13 @@ public:
|
||||
return damageByType.at(PenetratingIndex(type));
|
||||
};
|
||||
|
||||
bool operator==(const CombatDamage& cmpr) const { return damageByType == cmpr.damageByType; }
|
||||
[[nodiscard]] bool operator==(const CombatDamage& cmpr) const {
|
||||
return damageByType == cmpr.damageByType;
|
||||
}
|
||||
|
||||
bool operator!=(const CombatDamage& cmpr) const { return !(*this == cmpr); }
|
||||
[[nodiscard]] bool operator!=(const CombatDamage& cmpr) const { return !(*this == cmpr); }
|
||||
|
||||
bool operator<=(const CombatDamage& cmpr) const {
|
||||
[[nodiscard]] bool operator<=(const CombatDamage& cmpr) const {
|
||||
return std::ranges::equal(
|
||||
damageByType,
|
||||
cmpr.damageByType,
|
||||
@@ -145,7 +147,7 @@ public:
|
||||
});
|
||||
}
|
||||
|
||||
bool operator>=(const CombatDamage& cmpr) const {
|
||||
[[nodiscard]] bool operator>=(const CombatDamage& cmpr) const {
|
||||
return std::ranges::equal(
|
||||
damageByType,
|
||||
cmpr.damageByType,
|
||||
@@ -154,11 +156,15 @@ public:
|
||||
});
|
||||
}
|
||||
|
||||
bool operator>(const CombatDamage& cmpr) const { return ((*this >= cmpr) && !(*this == cmpr)); }
|
||||
[[nodiscard]] bool operator>(const CombatDamage& cmpr) const {
|
||||
return ((*this >= cmpr) && !(*this == cmpr));
|
||||
}
|
||||
|
||||
bool operator<(const CombatDamage& cmpr) const { return ((*this <= cmpr) && !(*this == cmpr)); }
|
||||
[[nodiscard]] bool operator<(const CombatDamage& cmpr) const {
|
||||
return ((*this <= cmpr) && !(*this == cmpr));
|
||||
}
|
||||
|
||||
CombatDamage operator+(const CombatDamage& add) const {
|
||||
[[nodiscard]] CombatDamage operator+(const CombatDamage& add) const {
|
||||
return Builder()
|
||||
.SetSlashing(
|
||||
GetNormalDamageOfType(DamageType_slashing) +
|
||||
@@ -219,7 +225,7 @@ public:
|
||||
.Build();
|
||||
}
|
||||
|
||||
CombatDamage operator*(const double factor) const {
|
||||
[[nodiscard]] CombatDamage operator*(const double factor) const {
|
||||
return Builder()
|
||||
.SetSlashing(GetNormalDamageOfType(DamageType_slashing) * factor)
|
||||
.SetPuncturing(GetNormalDamageOfType(DamageType_puncturing) * factor)
|
||||
@@ -442,7 +448,7 @@ public:
|
||||
|
||||
[[nodiscard]] auto ToBuilder() const -> Builder {
|
||||
Builder builder = Builder();
|
||||
for (int i = 0; i < static_cast<int>(NUM_DAMAGE_TYPES); i++) {
|
||||
for (int i = 0; i < static_cast<int>(NUM_DAMAGE_TYPES); ++i) {
|
||||
const auto type = static_cast<DamageType>(i);
|
||||
builder.SetDamageByType(type, false, this->GetNormalDamageOfType(type));
|
||||
builder.SetDamageByType(type, true, this->GetPenetratingDamageOfType(type));
|
||||
@@ -451,7 +457,9 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
inline CombatDamage operator*(const double factor, const CombatDamage& dmg) { return dmg * factor; }
|
||||
[[nodiscard]] inline CombatDamage operator*(const double factor, const CombatDamage& dmg) {
|
||||
return dmg * factor;
|
||||
}
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_LIBRARY_COMBAT_DAMAGE_HPP
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "FireUtils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/TileModifier.hpp"
|
||||
|
||||
@@ -103,14 +104,14 @@ auto GetExtinguishOdds(
|
||||
|
||||
const int16_t windFactor = -weather->wind().speed_in_mph();
|
||||
|
||||
std::vector<OtherFactor> others;
|
||||
const std::vector<double> stats = {agility, agility, intelligence};
|
||||
if (ownTile) {
|
||||
others.push_back(MakeOtherFactor(settings.Backing().extinguish_own_tile_buf(), "ownTile"));
|
||||
const std::array others = {
|
||||
MakeOtherFactor(settings.Backing().extinguish_own_tile_buf(), "ownTile")};
|
||||
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, others);
|
||||
}
|
||||
|
||||
const std::vector<double> stats = {agility, agility, intelligence};
|
||||
|
||||
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, others);
|
||||
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats);
|
||||
}
|
||||
|
||||
auto GetStartFireOdds(
|
||||
@@ -128,12 +129,13 @@ auto GetStartFireOdds(
|
||||
|
||||
const int16_t windFactor = -currentWeather.wind().speed_in_mph();
|
||||
|
||||
std::vector<OtherFactor> otherFactors;
|
||||
if (isMage) { otherFactors.push_back(MakeOtherFactor(+20, "mage")); }
|
||||
|
||||
const std::vector<double> stats = {agility, intelligence, intelligence};
|
||||
if (isMage) {
|
||||
const std::array otherFactors = {MakeOtherFactor(+20, "mage")};
|
||||
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, otherFactors);
|
||||
}
|
||||
|
||||
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, otherFactors);
|
||||
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -21,19 +21,23 @@ using WeatherFb = net::eagle0::shardok::storage::fb::Weather;
|
||||
using TerrainProto = net::eagle0::shardok::common::Terrain;
|
||||
using Terrain = net::eagle0::shardok::storage::fb::Terrain;
|
||||
|
||||
auto PropensityByTerrain(const Terrain *terrain, const SettingsGetter &settings) -> int;
|
||||
[[nodiscard]] auto PropensityByTerrain(const Terrain *terrain, const SettingsGetter &settings)
|
||||
-> int;
|
||||
|
||||
auto PropensityByTerrain(const TerrainProto &terrain, const SettingsGetter &settings) -> int;
|
||||
[[nodiscard]] auto PropensityByTerrain(const TerrainProto &terrain, const SettingsGetter &settings)
|
||||
-> int;
|
||||
|
||||
auto PropensityByTerrain(
|
||||
[[nodiscard]] auto PropensityByTerrain(
|
||||
const TerrainType &terrainType,
|
||||
const TileModifierProto &modifier,
|
||||
const SettingsGetter &settings) -> int;
|
||||
|
||||
auto GetFireDamage(int troops, double openEndedPercentileRoll1, double openEndedPercentileRoll2)
|
||||
-> CombatDamage;
|
||||
[[nodiscard]] auto GetFireDamage(
|
||||
int troops,
|
||||
double openEndedPercentileRoll1,
|
||||
double openEndedPercentileRoll2) -> CombatDamage;
|
||||
|
||||
auto GetExtinguishOdds(
|
||||
[[nodiscard]] auto GetExtinguishOdds(
|
||||
const SettingsGetter &settings,
|
||||
const Terrain *terrain,
|
||||
const WeatherFb *currentWeather,
|
||||
@@ -41,7 +45,7 @@ auto GetExtinguishOdds(
|
||||
double intelligence,
|
||||
bool ownTile) -> PercentileRollOdds;
|
||||
|
||||
auto GetStartFireOdds(
|
||||
[[nodiscard]] auto GetStartFireOdds(
|
||||
const SettingsGetter &settings,
|
||||
const Terrain &terrain,
|
||||
const WeatherFb ¤tWeather,
|
||||
|
||||
@@ -36,25 +36,19 @@ public:
|
||||
using BaseType::BaseType;
|
||||
|
||||
// Default constructor
|
||||
GameStateW() : BaseType() {}
|
||||
GameStateW() = default;
|
||||
|
||||
// Copy constructor
|
||||
GameStateW(const GameStateW& other) : BaseType(other) {}
|
||||
GameStateW(const GameStateW& other) = default;
|
||||
|
||||
// Move constructor
|
||||
GameStateW(GameStateW&& other) noexcept : BaseType(std::move(other)) {}
|
||||
GameStateW(GameStateW&& other) noexcept = default;
|
||||
|
||||
// Copy assignment
|
||||
GameStateW& operator=(const GameStateW& other) {
|
||||
BaseType::operator=(other);
|
||||
return *this;
|
||||
}
|
||||
GameStateW& operator=(const GameStateW& other) = default;
|
||||
|
||||
// Move assignment
|
||||
GameStateW& operator=(GameStateW&& other) noexcept {
|
||||
BaseType::operator=(std::move(other));
|
||||
return *this;
|
||||
}
|
||||
GameStateW& operator=(GameStateW&& other) noexcept = default;
|
||||
|
||||
// Constructor from base type
|
||||
GameStateW(const BaseType& base) : BaseType(base) {}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
|
||||
namespace shardok {
|
||||
double BonusFromStat(const double stat) { return pow(((double)stat - 50.0), 3.0) / 5000.0; }
|
||||
double BonusFromStat(const double stat) { return pow(stat - 50.0, 3.0) / 5000.0; }
|
||||
|
||||
int16_t AverageBonusFromStats(const std::vector<double> &stats) {
|
||||
double totalBonus = 0.0;
|
||||
@@ -23,7 +23,7 @@ int16_t AverageBonusFromStats(const std::vector<double> &stats) {
|
||||
}
|
||||
|
||||
int16_t GetSuccessChance(const PercentileRollOdds &odds) {
|
||||
return int16_t(std::floor(
|
||||
return static_cast<int16_t>(std::floor(
|
||||
100.0 * RandomGenerator::ChanceOpenEndedPercentileAtOrAbove(100 - GetTotalOdds(odds)) +
|
||||
0.5));
|
||||
}
|
||||
@@ -42,7 +42,7 @@ PercentileRollOdds MakeOdds(
|
||||
const int weath,
|
||||
const int wind,
|
||||
const int statF,
|
||||
const std::vector<OtherFactor> &others) {
|
||||
const std::span<const OtherFactor> others) {
|
||||
PercentileRollOdds odds;
|
||||
|
||||
odds.set_base_difficulty(base);
|
||||
@@ -61,7 +61,7 @@ PercentileRollOdds MakeOdds(
|
||||
const int weath,
|
||||
const int wind,
|
||||
const std::vector<double> &stats,
|
||||
const std::vector<OtherFactor> &others) {
|
||||
const std::span<const OtherFactor> others) {
|
||||
return MakeOdds(base, terr, weath, wind, AverageBonusFromStats(stats), others);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef EAGLE0_SHARDOK_LIBRARY_PERCENTILE_ROLL_ODDS_HPP
|
||||
#define EAGLE0_SHARDOK_LIBRARY_PERCENTILE_ROLL_ODDS_HPP
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -21,33 +22,33 @@ namespace shardok {
|
||||
using PercentileRollOdds = net::eagle0::shardok::storage::Odds;
|
||||
using OtherFactor = net::eagle0::shardok::storage::Odds_OtherFactor;
|
||||
|
||||
double BonusFromStat(double stat);
|
||||
[[nodiscard]] double BonusFromStat(double stat);
|
||||
|
||||
int16_t AverageBonusFromStats(const std::vector<double> &stats);
|
||||
[[nodiscard]] int16_t AverageBonusFromStats(const std::vector<double> &stats);
|
||||
|
||||
OtherFactor MakeOtherFactor(int value, const std::string &id);
|
||||
[[nodiscard]] OtherFactor MakeOtherFactor(int value, const std::string &id);
|
||||
|
||||
PercentileRollOdds MakeOdds(
|
||||
[[nodiscard]] PercentileRollOdds MakeOdds(
|
||||
int base = 0,
|
||||
int terr = 0,
|
||||
int weath = 0,
|
||||
int wind = 0,
|
||||
int statF = 0,
|
||||
const std::vector<OtherFactor> &others = std::vector<OtherFactor>());
|
||||
std::span<const OtherFactor> others = {});
|
||||
|
||||
PercentileRollOdds MakeOdds(
|
||||
[[nodiscard]] PercentileRollOdds MakeOdds(
|
||||
int base,
|
||||
int terr,
|
||||
int weath,
|
||||
int wind,
|
||||
const std::vector<double> &stats,
|
||||
const std::vector<OtherFactor> &others = std::vector<OtherFactor>());
|
||||
std::span<const OtherFactor> others = {});
|
||||
|
||||
int16_t GetTotalOdds(const PercentileRollOdds &odds);
|
||||
[[nodiscard]] int16_t GetTotalOdds(const PercentileRollOdds &odds);
|
||||
|
||||
bool PercentileRollSucceeds(const PercentileRollOdds &odds, double roll);
|
||||
[[nodiscard]] bool PercentileRollSucceeds(const PercentileRollOdds &odds, double roll);
|
||||
|
||||
int16_t GetSuccessChance(const PercentileRollOdds &odds);
|
||||
[[nodiscard]] int16_t GetSuccessChance(const PercentileRollOdds &odds);
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_LIBRARY_PERCENTILE_ROLL_ODDS_HPP
|
||||
|
||||
@@ -42,7 +42,7 @@ private:
|
||||
[[nodiscard]] virtual auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> std::vector<ActionResult> {
|
||||
return InternalExecuteWithRoll(currentState, generator, std::optional<int32_t>());
|
||||
return InternalExecuteWithRoll(currentState, generator, std::nullopt);
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual auto InternalExecuteWithRoll(
|
||||
|
||||
@@ -77,11 +77,11 @@ ShardokEngine::ShardokEngine(
|
||||
|
||||
ShardokEngine::ShardokEngine(
|
||||
const GameSettingsSPtr &settings,
|
||||
vector<Unit> &&unplacedUnorderedUnits,
|
||||
std::vector<Unit> &&unplacedUnorderedUnits,
|
||||
const HexMapProto &map,
|
||||
const int month,
|
||||
const GameId &existingGameId,
|
||||
const vector<PlayerInfoProto> &playerInfos,
|
||||
const std::vector<PlayerInfoProto> &playerInfos,
|
||||
const bool trackHistory)
|
||||
: gameSettings(settings),
|
||||
settingsGetter(settings->GetGetter()),
|
||||
@@ -106,7 +106,7 @@ ShardokEngine::ShardokEngine(
|
||||
ApplyAndAddActionResult(setupStartAction);
|
||||
}
|
||||
|
||||
void ShardokEngine::ApplyAndAddActionResults(const vector<ActionResultProto> &results) {
|
||||
void ShardokEngine::ApplyAndAddActionResults(const std::vector<ActionResultProto> &results) {
|
||||
for (const ActionResultProto &result : results) { ApplyAndAddActionResult(result); }
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ void ShardokEngine::ApplyAndAddActionResult(const ActionResultProto &result) {
|
||||
|
||||
ShardokEngine::ShardokEngine(
|
||||
const GameSettingsSPtr &settings,
|
||||
const vector<ShardokActionWithResultingState> &history,
|
||||
const std::vector<ShardokActionWithResultingState> &history,
|
||||
const bool trackHistory)
|
||||
: gameSettings(settings),
|
||||
settingsGetter(settings->GetGetter()),
|
||||
@@ -140,7 +140,12 @@ ShardokEngine::ShardokEngine(const ShardokEngine &toCopy, const bool trackHistor
|
||||
gameState(toCopy.gameState),
|
||||
trackHistory(trackHistory),
|
||||
startingHistoryCount(toCopy.startingHistoryCount),
|
||||
startingHistoryState(trackHistory ? toCopy.startingHistoryState : GameStateW{}),
|
||||
actionHistory(
|
||||
trackHistory ? toCopy.actionHistory : std::vector<ShardokActionWithResultingState>{}),
|
||||
criticalTileCoords(toCopy.criticalTileCoords),
|
||||
tutorialController_(toCopy.tutorialController_),
|
||||
pendingReinforcementPlacement_(toCopy.pendingReinforcementPlacement_),
|
||||
cachedAvailableCommands(toCopy.cachedAvailableCommands) {}
|
||||
|
||||
auto ShardokEngine::GetGameStateAtStartOfAction(const ActionId startingActionId) const
|
||||
@@ -156,11 +161,11 @@ auto ShardokEngine::GetGameStateAtStartOfAction(const ActionId startingActionId)
|
||||
}
|
||||
|
||||
auto ShardokEngine::GetGameHistory(const ActionId lastUpdatedActionId) const
|
||||
-> vector<ShardokActionWithResultingState> {
|
||||
-> std::vector<ShardokActionWithResultingState> {
|
||||
if (lastUpdatedActionId >= static_cast<int64_t>(actionHistory.size()) + startingHistoryCount) {
|
||||
return vector<ShardokActionWithResultingState>();
|
||||
return std::vector<ShardokActionWithResultingState>();
|
||||
}
|
||||
return vector<ShardokActionWithResultingState>(
|
||||
return std::vector<ShardokActionWithResultingState>(
|
||||
std::begin(actionHistory) + lastUpdatedActionId - startingHistoryCount,
|
||||
std::end(actionHistory));
|
||||
}
|
||||
@@ -172,14 +177,17 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
|
||||
|
||||
[[nodiscard]] auto ShardokEngine::FilterNewResults(
|
||||
PlayerId askingPlayer,
|
||||
int64_t previousActionCount) const -> vector<net::eagle0::shardok::api::ActionResultView> {
|
||||
int64_t previousActionCount) const
|
||||
-> std::vector<net::eagle0::shardok::api::ActionResultView> {
|
||||
if (previousActionCount < startingHistoryCount) {
|
||||
throw ShardokInternalErrorException("Unable to fetch history before startingHistoryCount");
|
||||
}
|
||||
|
||||
vector<ShardokActionWithResultingState> unfilteredHistory = GetGameHistory(previousActionCount);
|
||||
std::vector<ShardokActionWithResultingState> unfilteredHistory =
|
||||
GetGameHistory(previousActionCount);
|
||||
|
||||
vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
|
||||
std::vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
|
||||
filteredHistory.reserve(unfilteredHistory.size());
|
||||
|
||||
GameStateW startingState = (previousActionCount == 0)
|
||||
? GameStateW{}
|
||||
@@ -193,9 +201,7 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
|
||||
GameState *previousStatePtr = nullptr;
|
||||
GameStateView &previousView = startingView;
|
||||
|
||||
for (vector<ShardokActionWithResultingState> newHistory =
|
||||
{unfilteredHistory.begin(), unfilteredHistory.end()};
|
||||
const ShardokActionWithResultingState &awrs : newHistory) {
|
||||
for (const ShardokActionWithResultingState &awrs : unfilteredHistory) {
|
||||
GameStateView viewAfter = GameStateFilteredForPlayer(
|
||||
settingsGetter,
|
||||
GameStateW::FromByteString(awrs.state_after_fb()),
|
||||
@@ -220,16 +226,18 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
|
||||
|
||||
[[nodiscard]] auto ShardokEngine::FilterNewResultsForWatcher(
|
||||
const PlayerId askingPlayer,
|
||||
const vector<PlayerId> &alliedPids,
|
||||
const std::vector<PlayerId> &alliedPids,
|
||||
const int64_t previousActionCount) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView> {
|
||||
-> std::vector<net::eagle0::shardok::api::ActionResultView> {
|
||||
if (previousActionCount < startingHistoryCount) {
|
||||
throw ShardokInternalErrorException("Unable to fetch history before startingHistoryCount");
|
||||
}
|
||||
|
||||
vector<ShardokActionWithResultingState> unfilteredHistory = GetGameHistory(previousActionCount);
|
||||
std::vector<ShardokActionWithResultingState> unfilteredHistory =
|
||||
GetGameHistory(previousActionCount);
|
||||
|
||||
vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
|
||||
std::vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
|
||||
filteredHistory.reserve(unfilteredHistory.size());
|
||||
|
||||
GameStateW startingState = (previousActionCount == 0)
|
||||
? GameStateW{}
|
||||
@@ -271,7 +279,7 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
|
||||
}
|
||||
|
||||
auto ShardokEngine::GetFilteredGameHistory(const PlayerId askingPlayer) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView> {
|
||||
-> std::vector<net::eagle0::shardok::api::ActionResultView> {
|
||||
return FilterNewResults(askingPlayer, 0);
|
||||
}
|
||||
|
||||
@@ -423,7 +431,7 @@ void ShardokEngine::ResumeAfterReinforcementPlacement(
|
||||
|
||||
void ShardokEngine::PostPlacementCommands(
|
||||
const PlayerId player,
|
||||
const vector<UnitPlacementInfo> &placementInfos,
|
||||
const std::vector<UnitPlacementInfo> &placementInfos,
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator) {
|
||||
if (GetCurrentPlayerId() != player) {
|
||||
throw ShardokClientErrorException("Posting for a player who's not the current player ID!");
|
||||
@@ -440,7 +448,7 @@ void ShardokEngine::PostPlacementCommands(
|
||||
reinforcementPositions);
|
||||
|
||||
// first make sure they're all valid and there are no duplicates
|
||||
for (size_t i = 0; i < placementInfos.size(); i++) {
|
||||
for (size_t i = 0; i < placementInfos.size(); ++i) {
|
||||
const UnitPlacementInfo &pi = placementInfos[i];
|
||||
|
||||
const auto it = std::ranges::find_if(*placementCommands, [pi](const CommandSPtr &cmd) {
|
||||
@@ -452,7 +460,7 @@ void ShardokEngine::PostPlacementCommands(
|
||||
}
|
||||
|
||||
// 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 (size_t j = i + 1; j < placementInfos.size(); ++j) {
|
||||
const UnitPlacementInfo &other = placementInfos[j];
|
||||
|
||||
if (pi.unitId == other.unitId)
|
||||
@@ -471,7 +479,8 @@ void ShardokEngine::PostPlacementCommands(
|
||||
return cmd->GetCommandProto().actor().value() == pi.unitId &&
|
||||
cmd->GetCommandProto().target() == pi.location;
|
||||
});
|
||||
for (vector<ActionResult> onePlacementResults = (*it)->Execute(gameState, randomGenerator);
|
||||
for (std::vector<ActionResult> onePlacementResults =
|
||||
(*it)->Execute(gameState, randomGenerator);
|
||||
const ActionResultProto &oneResult : onePlacementResults) {
|
||||
HandleActionResult(oneResult, randomGenerator);
|
||||
}
|
||||
@@ -564,7 +573,7 @@ void ShardokEngine::PostActionUnchecked(
|
||||
const CommandSPtr &command,
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator,
|
||||
const std::optional<int32_t> roll) {
|
||||
for (const vector<ActionResultProto> results =
|
||||
for (const std::vector<ActionResultProto> results =
|
||||
command->CanUseClientRoll()
|
||||
? command->ExecuteWithRoll(gameState, randomGenerator, roll)
|
||||
: command->Execute(gameState, randomGenerator);
|
||||
@@ -614,7 +623,7 @@ void ShardokEngine::HandleActionResult(
|
||||
auto ShardokEngine::HandleUnitFallingIntoWater(
|
||||
const Terrain *terrain,
|
||||
const Unit *unit,
|
||||
std::shared_ptr<RandomGenerator> randomGenerator) const -> vector<ActionResultProto> {
|
||||
std::shared_ptr<RandomGenerator> randomGenerator) const -> std::vector<ActionResultProto> {
|
||||
// Do nothing if this unit can travel across water
|
||||
if (const auto &battType = settingsGetter.GetBattalionType(unit->battalion().type());
|
||||
battType->GetCostToEnterTerrain(terrain).type != ActionCost::impossible) {
|
||||
@@ -629,8 +638,9 @@ auto ShardokEngine::HandleUnitFallingIntoWater(
|
||||
return fallAction.Execute(gameState, std::move(randomGenerator));
|
||||
}
|
||||
|
||||
auto CommandProtosFromActionList(const CommandList &actionList) -> vector<CommandProto> {
|
||||
vector<CommandProto> protos;
|
||||
auto CommandProtosFromActionList(const CommandList &actionList) -> std::vector<CommandProto> {
|
||||
std::vector<CommandProto> protos;
|
||||
protos.reserve(actionList.size());
|
||||
|
||||
std::transform(
|
||||
std::begin(actionList),
|
||||
@@ -641,7 +651,7 @@ auto CommandProtosFromActionList(const CommandList &actionList) -> vector<Comman
|
||||
return protos;
|
||||
}
|
||||
|
||||
auto ShardokEngine::GetPreviewCommands(const PlayerId playerId) const -> vector<CommandProto> {
|
||||
auto ShardokEngine::GetPreviewCommands(const PlayerId playerId) const -> std::vector<CommandProto> {
|
||||
if (GameIsOver(GetGameStatus())) {
|
||||
// The game is over
|
||||
return {};
|
||||
@@ -670,7 +680,7 @@ auto ShardokEngine::GetAvailableCommandsForAIPlayer(const PlayerId playerId) con
|
||||
}
|
||||
|
||||
auto ShardokEngine::GetAvailableCommandProtos(const PlayerId playerId, const bool includeFollowUps)
|
||||
const -> vector<CommandProto> {
|
||||
const -> std::vector<CommandProto> {
|
||||
if (GameIsOver(GetGameStatus())) {
|
||||
// The game is over
|
||||
return {};
|
||||
@@ -703,7 +713,8 @@ auto ShardokEngine::UncachedGetAvailableCommands(
|
||||
using SecuredByPlayerByUnitId = std::unordered_map<UnitId, PlayerId>;
|
||||
|
||||
auto SecuredByPlayerByUnitIdFromHistory(
|
||||
const vector<ShardokActionWithResultingState> &actionHistory) -> SecuredByPlayerByUnitId {
|
||||
const std::vector<ShardokActionWithResultingState> &actionHistory)
|
||||
-> SecuredByPlayerByUnitId {
|
||||
SecuredByPlayerByUnitId securedByPlayerByUnitId;
|
||||
for (const auto &actionWithResultingState : actionHistory) {
|
||||
for (const auto &resolvedUnit : actionWithResultingState.action_result().resolved_units()) {
|
||||
@@ -712,15 +723,15 @@ auto SecuredByPlayerByUnitIdFromHistory(
|
||||
continue;
|
||||
}
|
||||
if (!resolvedUnit.has_secured_by_player()) { continue; }
|
||||
const auto *unit = reinterpret_cast<const Unit *>(resolvedUnit.unit_bytes().data());
|
||||
securedByPlayerByUnitId[unit->unit_id()] = resolvedUnit.secured_by_player().value();
|
||||
const auto unit = UnitFromBytes(resolvedUnit.unit_bytes());
|
||||
securedByPlayerByUnitId[unit.unit_id()] = resolvedUnit.secured_by_player().value();
|
||||
}
|
||||
}
|
||||
return securedByPlayerByUnitId;
|
||||
}
|
||||
|
||||
void AddUnits(
|
||||
vector<net::eagle0::shardok::storage::ResolvedUnit> &to,
|
||||
std::vector<net::eagle0::shardok::storage::ResolvedUnit> &to,
|
||||
const Units &from,
|
||||
const SecuredByPlayerByUnitId &securedByPlayerByUnitId) {
|
||||
for (const auto &unit : from) {
|
||||
@@ -769,11 +780,12 @@ void AddUnits(
|
||||
}
|
||||
AddUnitToResolved(ru, *unit);
|
||||
|
||||
to.push_back(ru);
|
||||
to.push_back(std::move(ru));
|
||||
}
|
||||
}
|
||||
|
||||
auto ShardokEngine::EndGameUnits() const -> vector<net::eagle0::shardok::storage::ResolvedUnit> {
|
||||
auto ShardokEngine::EndGameUnits() const
|
||||
-> std::vector<net::eagle0::shardok::storage::ResolvedUnit> {
|
||||
if (const auto state = GetGameStatus()->state();
|
||||
state != net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
|
||||
throw ShardokInternalErrorException(
|
||||
@@ -782,7 +794,8 @@ auto ShardokEngine::EndGameUnits() const -> vector<net::eagle0::shardok::storage
|
||||
|
||||
const auto &gs = GetCurrentGameState();
|
||||
|
||||
vector<net::eagle0::shardok::storage::ResolvedUnit> endgameUnits;
|
||||
std::vector<net::eagle0::shardok::storage::ResolvedUnit> endgameUnits;
|
||||
endgameUnits.reserve(gs->units()->size());
|
||||
AddUnits(endgameUnits, *gs->units(), SecuredByPlayerByUnitIdFromHistory(actionHistory));
|
||||
|
||||
return endgameUnits;
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "ShardokAction.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
@@ -27,12 +29,6 @@
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/action_with_resulting_state.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
using std::begin;
|
||||
using std::end;
|
||||
using std::optional;
|
||||
using std::shared_ptr;
|
||||
using std::vector;
|
||||
|
||||
using net::eagle0::shardok::api::UnitView;
|
||||
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
|
||||
using net::eagle0::shardok::storage::ShardokActionWithResultingState;
|
||||
@@ -43,14 +39,14 @@ class ShardokEngine {
|
||||
private:
|
||||
const GameSettingsSPtr gameSettings;
|
||||
const SettingsGetter settingsGetter;
|
||||
const shared_ptr<AvailableCommandsFactory> availableCommandsFactory;
|
||||
const std::shared_ptr<AvailableCommandsFactory> availableCommandsFactory;
|
||||
|
||||
GameStateW gameState;
|
||||
|
||||
const bool trackHistory;
|
||||
const int32_t startingHistoryCount = 0;
|
||||
const GameStateW startingHistoryState;
|
||||
mutable vector<ShardokActionWithResultingState> actionHistory;
|
||||
mutable std::vector<ShardokActionWithResultingState> actionHistory;
|
||||
|
||||
const CoordsSet criticalTileCoords;
|
||||
|
||||
@@ -61,12 +57,12 @@ private:
|
||||
|
||||
void ApplyAndAddActionResult(const ActionResult &result);
|
||||
|
||||
void ApplyAndAddActionResults(const vector<ActionResult> &results);
|
||||
void ApplyAndAddActionResults(const std::vector<ActionResult> &results);
|
||||
|
||||
[[nodiscard]] auto HandleUnitFallingIntoWater(
|
||||
const Terrain *terrain,
|
||||
const fb::Unit *unit,
|
||||
std::shared_ptr<RandomGenerator> randomGenerator) const -> vector<ActionResult>;
|
||||
std::shared_ptr<RandomGenerator> randomGenerator) const -> std::vector<ActionResult>;
|
||||
|
||||
void HandleActionResult(
|
||||
const ActionResult &actionResult,
|
||||
@@ -105,17 +101,17 @@ public:
|
||||
// Constructor for a brand new game
|
||||
ShardokEngine(
|
||||
const GameSettingsSPtr &settings,
|
||||
vector<Unit> &&unplacedUnorderedUnits,
|
||||
std::vector<Unit> &&unplacedUnorderedUnits,
|
||||
const HexMapProto &map,
|
||||
int month,
|
||||
const GameId &existingGameId,
|
||||
const vector<PlayerInfoProto> &playerInfos,
|
||||
const std::vector<PlayerInfoProto> &playerInfos,
|
||||
bool trackHistory = true);
|
||||
|
||||
// Constructor for a game with history
|
||||
ShardokEngine(
|
||||
const GameSettingsSPtr &settings,
|
||||
const vector<ShardokActionWithResultingState> &history,
|
||||
const std::vector<ShardokActionWithResultingState> &history,
|
||||
bool trackHistory = true);
|
||||
|
||||
// Copy constructor
|
||||
@@ -134,12 +130,12 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] auto EndGameUnits() const
|
||||
-> vector<net::eagle0::shardok::storage::ResolvedUnit>; // Throws if the game is not
|
||||
// over
|
||||
-> std::vector<net::eagle0::shardok::storage::ResolvedUnit>; // Throws if the game is
|
||||
// not over
|
||||
|
||||
// Controller API
|
||||
[[nodiscard]] auto GetGameHistory(ActionId lastUpdatedActionId) const
|
||||
-> vector<ShardokActionWithResultingState>;
|
||||
-> std::vector<ShardokActionWithResultingState>;
|
||||
|
||||
[[nodiscard]] auto GetUnfilteredHistoryCount() const -> size_t {
|
||||
return actionHistory.size() + startingHistoryCount;
|
||||
@@ -150,7 +146,7 @@ public:
|
||||
-> net::eagle0::shardok::api::GameStateView;
|
||||
|
||||
[[nodiscard]] auto FilterNewResults(PlayerId askingPlayer, int64_t previousActionCount) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
-> std::vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
|
||||
// Variant for non-participant watchers: askingPlayer is a sentinel (not in
|
||||
// player_infos) and alliedPids names the Shardok pids whose units the
|
||||
@@ -158,18 +154,18 @@ public:
|
||||
// and hidden actions.
|
||||
[[nodiscard]] auto FilterNewResultsForWatcher(
|
||||
PlayerId askingPlayer,
|
||||
const vector<PlayerId> &alliedPids,
|
||||
const std::vector<PlayerId> &alliedPids,
|
||||
int64_t previousActionCount) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
-> std::vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
|
||||
[[nodiscard]] auto GetFilteredGameHistory(PlayerId askingPlayer) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
-> std::vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
|
||||
[[nodiscard]] auto GetUnitById(PlayerId askingPlayer, UnitId unitId) const -> UnitView;
|
||||
|
||||
void PostPlacementCommands(
|
||||
PlayerId player,
|
||||
const vector<UnitPlacementInfo> &placementInfos,
|
||||
const std::vector<UnitPlacementInfo> &placementInfos,
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator = nullptr);
|
||||
void PostFinishedPlacementCommand(
|
||||
PlayerId player,
|
||||
@@ -186,11 +182,11 @@ public:
|
||||
PlayerId playerId,
|
||||
std::shared_ptr<RandomGenerator> randomGenerator) -> int;
|
||||
|
||||
[[nodiscard]] auto GetPreviewCommands(PlayerId playerId) const -> vector<CommandProto>;
|
||||
[[nodiscard]] auto GetPreviewCommands(PlayerId playerId) const -> std::vector<CommandProto>;
|
||||
|
||||
[[nodiscard]] auto GetAvailableCommandsForAIPlayer(PlayerId playerId) const -> CommandListSPtr;
|
||||
[[nodiscard]] auto GetAvailableCommandProtos(PlayerId playerId, bool includeFollowUps) const
|
||||
-> vector<CommandProto>;
|
||||
-> std::vector<CommandProto>;
|
||||
|
||||
[[nodiscard]] auto GetGameId() const -> GameId {
|
||||
return GetCurrentGameState()->game_id()->str();
|
||||
@@ -198,9 +194,9 @@ public:
|
||||
|
||||
[[nodiscard]] auto GetMonth() const -> int { return GetCurrentGameState()->month(); }
|
||||
|
||||
[[nodiscard]] auto GetPlayerInfos() const -> vector<PlayerInfoProto> {
|
||||
[[nodiscard]] auto GetPlayerInfos() const -> std::vector<PlayerInfoProto> {
|
||||
const auto ¤tGameState = GetCurrentGameState();
|
||||
vector<PlayerInfoProto> protos{};
|
||||
std::vector<PlayerInfoProto> protos{};
|
||||
for (const auto *const piFB : *currentGameState->player_infos()) {
|
||||
protos.push_back(fb::ToPlayerInfoProto(piFB));
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ auto PlayerSetupCommandFactory::AddAvailablePlayerSetupCommands(
|
||||
const auto unplacedUnits = ReserveUnitsForPlayer(gameState->units(), playerId);
|
||||
if (unplacedUnits.empty()) return;
|
||||
|
||||
existingCommands.reserve(
|
||||
existingCommands.size() + (unplacedUnits.size() * reinforcementPositions->size()));
|
||||
for (const auto &[unitId, unit] : unplacedUnits) {
|
||||
for (const Coords &position : *reinforcementPositions) {
|
||||
if (!gameState.GetOccupant(position)) {
|
||||
|
||||
+7
-7
@@ -29,8 +29,9 @@ auto ActionPointDistances::BraveWaterPossibleCoords(const HexMap* hexMap) const
|
||||
std::shared_ptr<BraveableTileInfo> info = std::make_shared<BraveableTileInfo>(hexMap);
|
||||
CoordsSet& braveWaterPossibleCoords = info->cs;
|
||||
const int indexCount = hexMap->row_count() * hexMap->column_count();
|
||||
info->details.reserve(indexCount);
|
||||
const auto indexToCoords = CreateIndexToCoords(hexMap);
|
||||
for (int i = 0; i < indexCount; i++) {
|
||||
for (int i = 0; i < indexCount; ++i) {
|
||||
if (const Terrain* terrain = hexMap->terrain()->Get(i);
|
||||
IsWater(terrain->type()) && !terrain->modifier().bridge().present() &&
|
||||
!terrain->modifier().ice().present()) {
|
||||
@@ -70,7 +71,7 @@ auto ActionPointDistances::CreateIndexToCoords(const HexMap* hexMap) -> std::vec
|
||||
const int indexCount = hexMap->row_count() * columnCount;
|
||||
std::vector<Coords> indexToCoords;
|
||||
indexToCoords.reserve(indexCount);
|
||||
for (int i = 0; i < indexCount; i++) {
|
||||
for (int i = 0; i < indexCount; ++i) {
|
||||
indexToCoords.emplace_back(
|
||||
static_cast<int8_t>(i / columnCount),
|
||||
static_cast<int8_t>(i % columnCount));
|
||||
@@ -84,7 +85,7 @@ auto ActionPointDistances::CreateAdjacencyTable(const HexMap* hexMap)
|
||||
const int indexCount = hexMap->row_count() * columnCount;
|
||||
std::vector<std::array<int, 6>> adjacencyTable;
|
||||
adjacencyTable.reserve(indexCount);
|
||||
for (int i = 0; i < indexCount; i++) {
|
||||
for (int i = 0; i < indexCount; ++i) {
|
||||
const Coords coords(
|
||||
static_cast<int8_t>(i / columnCount),
|
||||
static_cast<int8_t>(i % columnCount));
|
||||
@@ -118,7 +119,7 @@ void ActionPointDistances::PopulateOne(
|
||||
|
||||
// Find starting index (the one with distance 0)
|
||||
const int distanceCount = static_cast<int>(ds.size());
|
||||
for (int i = 0; i < distanceCount; i++) {
|
||||
for (int i = 0; i < distanceCount; ++i) {
|
||||
if (ds[i] == 0) {
|
||||
pq.emplace(0, i);
|
||||
break;
|
||||
@@ -140,7 +141,7 @@ void ActionPointDistances::PopulateOne(
|
||||
|
||||
// Prefetch terrain data for all neighbors to reduce memory stalls
|
||||
const std::array<int, 6>& neighbors = adjacencyTable[currentIndex];
|
||||
for (int i = 0; i < 6 && neighbors[i] != -1; i++) {
|
||||
for (int i = 0; i < 6 && neighbors[i] != -1; ++i) {
|
||||
__builtin_prefetch(hexMap->terrain()->Get(neighbors[i]), 0, 3);
|
||||
}
|
||||
|
||||
@@ -239,11 +240,10 @@ OnDemandActionPointDistances::OnDemandActionPointDistances(
|
||||
battalionType(std::move(battTp)),
|
||||
distances(static_cast<size_t>(map->row_count() * map->column_count())) {
|
||||
const int indexCount = map->row_count() * map->column_count();
|
||||
distances.resize(indexCount);
|
||||
|
||||
auto braveWaterPossibleCoords = includeBravingWater ? BraveWaterPossibleCoords(map) : nullptr;
|
||||
|
||||
for (int fromIndex = 0; fromIndex < indexCount; fromIndex++) {
|
||||
for (int fromIndex = 0; fromIndex < indexCount; ++fromIndex) {
|
||||
distances[fromIndex] = std::async(
|
||||
std::launch::deferred,
|
||||
&OnDemandActionPointDistances::GenerateDistances,
|
||||
|
||||
+10
-8
@@ -43,7 +43,7 @@ protected:
|
||||
const std::shared_ptr<BraveableTileInfo> &braveWaterPossibleCoords,
|
||||
const std::vector<Coords> &indexToCoords,
|
||||
const std::vector<std::array<int, 6>> &adjacencyTable);
|
||||
static auto GenerateDistances(
|
||||
[[nodiscard]] static auto GenerateDistances(
|
||||
int fromIndex,
|
||||
const HexMap *hexMap,
|
||||
bool includeBravingWater,
|
||||
@@ -51,13 +51,15 @@ protected:
|
||||
const BattalionTypeSPtr &battalionType,
|
||||
const std::shared_ptr<BraveableTileInfo> &braveWaterPossibleCoords)
|
||||
-> std::vector<DIST_T>;
|
||||
auto BraveWaterPossibleCoords(const HexMap *hexMap) const -> std::shared_ptr<BraveableTileInfo>;
|
||||
[[nodiscard]] auto BraveWaterPossibleCoords(const HexMap *hexMap) const
|
||||
-> std::shared_ptr<BraveableTileInfo>;
|
||||
|
||||
// Create coordinate lookup table for efficient index->coords conversion
|
||||
static auto CreateIndexToCoords(const HexMap *hexMap) -> std::vector<Coords>;
|
||||
[[nodiscard]] static auto CreateIndexToCoords(const HexMap *hexMap) -> std::vector<Coords>;
|
||||
|
||||
// Create adjacency lookup table for efficient neighbor access
|
||||
static auto CreateAdjacencyTable(const HexMap *hexMap) -> std::vector<std::array<int, 6>>;
|
||||
[[nodiscard]] static auto CreateAdjacencyTable(const HexMap *hexMap)
|
||||
-> std::vector<std::array<int, 6>>;
|
||||
|
||||
[[nodiscard]] auto ToIndex(const Coords &coords) const -> int {
|
||||
return coords.row() * column_count + coords.column();
|
||||
@@ -70,9 +72,9 @@ public:
|
||||
|
||||
virtual ~ActionPointDistances() = default;
|
||||
|
||||
virtual auto Distance(int fromIndex, int toIndex) const -> DIST_T = 0;
|
||||
[[nodiscard]] virtual auto Distance(int fromIndex, int toIndex) const -> DIST_T = 0;
|
||||
|
||||
virtual auto Distance(const Coords &from, const Coords &to) const -> DIST_T = 0;
|
||||
[[nodiscard]] virtual auto Distance(const Coords &from, const Coords &to) const -> DIST_T = 0;
|
||||
};
|
||||
|
||||
class OnDemandActionPointDistances final : public ActionPointDistances {
|
||||
@@ -91,11 +93,11 @@ public:
|
||||
|
||||
~OnDemandActionPointDistances() override = default;
|
||||
|
||||
auto Distance(const int fromIndex, const int toIndex) const -> int16_t override {
|
||||
[[nodiscard]] auto Distance(const int fromIndex, const int toIndex) const -> int16_t override {
|
||||
return distances[fromIndex].get()[toIndex];
|
||||
}
|
||||
|
||||
auto Distance(const Coords &from, const Coords &to) const -> int16_t override {
|
||||
[[nodiscard]] auto Distance(const Coords &from, const Coords &to) const -> int16_t override {
|
||||
return Distance(ToIndex(from), ToIndex(to));
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ static auto CreateIceClearedMap(const HexMap* map) -> fb::HexMapW {
|
||||
auto* mutableMap = mapCopy.Get();
|
||||
auto* terrainVec = mutableMap->mutable_terrain();
|
||||
|
||||
for (size_t i = 0; i < terrainVec->size(); i++) {
|
||||
for (size_t i = 0; i < terrainVec->size(); ++i) {
|
||||
// Only process tiles with ice
|
||||
// const_cast is safe here because we own the mutable buffer (mapCopy)
|
||||
if (auto* terrain = const_cast<Terrain*>(terrainVec->GetMutableObject(i));
|
||||
|
||||
+6
-6
@@ -22,7 +22,7 @@ struct MapId {
|
||||
uint64_t terrainTypesId;
|
||||
uint64_t modifierId;
|
||||
|
||||
auto operator==(const MapId& other) const -> bool {
|
||||
[[nodiscard]] auto operator==(const MapId& other) const -> bool {
|
||||
return terrainTypesId == other.terrainTypesId && modifierId == other.modifierId;
|
||||
}
|
||||
};
|
||||
@@ -34,7 +34,7 @@ struct FullCacheKey {
|
||||
bool includeBravingWater;
|
||||
int braveWaterCost;
|
||||
|
||||
bool operator==(const FullCacheKey& other) const {
|
||||
[[nodiscard]] bool operator==(const FullCacheKey& other) const {
|
||||
return mapId == other.mapId && battalionTypeId == other.battalionTypeId &&
|
||||
includeBravingWater == other.includeBravingWater &&
|
||||
braveWaterCost == other.braveWaterCost;
|
||||
@@ -43,7 +43,7 @@ struct FullCacheKey {
|
||||
|
||||
// Hash function for FullCacheKey
|
||||
struct FullCacheKeyHash {
|
||||
size_t operator()(const FullCacheKey& key) const {
|
||||
[[nodiscard]] 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) |
|
||||
@@ -85,7 +85,7 @@ private:
|
||||
// Epoch system removed - TLS cache uses size-based eviction instead
|
||||
|
||||
// Helper to build cache key
|
||||
static auto MakeCacheKey(
|
||||
[[nodiscard]] static auto MakeCacheKey(
|
||||
const MapId& mapId,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
bool includeBravingWater,
|
||||
@@ -107,7 +107,7 @@ public:
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost = -1) -> const ActionPointDistances*;
|
||||
|
||||
static auto GetMapId(const HexMap* map) -> MapId;
|
||||
[[nodiscard]] 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
|
||||
@@ -116,7 +116,7 @@ public:
|
||||
|
||||
// Cache management methods
|
||||
static void ClearThreadLocalCache();
|
||||
static size_t GetThreadLocalCacheSize();
|
||||
[[nodiscard]] static size_t GetThreadLocalCacheSize();
|
||||
};
|
||||
|
||||
using APDCache = std::shared_ptr<ActionPointDistancesCache>;
|
||||
|
||||
+5
-5
@@ -12,7 +12,7 @@
|
||||
// Dynamic thread count based on hardware capabilities
|
||||
static const int ASYNC_COUNT = []() {
|
||||
const int cores = static_cast<int>(std::thread::hardware_concurrency());
|
||||
// Use cores-2 to leave room for OS and other processes, minimum 4 threads
|
||||
// Use cores-4 to leave room for OS and other processes, minimum 4 threads
|
||||
const int threadCount = std::max(4, cores - 4);
|
||||
std::cerr << "ActionPointDistances using " << threadCount << " threads (detected " << cores
|
||||
<< " cores)\n";
|
||||
@@ -35,20 +35,20 @@ auto FixedActionPointDistances::Create(
|
||||
|
||||
const int indexCount = map->row_count() * map->column_count();
|
||||
|
||||
vector<std::future<vector<vector<DIST_T>>>> futures(indexCount);
|
||||
vector<std::future<vector<vector<DIST_T>>>> futures(ASYNC_COUNT);
|
||||
|
||||
auto braveWaterPossibleCoords =
|
||||
includeBravingWater ? apd->BraveWaterPossibleCoords(map) : nullptr;
|
||||
|
||||
int chunkSize = (indexCount + ASYNC_COUNT - 1) / ASYNC_COUNT;
|
||||
// Break into chunks for async
|
||||
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
|
||||
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; ++chunkIdx) {
|
||||
futures[chunkIdx] = std::async(std::launch::async, [=]() -> vector<vector<DIST_T>> {
|
||||
vector<vector<DIST_T>> chunkVec;
|
||||
chunkVec.reserve(chunkSize);
|
||||
const int chunkStartIndex = chunkIdx * chunkSize;
|
||||
|
||||
for (int i = 0; i < chunkSize; i++) {
|
||||
for (int i = 0; i < chunkSize; ++i) {
|
||||
const auto fromIndex = chunkStartIndex + i;
|
||||
if (fromIndex >= indexCount) { continue; }
|
||||
chunkVec.push_back(ActionPointDistances::GenerateDistances(
|
||||
@@ -65,7 +65,7 @@ auto FixedActionPointDistances::Create(
|
||||
|
||||
apd->distances.reserve(indexCount);
|
||||
|
||||
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
|
||||
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; ++chunkIdx) {
|
||||
auto resultsVec = futures[chunkIdx].get();
|
||||
apd->distances.insert(apd->distances.end(), resultsVec.begin(), resultsVec.end());
|
||||
}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ private:
|
||||
|
||||
public:
|
||||
// Factory method to create FixedActionPointDistances
|
||||
static auto Create(
|
||||
[[nodiscard]] static auto Create(
|
||||
const HexMap *map,
|
||||
const BattalionTypeSPtr &battalionType,
|
||||
bool includeBravingWater,
|
||||
|
||||
+30
-35
@@ -14,6 +14,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/UnitHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
@@ -30,10 +31,6 @@ inline auto GetMutableUnit(GameState *state, UnitId unitId) -> Unit * {
|
||||
return const_cast<Unit *>(state->mutable_units()->GetMutableObject(unitId));
|
||||
}
|
||||
|
||||
inline auto UnitFromBytes(const std::string &unitBytes) -> const Unit & {
|
||||
return *reinterpret_cast<const Unit *>(unitBytes.data());
|
||||
}
|
||||
|
||||
using net::eagle0::shardok::storage::fb::VictoryCondition;
|
||||
using net::eagle0::shardok::storage::fb::VictoryType;
|
||||
|
||||
@@ -68,7 +65,7 @@ void ApplyResolvedUnit(
|
||||
// If a VIP was captured, alter the morale of all this player's units
|
||||
if (status == net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT &&
|
||||
unit.has_attached_hero() && unit.attached_hero().is_vip()) {
|
||||
for (uint32_t i = 0; i < inoutState->units()->size(); i++) {
|
||||
for (uint32_t i = 0; i < inoutState->units()->size(); ++i) {
|
||||
auto *playerUnit = GetMutableUnit(inoutState, i);
|
||||
if (playerUnit->player_id() != unit.player_id()) continue;
|
||||
if (playerUnit->unit_id() == unit.unit_id()) continue;
|
||||
@@ -101,7 +98,7 @@ void ApplyResolvedUnit(
|
||||
GameStateT &inoutState,
|
||||
const ResolvedUnitProto &resolvedUnit,
|
||||
const double vipCapturedMoraleAdjustment) {
|
||||
const Unit &unit = UnitFromBytes(resolvedUnit.unit_bytes());
|
||||
const Unit unit = UnitFromBytes(resolvedUnit.unit_bytes());
|
||||
UnitId unitId = unit.unit_id();
|
||||
|
||||
if (unit.has_attached_hero() &&
|
||||
@@ -169,15 +166,15 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
|
||||
|
||||
// First pass: check what kind of modifications we need
|
||||
for (const auto &unitBytes : result.changed_units_fb()) {
|
||||
const auto *unit = &UnitFromBytes(unitBytes);
|
||||
maxChangedUnitId = std::max(maxChangedUnitId, unit->unit_id());
|
||||
const auto unit = UnitFromBytes(unitBytes);
|
||||
maxChangedUnitId = std::max(maxChangedUnitId, unit.unit_id());
|
||||
|
||||
if (static_cast<unsigned int>(unit->unit_id()) >= mutatingState->units()->size()) {
|
||||
if (static_cast<unsigned int>(unit.unit_id()) >= mutatingState->units()->size()) {
|
||||
// Unit ID beyond vector size - must expand
|
||||
needsVectorExpansion = true;
|
||||
break; // No point checking further
|
||||
} else if (
|
||||
mutatingState->units()->Get(unit->unit_id())->status() ==
|
||||
mutatingState->units()->Get(unit.unit_id())->status() ==
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
|
||||
// Unit wants to use a reserved slot
|
||||
needsReservedSlotConversion = true;
|
||||
@@ -199,8 +196,8 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
|
||||
// Convert reserved slots to real units in place
|
||||
// We only need to process the units that are being changed
|
||||
for (const auto &unitBytes : result.changed_units_fb()) {
|
||||
const auto *unit = &UnitFromBytes(unitBytes);
|
||||
auto *mutableUnit = GetMutableUnit(mutatingState.Get(), unit->unit_id());
|
||||
const auto unit = UnitFromBytes(unitBytes);
|
||||
auto *mutableUnit = GetMutableUnit(mutatingState.Get(), unit.unit_id());
|
||||
if (mutableUnit->status() ==
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
|
||||
// Convert this reserved slot to a real unit
|
||||
@@ -248,12 +245,12 @@ void MutatingApplyResult(
|
||||
static_cast<net::eagle0::shardok::storage::fb::GameStatus_::State>(
|
||||
result.game_status().state())));
|
||||
const int winningShardokIdCount = result.game_status().winning_shardok_ids_size();
|
||||
for (int i = 0; i < winningShardokIdCount; i++) {
|
||||
for (int i = 0; i < winningShardokIdCount; ++i) {
|
||||
mutatingGameState->mutable_status()->mutable_winning_shardok_ids()->Mutate(
|
||||
i,
|
||||
result.game_status().winning_shardok_ids(i));
|
||||
}
|
||||
for (int i = winningShardokIdCount; i < 10; i++) {
|
||||
for (int i = winningShardokIdCount; i < 10; ++i) {
|
||||
mutatingGameState->mutable_status()->mutable_winning_shardok_ids()->Mutate(i, -1);
|
||||
}
|
||||
|
||||
@@ -299,50 +296,48 @@ void MutatingApplyResult(
|
||||
const double vipCapturedMoraleAdjustment = settings.Backing().vip_captured_morale_adjustment();
|
||||
|
||||
for (const auto &changedUnitBytes : result.changed_units_fb()) {
|
||||
const Unit *changedUnit = &UnitFromBytes(changedUnitBytes);
|
||||
const Unit changedUnit = UnitFromBytes(changedUnitBytes);
|
||||
|
||||
internalAssert(
|
||||
changedUnit->unit_id() < (int32_t)mutatingGameState->units()->size(),
|
||||
changedUnit.unit_id() < static_cast<int32_t>(mutatingGameState->units()->size()),
|
||||
"Got a changed unit outside acceptable range");
|
||||
const auto battalionSizeAfter = changedUnit->battalion().size();
|
||||
const auto battalionSizeAfter = changedUnit.battalion().size();
|
||||
|
||||
const int battalionSizeBefore = (changedUnit->unit_id() < (int32_t)initialUnitCount)
|
||||
? mutatingGameState->units()
|
||||
->Get(changedUnit->unit_id())
|
||||
->battalion()
|
||||
.size()
|
||||
: changedUnit->battalion().size();
|
||||
const int battalionSizeBefore =
|
||||
(changedUnit.unit_id() < static_cast<int32_t>(initialUnitCount))
|
||||
? mutatingGameState->units()->Get(changedUnit.unit_id())->battalion().size()
|
||||
: changedUnit.battalion().size();
|
||||
|
||||
auto status = changedUnit->status();
|
||||
if (IsDestroyed(*changedUnit)) {
|
||||
auto status = changedUnit.status();
|
||||
if (IsDestroyed(changedUnit)) {
|
||||
status =
|
||||
changedUnit->has_attached_hero()
|
||||
changedUnit.has_attached_hero()
|
||||
? net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT
|
||||
: net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT;
|
||||
|
||||
ApplyResolvedUnit(
|
||||
mutatingGameState.Get(),
|
||||
*changedUnit,
|
||||
changedUnit,
|
||||
status,
|
||||
vipCapturedMoraleAdjustment,
|
||||
settings);
|
||||
}
|
||||
|
||||
// Capture old position before applying changes
|
||||
auto *mutableUnit = GetMutableUnit(mutatingGameState.Get(), changedUnit->unit_id());
|
||||
auto *mutableUnit = GetMutableUnit(mutatingGameState.Get(), changedUnit.unit_id());
|
||||
const auto oldLocation = mutableUnit->location();
|
||||
|
||||
fb::ApplyUnit(mutableUnit, changedUnit, status);
|
||||
fb::ApplyUnit(mutableUnit, &changedUnit, status);
|
||||
|
||||
// Update occupied tiles bitfield if position changed
|
||||
const auto &newLocation = changedUnit->location();
|
||||
const auto &newLocation = changedUnit.location();
|
||||
if (oldLocation.row() != newLocation.row() ||
|
||||
oldLocation.column() != newLocation.column()) {
|
||||
mutatingGameState.UpdateOccupiedTile(oldLocation, newLocation);
|
||||
}
|
||||
|
||||
if (battalionSizeBefore != battalionSizeAfter) {
|
||||
if (changedUnit->battalion().type() ==
|
||||
if (changedUnit.battalion().type() ==
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
|
||||
if (battalionSizeAfter > battalionSizeBefore) {
|
||||
// Undead consumed some bodies
|
||||
@@ -354,7 +349,7 @@ void MutatingApplyResult(
|
||||
if (battalionSizeAfter > battalionSizeBefore) {
|
||||
std::ostringstream stringStream;
|
||||
stringStream << "Battalion got larger! Was " << battalionSizeBefore << ", now "
|
||||
<< changedUnit << " result was " << result.DebugString();
|
||||
<< &changedUnit << " result was " << result.DebugString();
|
||||
throw ShardokInternalErrorException(stringStream.str());
|
||||
}
|
||||
|
||||
@@ -364,7 +359,7 @@ void MutatingApplyResult(
|
||||
if (newCount != 0) {
|
||||
newArmament =
|
||||
(mutatingGameState->dead_count() * mutatingGameState->dead_armament() +
|
||||
casualties * changedUnit->battalion().armament()) /
|
||||
casualties * changedUnit.battalion().armament()) /
|
||||
newCount;
|
||||
}
|
||||
|
||||
@@ -384,10 +379,10 @@ void MutatingApplyResult(
|
||||
internalAssert(mutatingGameState->mutate_eligible_charger_id(-1));
|
||||
}
|
||||
const int possibleChargeeCount = result.possible_chargees_size();
|
||||
for (int i = 0; i < possibleChargeeCount; i++) {
|
||||
for (int i = 0; i < possibleChargeeCount; ++i) {
|
||||
mutatingGameState->mutable_possible_chargee_ids()->Mutate(i, result.possible_chargees(i));
|
||||
}
|
||||
for (int i = possibleChargeeCount; i < 6; i++) {
|
||||
for (int i = possibleChargeeCount; i < 6; ++i) {
|
||||
mutatingGameState->mutable_possible_chargee_ids()->Mutate(i, -1);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -23,11 +23,11 @@ void MutatingApplyResult(
|
||||
GameStateW& mutatingGameState,
|
||||
const ActionResultProto& actionResult,
|
||||
const SettingsGetter& settings);
|
||||
auto ApplyResult(
|
||||
[[nodiscard]] auto ApplyResult(
|
||||
GameStateW startingState,
|
||||
const ActionResultProto& actionResult,
|
||||
const SettingsGetter& settings) -> GameStateW;
|
||||
auto ApplyResults(
|
||||
[[nodiscard]] auto ApplyResults(
|
||||
const GameStateW& startingState,
|
||||
const std::vector<ActionResultProto>& actionResults,
|
||||
const SettingsGetter& settings) -> GameStateW;
|
||||
|
||||
@@ -16,6 +16,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:hex_map_hasher",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/unit",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user