mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 05:15:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
560ceb8097 | ||
|
|
91ec899247 | ||
|
|
266ddf5570 | ||
|
|
5e31e3ff6b | ||
|
|
c5c8252325 | ||
|
|
36124919b9 | ||
|
|
4ffdca7c8d | ||
|
|
6eaf748d6f | ||
|
|
5a346cd8df | ||
|
|
585cbb135c | ||
|
|
292573e137 | ||
|
|
3492e49433 | ||
|
|
4529e0fa2b | ||
|
|
9a68094b07 | ||
|
|
1ca8e81062 | ||
|
|
9e430e786e | ||
|
|
bb302001f2 | ||
|
|
7c300733e0 | ||
|
|
57b6f6f1eb | ||
|
|
5bddf66240 | ||
|
|
c8fc611182 | ||
|
|
cbd1058523 | ||
|
|
e0d67f4750 | ||
|
|
5b2c0214db | ||
|
|
511c69b14e | ||
|
|
9ebe6dc841 | ||
|
|
f2bed66119 | ||
|
|
96eebb87af | ||
|
|
716ee0ca50 | ||
|
|
6492215005 | ||
|
|
ab5c337b22 | ||
|
|
16d82332cd | ||
|
|
023ffbafc9 | ||
|
|
8ec8b902f6 | ||
|
|
ae4916e7bf | ||
|
|
73699ae901 | ||
|
|
910c60236e | ||
|
|
9b989dd35e | ||
|
|
9400dbeba2 | ||
|
|
33f6c4dfc9 | ||
|
|
52941d9f50 | ||
|
|
4cdda1fab6 | ||
|
|
67749672fd | ||
|
|
23b6dfb0a9 | ||
|
|
c98eeb9f88 | ||
|
|
ec0c391467 | ||
|
|
7305149729 | ||
|
|
556094d15d | ||
|
|
22c7965314 | ||
|
|
4fd8f81686 | ||
|
|
3d650ff5c8 | ||
|
|
9e2bcdf8a5 | ||
|
|
d2d85f1a2d | ||
|
|
b90a5835da | ||
|
|
54c418c07b | ||
|
|
d4083791c1 | ||
|
|
0ed403ded1 | ||
|
|
f8b064f28b | ||
|
|
38973f07a0 | ||
|
|
1f5bfe9e4d | ||
|
|
1d0b1dafed | ||
|
|
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 |
@@ -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]
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
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[@]}"
|
||||
@@ -32,23 +32,33 @@ echo "=== Creating ZIP for notarization: $ZIP_PATH ===" >&2
|
||||
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
|
||||
|
||||
echo "=== Submitting to Apple for notarization ===" >&2
|
||||
set +e
|
||||
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APP_SPECIFIC_PASSWORD" \
|
||||
--team-id "$TEAM_ID" 2>&1)
|
||||
SUBMIT_STATUS=$?
|
||||
set -e
|
||||
|
||||
echo "$SUBMIT_OUTPUT" >&2
|
||||
|
||||
if [ "$SUBMIT_STATUS" -ne 0 ]; then
|
||||
rm -f "$ZIP_PATH"
|
||||
echo "ERROR: notarytool submit failed with exit code $SUBMIT_STATUS" >&2
|
||||
exit "$SUBMIT_STATUS"
|
||||
fi
|
||||
|
||||
# Extract submission ID
|
||||
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
|
||||
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | awk '/^[[:space:]]*id:/ {print $2; exit}')
|
||||
|
||||
if [ -z "$SUBMISSION_ID" ]; then
|
||||
rm -f "$ZIP_PATH"
|
||||
echo "ERROR: Failed to get submission ID" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up the zip
|
||||
rm "$ZIP_PATH"
|
||||
rm -f "$ZIP_PATH"
|
||||
|
||||
echo "Submission ID: $SUBMISSION_ID" >&2
|
||||
|
||||
|
||||
@@ -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,6 +9,7 @@ namespace shardok {
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
|
||||
constexpr double kDefaultMorale = 50.0;
|
||||
constexpr float kExplicitMoraleSentinel = -1.0f;
|
||||
|
||||
auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) -> Battalion {
|
||||
Battalion shardokBattalion{};
|
||||
@@ -18,6 +19,10 @@ auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) ->
|
||||
shardokBattalion.mutate_type(
|
||||
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(battalion.type()));
|
||||
shardokBattalion.mutate_morale(kDefaultMorale);
|
||||
if (battalion.has_morale()) {
|
||||
shardokBattalion.mutate_morale(static_cast<float>(battalion.morale()));
|
||||
shardokBattalion.mutate_base_morale(kExplicitMoraleSentinel);
|
||||
}
|
||||
shardokBattalion.mutate_armament(static_cast<float>(battalion.armament()));
|
||||
shardokBattalion.mutate_training(static_cast<float>(battalion.training()));
|
||||
|
||||
@@ -125,4 +130,4 @@ auto ConvertUnit(
|
||||
return shardokUnit;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -163,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) {
|
||||
@@ -179,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);
|
||||
@@ -195,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;
|
||||
@@ -215,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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "AICommandFilter.hpp"
|
||||
#include "TranspositionTable.hpp"
|
||||
@@ -21,12 +23,49 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
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;
|
||||
|
||||
[[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:
|
||||
@@ -39,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;
|
||||
@@ -59,6 +96,14 @@ static auto RandomnessSampleForRepeat(const int repeatIteration, const int maxRe
|
||||
return static_cast<double>(repeatIteration) / static_cast<double>(maxRepeatCount - 1);
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -72,13 +117,50 @@ static auto CommandSorter(
|
||||
return false;
|
||||
}
|
||||
|
||||
static auto FollowUpCommandScoreBonus(const CommandType commandType) -> ScoreValue {
|
||||
switch (commandType) {
|
||||
case net::eagle0::shardok::common::CHARGE_COMMAND: return 30.0;
|
||||
case net::eagle0::shardok::common::ARCHERY_COMMAND: return 20.0;
|
||||
case net::eagle0::shardok::common::METEOR_START_COMMAND: return 35.0;
|
||||
case net::eagle0::shardok::common::LIGHTNING_BOLT_COMMAND: return 25.0;
|
||||
case net::eagle0::shardok::common::REDUCE_COMMAND: return 25.0;
|
||||
case net::eagle0::shardok::common::FEAR_COMMAND: return 15.0;
|
||||
case net::eagle0::shardok::common::START_FIRE_COMMAND: return 15.0;
|
||||
case net::eagle0::shardok::common::REPAIR_COMMAND: return 10.0;
|
||||
case net::eagle0::shardok::common::REINFORCE_COMMAND: return 10.0;
|
||||
case net::eagle0::shardok::common::SCOUT_COMMAND: return 5.0;
|
||||
default: return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
static auto LeafMoveFollowUpScoreBonus(
|
||||
const ShardokEngine& guessedEngine,
|
||||
const PlayerId pid,
|
||||
const size_t commandIndex) -> ScoreValue {
|
||||
const CommandListSPtr commandsWithFollowUps =
|
||||
guessedEngine.GetAvailableCommandsForAIPlayer(pid, true);
|
||||
if (!commandsWithFollowUps || commandIndex >= commandsWithFollowUps->size()) { return 0.0; }
|
||||
|
||||
const CommandSPtr& command = commandsWithFollowUps->at(commandIndex);
|
||||
if (command->GetCommandType() != net::eagle0::shardok::common::MOVE_COMMAND) { return 0.0; }
|
||||
|
||||
ScoreValue bonus = 0.0;
|
||||
const auto proto = command->GetCommandProto();
|
||||
for (const int followUpType : proto.follow_up_command_types()) {
|
||||
bonus += FollowUpCommandScoreBonus(static_cast<CommandType>(followUpType));
|
||||
}
|
||||
return bonus;
|
||||
}
|
||||
|
||||
AICommandEvaluator::AICommandEvaluator(
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter)
|
||||
BattalionTypeGetter battalionTypeGetter,
|
||||
const bool scoreMoveFollowUps)
|
||||
: scorer_(scorer),
|
||||
apdCache_(apdCache),
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)) {}
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)),
|
||||
scoreMoveFollowUps_(scoreMoveFollowUps) {}
|
||||
|
||||
auto AICommandEvaluator::PerformLookahead(
|
||||
const PlayerId pid,
|
||||
@@ -133,17 +215,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;
|
||||
@@ -194,14 +293,41 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
const auto guessedCommands = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
|
||||
const ScoreValue leafMoveFollowUpBonus =
|
||||
scoreMoveFollowUps_ && remainingLookahead <= 0
|
||||
? LeafMoveFollowUpScoreBonus(guessedEngine, pid, commandIndex)
|
||||
: 0.0;
|
||||
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) +
|
||||
leafMoveFollowUpBonus;
|
||||
} 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;
|
||||
@@ -216,22 +342,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) {
|
||||
@@ -333,7 +470,19 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
|
||||
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;
|
||||
@@ -351,7 +500,7 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
originalIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
kAverageGenerator,
|
||||
AverageGenerator(),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
@@ -365,7 +514,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,
|
||||
@@ -373,13 +522,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,
|
||||
@@ -387,7 +536,7 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(
|
||||
std::vector{(1.0 - successChance) / 2.0}),
|
||||
std::vector{FailureSampleForOdds(successChance)}),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
@@ -455,7 +604,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;
|
||||
}
|
||||
@@ -522,7 +681,7 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
kAverageGenerator,
|
||||
AverageGenerator(),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
@@ -541,7 +700,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 +714,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,
|
||||
|
||||
@@ -35,7 +35,8 @@ public:
|
||||
AICommandEvaluator(
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter); // Pass by value
|
||||
BattalionTypeGetter battalionTypeGetter,
|
||||
bool scoreMoveFollowUps = true); // Pass by value
|
||||
|
||||
/// Evaluates the score for a particular command index with lookahead.
|
||||
struct EvaluationResult {
|
||||
@@ -79,6 +80,7 @@ private:
|
||||
const AIScoreCalculator& scorer_;
|
||||
const APDCache& apdCache_;
|
||||
BattalionTypeGetter battalionTypeGetter_;
|
||||
bool scoreMoveFollowUps_;
|
||||
|
||||
struct ImmediateAndLookaheadScore {
|
||||
ScoreValue immediateScore{};
|
||||
|
||||
@@ -116,8 +116,9 @@ auto DefenderDistanceBuf(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,8 @@ using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
net::eagle0::shardok::storage::fb::Profession profession) -> bool;
|
||||
|
||||
[[nodiscard]] auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int;
|
||||
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &, PlayerId pid) -> const PlayerInfo *;
|
||||
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &gameState, PlayerId pid)
|
||||
-> const PlayerInfo *;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -88,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 =
|
||||
@@ -120,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
|
||||
@@ -235,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:
|
||||
@@ -247,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(
|
||||
|
||||
@@ -6,9 +6,18 @@
|
||||
|
||||
#include "AIMinimumDistanceAndTarget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
auto RecomputeModifierHash(HexMap *hexMap) -> MapId {
|
||||
hexMap->mutate_modifier_hash(GetModifierHash(hexMap));
|
||||
return ActionPointDistancesCache::GetMapId(hexMap);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto UnitIdsRequiringWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
@@ -169,6 +178,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);
|
||||
@@ -185,6 +195,50 @@ auto WaterCrossingTiles(
|
||||
return returnCoords;
|
||||
}
|
||||
|
||||
// Returns currently frozen water tiles that can act as an existing crossing from origin to
|
||||
// destination. The shared APD cache deliberately ignores ice, so this probes frozen tiles by
|
||||
// temporarily treating one as bridged for the water-crossing strategy only.
|
||||
auto FrozenWaterCrossingTiles(
|
||||
const Coords &origin,
|
||||
const Coords &destination,
|
||||
const HexMap *hexMap,
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeSPtr &battalionType) -> CoordsSet {
|
||||
AssertValid(origin, hexMap);
|
||||
AssertValid(destination, hexMap);
|
||||
auto returnCoords = CoordsSet(hexMap);
|
||||
|
||||
fb::HexMapW mapCopy = fb::CopyHexMap(hexMap);
|
||||
const int mapSize = hexMap->row_count() * hexMap->column_count();
|
||||
|
||||
for (int index = 0; index < mapSize; ++index) {
|
||||
const auto startTerrain = *hexMap->terrain()->Get(index);
|
||||
|
||||
if (!IsWater(startTerrain.type())) continue;
|
||||
|
||||
const auto modifier = startTerrain.modifier();
|
||||
if (!modifier.ice().present() || modifier.fire().present()) continue;
|
||||
|
||||
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);
|
||||
|
||||
const MapId mapId = RecomputeModifierHash(mapCopy.Get());
|
||||
|
||||
if (const auto *distances = apdCache->GetRaw(mapCopy, mapId, battalionType, false);
|
||||
distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE) {
|
||||
returnCoords.Add(index / hexMap->column_count(), index % hexMap->column_count());
|
||||
}
|
||||
|
||||
terr->mutable_modifier().mutable_bridge().mutate_present(false);
|
||||
terr->mutable_modifier().mutable_bridge().mutate_integrity(0.0);
|
||||
}
|
||||
|
||||
return returnCoords;
|
||||
}
|
||||
|
||||
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
|
||||
auto IntendedCrossingStarts(
|
||||
const GameStateW &gameState,
|
||||
@@ -213,7 +267,6 @@ auto IntendedCrossingStarts(
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
constexpr double kNoRequiredCrossingScore = std::numeric_limits<double>::max();
|
||||
constexpr double kNoCrossingCreatorsScore = std::numeric_limits<double>::min();
|
||||
|
||||
auto WaterCrossingScore(
|
||||
const PlayerId playerId,
|
||||
@@ -256,12 +309,26 @@ auto WaterCrossingScore(
|
||||
|
||||
const auto unitIdsCreatingCrossing =
|
||||
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
|
||||
if (unitIdsCreatingCrossing.empty()) return kNoCrossingCreatorsScore;
|
||||
|
||||
double totalScore = 0;
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
|
||||
|
||||
if (unitIdsCreatingCrossing.empty()) {
|
||||
for (const UnitId uid : unitIdsRequiringCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
const Coords location = unit->location();
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
const int thisDistance =
|
||||
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
|
||||
totalScore -= thisDistance;
|
||||
}
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
// First put a big penalty on the distance for units that can create a crossing
|
||||
for (const UnitId uid : unitIdsCreatingCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
|
||||
@@ -68,6 +68,15 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeSPtr& battalionType) -> CoordsSet;
|
||||
|
||||
// Returns currently frozen water tiles that can act as an existing crossing from origin to
|
||||
// destination.
|
||||
[[nodiscard]] auto FrozenWaterCrossingTiles(
|
||||
const Coords& origin,
|
||||
const Coords& destination,
|
||||
const HexMap* hexMap,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeSPtr& battalionType) -> CoordsSet;
|
||||
|
||||
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
|
||||
[[nodiscard]] auto IntendedCrossingStarts(
|
||||
const GameStateW& gameState,
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace shardok {
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
constexpr ScoreValue kNoRequiredCrossingScore = std::numeric_limits<ScoreValue>::max();
|
||||
constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>::min();
|
||||
|
||||
[[nodiscard]] auto AIWaterCrossingCommandChooser::WaterCrossingScore(
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
@@ -56,12 +55,26 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
|
||||
|
||||
const auto unitIdsCreatingCrossing =
|
||||
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
|
||||
if (unitIdsCreatingCrossing.empty()) return kNoCrossingCreatorsScore;
|
||||
|
||||
ScoreValue totalScore = 0;
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
|
||||
|
||||
if (unitIdsCreatingCrossing.empty()) {
|
||||
for (const UnitId uid : unitIdsRequiringCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
const Coords location = unit->location();
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
const int thisDistance =
|
||||
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
|
||||
totalScore -= thisDistance;
|
||||
}
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
// First put a big penalty on the distance for units that can create a crossing
|
||||
for (const UnitId uid : unitIdsCreatingCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
@@ -157,7 +170,6 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
|
||||
const auto unitIdsCreatingCrossing =
|
||||
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
|
||||
if (unitIdsCreatingCrossing.empty()) return startCrossingFrom;
|
||||
|
||||
for (const UnitId uid : unitIdsRequiringCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
@@ -177,6 +189,17 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
Coords destination = *destinations.begin();
|
||||
AssertValid(destination, gameState->hex_map());
|
||||
|
||||
auto frozenWaterCrossingTiles = FrozenWaterCrossingTiles(
|
||||
origin,
|
||||
destination,
|
||||
gameState->hex_map(),
|
||||
apdCache,
|
||||
battalionType);
|
||||
startCrossingFrom +=
|
||||
CrossingStartLocations(gameState->hex_map(), frozenWaterCrossingTiles);
|
||||
|
||||
if (unitIdsCreatingCrossing.empty()) continue;
|
||||
|
||||
auto waterCrossingTiles = WaterCrossingTiles(
|
||||
origin,
|
||||
destination,
|
||||
@@ -192,4 +215,4 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
return startCrossingFrom;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
@@ -44,14 +44,16 @@ IterativeDeepeningAI::IterativeDeepeningAI(
|
||||
const CoordsSet& castleCoords,
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter)
|
||||
BattalionTypeGetter battalionTypeGetter,
|
||||
const bool scoreMoveFollowUps)
|
||||
: playerId(playerId),
|
||||
isDefender(isDefender),
|
||||
strategy(std::move(strategy)),
|
||||
castleCoords(castleCoords),
|
||||
scorer(scorer),
|
||||
apdCache(apdCache),
|
||||
battalionTypeGetter(std::move(battalionTypeGetter)) {}
|
||||
battalionTypeGetter(std::move(battalionTypeGetter)),
|
||||
scoreMoveFollowUps(scoreMoveFollowUps) {}
|
||||
|
||||
auto IterativeDeepeningAI::IterativeSearch(
|
||||
const GameSettingsSPtr& settings,
|
||||
@@ -333,7 +335,7 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
|
||||
const auto deadline = startTime + timeBudget.remainingBudget;
|
||||
|
||||
// Create command evaluator for lookahead search
|
||||
AICommandEvaluator evaluator(scorer, apdCache, battalionTypeGetter);
|
||||
AICommandEvaluator evaluator(scorer, apdCache, battalionTypeGetter, scoreMoveFollowUps);
|
||||
|
||||
// Get the future from EvaluateCommand - don't wait yet
|
||||
// Note: EvaluateCommand expects remainingLookahead, not desiredDepth
|
||||
|
||||
@@ -64,7 +64,8 @@ public:
|
||||
const CoordsSet& castleCoords,
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter); // Pass by value
|
||||
BattalionTypeGetter battalionTypeGetter,
|
||||
bool scoreMoveFollowUps); // Pass by value
|
||||
|
||||
[[nodiscard]] SearchResult IterativeSearch(
|
||||
const GameSettingsSPtr& settings,
|
||||
@@ -80,6 +81,7 @@ private:
|
||||
const AIScoreCalculator& scorer;
|
||||
const APDCache& apdCache;
|
||||
BattalionTypeGetter battalionTypeGetter;
|
||||
bool scoreMoveFollowUps;
|
||||
|
||||
// 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>
|
||||
|
||||
@@ -62,12 +65,14 @@ ShardokAIClient::ShardokAIClient(
|
||||
const SettingsGetter &settings,
|
||||
const AIAlgorithmType aiAlgorithmType,
|
||||
const ScoringCalculatorType scoringCalculatorType,
|
||||
const mcts::MCTSConfig &mctsConfig)
|
||||
const mcts::MCTSConfig &mctsConfig,
|
||||
const bool scoreMoveFollowUps)
|
||||
: playerId(playerId),
|
||||
isDefender(isDefender),
|
||||
isAllAiBattle(isAllAiBattle),
|
||||
aiAlgorithmType(aiAlgorithmType),
|
||||
scoringCalculatorType(scoringCalculatorType),
|
||||
scoreMoveFollowUps(scoreMoveFollowUps),
|
||||
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
|
||||
waterCrossingCommandChooser(playerId, apdCache),
|
||||
mctsConfig(mctsConfig) {
|
||||
@@ -362,7 +367,8 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
castleCoords,
|
||||
*scorer,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
battalionTypeGetter,
|
||||
scoreMoveFollowUps);
|
||||
search_result =
|
||||
ai.IterativeSearch(settings, guessedState, realAvailableCommands, timeBudget);
|
||||
}
|
||||
@@ -465,8 +471,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 +492,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));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ private:
|
||||
const bool isAllAiBattle; // Whether this battle has only AI players (for faster time budgets)
|
||||
const AIAlgorithmType aiAlgorithmType;
|
||||
const ScoringCalculatorType scoringCalculatorType;
|
||||
const bool scoreMoveFollowUps;
|
||||
|
||||
APDCache apdCache = std::make_shared<ActionPointDistancesCache>();
|
||||
ALCache alCache;
|
||||
@@ -75,7 +76,8 @@ public:
|
||||
const SettingsGetter& settings,
|
||||
AIAlgorithmType aiAlgorithmType,
|
||||
ScoringCalculatorType scoringCalculatorType,
|
||||
const mcts::MCTSConfig& mctsConfig);
|
||||
const mcts::MCTSConfig& mctsConfig,
|
||||
bool scoreMoveFollowUps = true);
|
||||
~ShardokAIClient() = default;
|
||||
|
||||
[[nodiscard]] auto GetPlayerId() const -> PlayerId { return playerId; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,30 @@ 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(
|
||||
@@ -72,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,
|
||||
@@ -92,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(
|
||||
@@ -106,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
|
||||
@@ -187,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;
|
||||
@@ -211,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();
|
||||
}
|
||||
|
||||
@@ -258,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(),
|
||||
@@ -266,40 +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);
|
||||
return SortActionsByWeight(std::move(actions), weights);
|
||||
if (cacheEntryMatches) {
|
||||
cacheHits_.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
// Convert immutable cached action snapshots to MCTS action instances.
|
||||
std::vector<std::unique_ptr<MCTSAction>> actions;
|
||||
actions.reserve(it->second.actions.size());
|
||||
|
||||
for (const auto& cachedAction : it->second.actions) {
|
||||
actions.push_back(cachedAction.clone());
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -307,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);
|
||||
|
||||
@@ -338,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,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;
|
||||
@@ -414,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
|
||||
@@ -441,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_,
|
||||
@@ -488,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
|
||||
@@ -590,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() !=
|
||||
|
||||
@@ -571,6 +571,8 @@ std::unique_ptr<ShardokAIClient> AiBattleSimulator::CreateAIClient(
|
||||
const ScoringCalculatorType scoringType =
|
||||
ConvertScoringCalculatorType(playerConfig.scoring_calculator());
|
||||
const auto mctsConfig = BuildMCTSConfig(playerConfig, config_.random_seed(), playerId);
|
||||
const bool scoreMoveFollowUps =
|
||||
!playerConfig.has_score_move_follow_ups() || playerConfig.score_move_follow_ups();
|
||||
|
||||
return ai_testing_common::AIClientFactory::Create(
|
||||
playerId,
|
||||
@@ -580,7 +582,8 @@ std::unique_ptr<ShardokAIClient> AiBattleSimulator::CreateAIClient(
|
||||
algorithmType,
|
||||
scoringType,
|
||||
true,
|
||||
mctsConfig);
|
||||
mctsConfig,
|
||||
scoreMoveFollowUps);
|
||||
}
|
||||
|
||||
BattleResult AiBattleSimulator::RunSetupPhase(
|
||||
|
||||
+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,
|
||||
|
||||
@@ -17,7 +17,8 @@ auto AIClientFactory::Create(
|
||||
AIAlgorithmType algorithmType,
|
||||
ScoringCalculatorType scoringType,
|
||||
bool isAllAiBattle,
|
||||
mcts::MCTSConfig mctsConfig) -> std::unique_ptr<ShardokAIClient> {
|
||||
mcts::MCTSConfig mctsConfig,
|
||||
const bool scoreMoveFollowUps) -> std::unique_ptr<ShardokAIClient> {
|
||||
return std::make_unique<ShardokAIClient>(
|
||||
playerId,
|
||||
isDefender,
|
||||
@@ -26,7 +27,8 @@ auto AIClientFactory::Create(
|
||||
settings,
|
||||
algorithmType,
|
||||
scoringType,
|
||||
mctsConfig);
|
||||
mctsConfig,
|
||||
scoreMoveFollowUps);
|
||||
}
|
||||
|
||||
} // 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,
|
||||
@@ -62,7 +62,8 @@ public:
|
||||
AIAlgorithmType algorithmType = AIAlgorithmType::ITERATIVE_DEEPENING,
|
||||
ScoringCalculatorType scoringType = ScoringCalculatorType::STANDARD,
|
||||
bool isAllAiBattle = false,
|
||||
mcts::MCTSConfig mctsConfig = mcts::MCTSConfig{}) -> std::unique_ptr<ShardokAIClient>;
|
||||
mcts::MCTSConfig mctsConfig = mcts::MCTSConfig{},
|
||||
bool scoreMoveFollowUps = true) -> std::unique_ptr<ShardokAIClient>;
|
||||
};
|
||||
|
||||
} // namespace ai_testing_common
|
||||
|
||||
@@ -29,7 +29,7 @@ public:
|
||||
*
|
||||
* @return Shared pointer to initialized GameSettings
|
||||
*/
|
||||
static auto CreateDefault() -> GameSettingsSPtr;
|
||||
[[nodiscard]] static auto CreateDefault() -> GameSettingsSPtr;
|
||||
};
|
||||
|
||||
} // namespace shardok::ai_testing_common
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -27,6 +28,77 @@ using net::eagle0::common::ScopedShardokLatencyTrace;
|
||||
|
||||
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 {
|
||||
@@ -154,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 = [&] {
|
||||
@@ -165,6 +239,7 @@ 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)
|
||||
{
|
||||
@@ -183,6 +258,7 @@ void ShardokGameController::DoAIThread() {
|
||||
}
|
||||
|
||||
{
|
||||
aiPhase = "posting chosen command";
|
||||
ScopedShardokLatencyTrace trace(
|
||||
"ai_engine_post_command",
|
||||
cachedGameId,
|
||||
@@ -193,6 +269,7 @@ void ShardokGameController::DoAIThread() {
|
||||
}
|
||||
LockedNotifyClients();
|
||||
{
|
||||
aiPhase = "posting forced follow-up commands";
|
||||
ScopedShardokLatencyTrace trace(
|
||||
"ai_engine_post_forced_commands",
|
||||
cachedGameId,
|
||||
@@ -204,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;
|
||||
|
||||
@@ -189,7 +189,8 @@ struct BattalionType {
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetCostToEnterTerrain(const TerrainProto &terrain) const -> ActionCost {
|
||||
if (HasBridge(terrain.modifier())) {
|
||||
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 /
|
||||
@@ -208,7 +209,7 @@ 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()) {
|
||||
if (tm.bridge().present() && tm.bridge().integrity() > 0.0) {
|
||||
return {ActionCost::standard,
|
||||
ActionPoints(
|
||||
std::ceil(100.0 * minimumCostToEnterBridge / tm.bridge().integrity()))};
|
||||
|
||||
@@ -22,13 +22,13 @@ 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,
|
||||
@@ -36,7 +36,7 @@ PercentileRollOdds MakeOdds(
|
||||
int statF = 0,
|
||||
std::span<const OtherFactor> others = {});
|
||||
|
||||
PercentileRollOdds MakeOdds(
|
||||
[[nodiscard]] PercentileRollOdds MakeOdds(
|
||||
int base,
|
||||
int terr,
|
||||
int weath,
|
||||
@@ -44,11 +44,11 @@ PercentileRollOdds MakeOdds(
|
||||
const std::vector<double> &stats,
|
||||
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
|
||||
|
||||
@@ -112,6 +112,7 @@ void ShardokEngine::ApplyAndAddActionResults(const std::vector<ActionResultProto
|
||||
|
||||
void ShardokEngine::ApplyAndAddActionResult(const ActionResultProto &result) {
|
||||
gameState = ApplyResult(std::move(gameState), result, settingsGetter);
|
||||
ClearCachedAvailableCommands();
|
||||
|
||||
if (trackHistory) {
|
||||
actionHistory.emplace_back();
|
||||
@@ -140,8 +141,19 @@ 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),
|
||||
cachedAvailableCommands(toCopy.cachedAvailableCommands) {}
|
||||
tutorialController_(toCopy.tutorialController_),
|
||||
pendingReinforcementPlacement_(toCopy.pendingReinforcementPlacement_),
|
||||
cachedAvailableCommandsWithoutFollowUps(toCopy.cachedAvailableCommandsWithoutFollowUps),
|
||||
cachedAvailableCommandsWithFollowUps(toCopy.cachedAvailableCommandsWithFollowUps) {}
|
||||
|
||||
void ShardokEngine::ClearCachedAvailableCommands() const {
|
||||
cachedAvailableCommandsWithoutFollowUps = nullptr;
|
||||
cachedAvailableCommandsWithFollowUps = nullptr;
|
||||
}
|
||||
|
||||
auto ShardokEngine::GetGameStateAtStartOfAction(const ActionId startingActionId) const
|
||||
-> GameStateW {
|
||||
@@ -311,13 +323,13 @@ auto ShardokEngine::PostWhilePlayerHasOnlyOneOption(
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING &&
|
||||
playerId == GetCurrentPlayerId() && playerId != UNCONTROLLED_PLAYER_ID) {
|
||||
// if there's only one command available, go ahead and take it.
|
||||
cachedAvailableCommands = availableCommandsFactory->GetAvailableCommands(
|
||||
cachedAvailableCommandsWithFollowUps = availableCommandsFactory->GetAvailableCommands(
|
||||
gameState,
|
||||
playerId,
|
||||
/* includeFollowUps=*/true);
|
||||
if (cachedAvailableCommands->size() == 1) {
|
||||
const auto loneCommand = cachedAvailableCommands->front();
|
||||
cachedAvailableCommands = nullptr;
|
||||
if (cachedAvailableCommandsWithFollowUps->size() == 1) {
|
||||
const auto loneCommand = cachedAvailableCommandsWithFollowUps->front();
|
||||
ClearCachedAvailableCommands();
|
||||
PostActionUnchecked(loneCommand, randomGenerator, std::nullopt);
|
||||
commandCount++;
|
||||
} else {
|
||||
@@ -466,7 +478,7 @@ void ShardokEngine::PostPlacementCommands(
|
||||
}
|
||||
}
|
||||
|
||||
cachedAvailableCommands = nullptr;
|
||||
ClearCachedAvailableCommands();
|
||||
|
||||
// now execute
|
||||
for (const auto &pi : placementInfos) {
|
||||
@@ -528,7 +540,7 @@ void ShardokEngine::PostFinishedPlacementCommand(
|
||||
if (it == placementCommands->end()) {
|
||||
throw ShardokClientErrorException("No finish placement command found");
|
||||
}
|
||||
cachedAvailableCommands = nullptr;
|
||||
ClearCachedAvailableCommands();
|
||||
|
||||
PostActionUnchecked(*it, randomGenerator, std::nullopt);
|
||||
}
|
||||
@@ -545,21 +557,22 @@ void ShardokEngine::PostCommand(
|
||||
}
|
||||
|
||||
// Make sure this command is available & legal
|
||||
if (!cachedAvailableCommands) {
|
||||
cachedAvailableCommands = availableCommandsFactory->GetAvailableCommands(
|
||||
if (!cachedAvailableCommandsWithFollowUps) {
|
||||
cachedAvailableCommandsWithFollowUps = availableCommandsFactory->GetAvailableCommands(
|
||||
gameState,
|
||||
player,
|
||||
/* includeFollowUps=*/true);
|
||||
}
|
||||
|
||||
if (commandIndex >= static_cast<int64_t>(cachedAvailableCommands->size()) || commandIndex < 0) {
|
||||
if (commandIndex >= static_cast<int64_t>(cachedAvailableCommandsWithFollowUps->size()) ||
|
||||
commandIndex < 0) {
|
||||
throw ShardokClientErrorException(
|
||||
string("Illegal command index ") + std::to_string(commandIndex) +
|
||||
string(", max is ") + std::to_string(cachedAvailableCommands->size()));
|
||||
string(", max is ") + std::to_string(cachedAvailableCommandsWithFollowUps->size()));
|
||||
}
|
||||
|
||||
const CommandSPtr command = (*cachedAvailableCommands)[commandIndex];
|
||||
cachedAvailableCommands = nullptr;
|
||||
const CommandSPtr command = (*cachedAvailableCommandsWithFollowUps)[commandIndex];
|
||||
ClearCachedAvailableCommands();
|
||||
|
||||
PostActionUnchecked(command, randomGenerator, roll);
|
||||
}
|
||||
@@ -660,6 +673,12 @@ auto ShardokEngine::GetPreviewCommands(const PlayerId playerId) const -> std::ve
|
||||
|
||||
auto ShardokEngine::GetAvailableCommandsForAIPlayer(const PlayerId playerId) const
|
||||
-> CommandListSPtr {
|
||||
return GetAvailableCommandsForAIPlayer(playerId, false);
|
||||
}
|
||||
|
||||
auto ShardokEngine::GetAvailableCommandsForAIPlayer(
|
||||
const PlayerId playerId,
|
||||
const bool includeFollowUps) const -> CommandListSPtr {
|
||||
if (GameIsOver(GetGameStatus())) {
|
||||
// The game is over
|
||||
return {};
|
||||
@@ -667,8 +686,12 @@ auto ShardokEngine::GetAvailableCommandsForAIPlayer(const PlayerId playerId) con
|
||||
|
||||
if (playerId != GetCurrentPlayerId()) return {};
|
||||
|
||||
CommandListSPtr &cachedAvailableCommands = includeFollowUps
|
||||
? cachedAvailableCommandsWithFollowUps
|
||||
: cachedAvailableCommandsWithoutFollowUps;
|
||||
|
||||
if (!cachedAvailableCommands) {
|
||||
cachedAvailableCommands = UncachedGetAvailableCommands(playerId, false);
|
||||
cachedAvailableCommands = UncachedGetAvailableCommands(playerId, includeFollowUps);
|
||||
}
|
||||
|
||||
return cachedAvailableCommands;
|
||||
@@ -683,11 +706,8 @@ auto ShardokEngine::GetAvailableCommandProtos(const PlayerId playerId, const boo
|
||||
|
||||
if (playerId != GetCurrentPlayerId()) return {};
|
||||
|
||||
if (!cachedAvailableCommands) {
|
||||
cachedAvailableCommands = UncachedGetAvailableCommands(playerId, includeFollowUps);
|
||||
}
|
||||
|
||||
return CommandProtosFromActionList(*cachedAvailableCommands);
|
||||
return CommandProtosFromActionList(
|
||||
*GetAvailableCommandsForAIPlayer(playerId, includeFollowUps));
|
||||
}
|
||||
|
||||
auto ShardokEngine::UncachedGetAvailableCommands(
|
||||
@@ -718,8 +738,8 @@ 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;
|
||||
|
||||
@@ -53,7 +53,10 @@ private:
|
||||
TutorialBattleController tutorialController_;
|
||||
std::optional<ReinforcementPlacementInfo> pendingReinforcementPlacement_;
|
||||
|
||||
mutable CommandListSPtr cachedAvailableCommands{};
|
||||
mutable CommandListSPtr cachedAvailableCommandsWithoutFollowUps{};
|
||||
mutable CommandListSPtr cachedAvailableCommandsWithFollowUps{};
|
||||
|
||||
void ClearCachedAvailableCommands() const;
|
||||
|
||||
void ApplyAndAddActionResult(const ActionResult &result);
|
||||
|
||||
@@ -185,6 +188,8 @@ public:
|
||||
[[nodiscard]] auto GetPreviewCommands(PlayerId playerId) const -> std::vector<CommandProto>;
|
||||
|
||||
[[nodiscard]] auto GetAvailableCommandsForAIPlayer(PlayerId playerId) const -> CommandListSPtr;
|
||||
[[nodiscard]] auto GetAvailableCommandsForAIPlayer(PlayerId playerId, bool includeFollowUps)
|
||||
const -> CommandListSPtr;
|
||||
[[nodiscard]] auto GetAvailableCommandProtos(PlayerId playerId, bool includeFollowUps) const
|
||||
-> std::vector<CommandProto>;
|
||||
|
||||
|
||||
+24
-30
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -299,51 +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() < static_cast<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() < static_cast<int32_t>(initialUnitCount))
|
||||
? mutatingGameState->units()
|
||||
->Get(changedUnit->unit_id())
|
||||
->battalion()
|
||||
.size()
|
||||
: changedUnit->battalion().size();
|
||||
(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
|
||||
@@ -355,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());
|
||||
}
|
||||
|
||||
@@ -365,7 +359,7 @@ void MutatingApplyResult(
|
||||
if (newCount != 0) {
|
||||
newArmament =
|
||||
(mutatingGameState->dead_count() * mutatingGameState->dead_armament() +
|
||||
casualties * changedUnit->battalion().armament()) /
|
||||
casualties * changedUnit.battalion().armament()) /
|
||||
newCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace shardok::fb {
|
||||
|
||||
using namespace net::eagle0::shardok::storage::fb;
|
||||
using net::eagle0::shardok::storage::fb::UnitStatus;
|
||||
|
||||
auto ApplyUnit(
|
||||
Unit* toUnit,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
using namespace net::eagle0::shardok::common;
|
||||
using net::eagle0::shardok::common::WATER_DAMAGE;
|
||||
|
||||
auto EscapeChance(
|
||||
const int base,
|
||||
@@ -65,7 +65,7 @@ auto EscapeChance(
|
||||
}
|
||||
|
||||
auto ToVector(const Units &units) -> vector<const Unit *> {
|
||||
auto v = vector<const Unit *>{};
|
||||
vector<const Unit *> v{};
|
||||
v.reserve(units.size());
|
||||
for (const Unit *u : units) { v.push_back(u); }
|
||||
return v;
|
||||
|
||||
@@ -60,7 +60,8 @@ auto StartPlayerTurnAction::InternalExecute(
|
||||
->Get(startingState->month() - 1)
|
||||
->is_winter() &&
|
||||
!inCastle) {
|
||||
effectiveBase += float(settingsGetter.Backing().winter_morale_adjustment());
|
||||
effectiveBase +=
|
||||
static_cast<float>(settingsGetter.Backing().winter_morale_adjustment());
|
||||
}
|
||||
float moraleDiff = effectiveBase - updatedUnit.battalion().morale();
|
||||
MutatingMaybeAdjustMorale(
|
||||
|
||||
@@ -34,7 +34,8 @@ auto UndeadChangeAction::InternalExecute(
|
||||
int growth = newCount - decayedSize;
|
||||
|
||||
float newArmament =
|
||||
float(unit->battalion().armament() * decayedSize + armament * growth) / newCount;
|
||||
static_cast<float>(unit->battalion().armament() * decayedSize + armament * growth) /
|
||||
newCount;
|
||||
|
||||
auto newUnit = *unit;
|
||||
newUnit.mutable_battalion().mutate_size(newCount);
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
namespace shardok {
|
||||
|
||||
class EvacuatePrisonersCommandFactory : public CommandFactory {
|
||||
const SettingsGetter& settings;
|
||||
const SettingsGetter settings;
|
||||
|
||||
public:
|
||||
explicit EvacuatePrisonersCommandFactory(const SettingsGetter& getter);
|
||||
|
||||
@@ -61,9 +61,11 @@ void FleeCommandFactory::AddAvailableFleeCommands(
|
||||
const auto *occupant = Occupant(allUnits, twoAwayCoords);
|
||||
if (occupant && !occupant->hidden()) {
|
||||
if (std::ranges::contains(allyPids, occupant->player_id())) {
|
||||
adjacentFriendliesMod += int32_t(perAdjacentFriendly * adjustmentForTwoAway);
|
||||
adjacentFriendliesMod +=
|
||||
static_cast<int32_t>(perAdjacentFriendly * adjustmentForTwoAway);
|
||||
} else {
|
||||
adjacentEnemiesMod += int32_t(perAdjacentEnemy * adjustmentForTwoAway);
|
||||
adjacentEnemiesMod +=
|
||||
static_cast<int32_t>(perAdjacentEnemy * adjustmentForTwoAway);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct AccumulatedMoveInfo {
|
||||
Coords endLocation;
|
||||
ActionPoints pointCost = 0;
|
||||
bool willUnhide = false;
|
||||
vector<Coords> targets = {};
|
||||
vector<Coords> targets{};
|
||||
|
||||
AccumulatedMoveInfo(
|
||||
const Coords &startLocation,
|
||||
|
||||
@@ -305,6 +305,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library:percentile_roll_odds",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_command",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:odds_filter",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/PercentileRollOdds.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/OddsFilter.hpp"
|
||||
|
||||
namespace shardok {
|
||||
auto BecomeOutlawCommand::InternalExecute(
|
||||
@@ -59,7 +60,12 @@ auto BecomeOutlawCommand::GetCommandProto() const -> CommandProto {
|
||||
proto.set_player(GetPlayerId());
|
||||
proto.set_type(net::eagle0::shardok::common::BECOME_OUTLAW_COMMAND);
|
||||
proto.mutable_actor()->set_value(fleeingUnitId);
|
||||
*proto.mutable_odds() = OddsFilteredForPlayer(successOdds, GetPlayerId());
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
auto BecomeOutlawCommand::GetOddsPercentile() const -> int32_t {
|
||||
return OddsFilteredForPlayer(successOdds, GetPlayerId()).success_chance();
|
||||
}
|
||||
} // namespace shardok
|
||||
|
||||
@@ -41,6 +41,8 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetCommandProto() const -> CommandProto override;
|
||||
[[nodiscard]] auto HasOdds() const -> bool override { return true; }
|
||||
[[nodiscard]] auto GetOddsPercentile() const -> int32_t override;
|
||||
[[nodiscard]] auto CanBeFollowUp() const -> bool override { return false; }
|
||||
[[nodiscard]] auto CanDoWithLowMorale() const -> bool override { return true; }
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetCommandProto() const -> CommandProto override;
|
||||
[[nodiscard]] auto HasOdds() const -> bool override { return true; }
|
||||
[[nodiscard]] auto GetOddsPercentile() const -> int32_t override;
|
||||
[[nodiscard]] auto CanBeFollowUp() const -> bool override { return false; }
|
||||
[[nodiscard]] auto CanDoWithLowMorale() const -> bool override { return true; }
|
||||
|
||||
@@ -52,7 +52,7 @@ auto FreezeWaterCommand::InternalExecute(
|
||||
if (PercentileRollSucceeds(odds, roll)) {
|
||||
freezeResult.set_type(ActionType::FREEZE_WATER);
|
||||
|
||||
TileModifierProto newTileModifier = fb::ToTileModifierProto(targetTerrain->modifier());
|
||||
TileModifierProto newTileModifier = fb::ToTileModifierProto(targetTerrain.modifier());
|
||||
IncreaseFrozen(newTileModifier, freezeWaterIntegrity);
|
||||
*freezeResult.add_changed_tile_modifiers() = MakeTmc(target, newTileModifier);
|
||||
} else {
|
||||
|
||||
@@ -29,7 +29,7 @@ protected:
|
||||
const ActionCost cost;
|
||||
const UnitId actorId;
|
||||
const Coords target;
|
||||
const Terrain* targetTerrain;
|
||||
const Terrain targetTerrain;
|
||||
const UnitId occupantId;
|
||||
const PercentileRollOdds odds;
|
||||
const int freezeWaterIntegrity;
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
cost(cost),
|
||||
actorId(actor->unit_id()),
|
||||
target(target),
|
||||
targetTerrain(targetTerrain),
|
||||
targetTerrain(*targetTerrain),
|
||||
occupantId(underwaterOccupant->unit_id()),
|
||||
odds(std::move(odds)),
|
||||
freezeWaterIntegrity(freezeWaterIntegrity),
|
||||
@@ -71,7 +71,7 @@ public:
|
||||
cost(cost),
|
||||
actorId(actor->unit_id()),
|
||||
target(target),
|
||||
targetTerrain(targetTerrain),
|
||||
targetTerrain(*targetTerrain),
|
||||
occupantId(-1),
|
||||
odds(std::move(odds)),
|
||||
freezeWaterIntegrity(freezeWaterIntegrity),
|
||||
|
||||
@@ -59,7 +59,7 @@ auto HolyWaveDamageAction::InternalExecute(
|
||||
resultProto.set_type(net::eagle0::shardok::common::HOLY_WAVE_DAMAGE);
|
||||
|
||||
const Unit *damagedUnit = currentState->units()->Get(damagedUnitId);
|
||||
auto newSize = int32_t(damagedUnit->battalion().size() * (1.0 - damagePercentage));
|
||||
auto newSize = static_cast<int32_t>(damagedUnit->battalion().size() * (1.0 - damagePercentage));
|
||||
|
||||
auto damagedUnitAfter = *damagedUnit;
|
||||
damagedUnitAfter.mutable_battalion().mutate_size(newSize);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/ByteHasher.hpp"
|
||||
@@ -34,6 +35,7 @@ private:
|
||||
uint8_t* buffer;
|
||||
|
||||
static auto CopyBuffer(const uint8_t* data, const size_t size) -> uint8_t* {
|
||||
if (data == nullptr || size == 0) { return nullptr; }
|
||||
auto* b = WrapperAllocator::GetDefaultAllocator()->allocate(size);
|
||||
std::memcpy(b, data, size);
|
||||
return b;
|
||||
@@ -47,12 +49,29 @@ private:
|
||||
return CopyBuffer(copiedFrom.buffer, copiedFrom.size);
|
||||
}
|
||||
|
||||
static void ValidateBufferParams(const uint8_t* data, const size_t offset, const size_t size) {
|
||||
if (data == nullptr) {
|
||||
throw std::invalid_argument("Cannot construct wrapper from null bytes");
|
||||
}
|
||||
if (size == 0) { throw std::invalid_argument("Cannot construct wrapper from zero bytes"); }
|
||||
if (offset >= size) {
|
||||
throw std::invalid_argument("Cannot construct wrapper with offset outside buffer");
|
||||
}
|
||||
}
|
||||
|
||||
static auto CopyValidatedBuffer(const uint8_t* data, const size_t offset, const size_t size)
|
||||
-> uint8_t* {
|
||||
ValidateBufferParams(data, offset, size);
|
||||
return CopyBuffer(data, size);
|
||||
}
|
||||
|
||||
public:
|
||||
explicit Wrapper(flatbuffers::FlatBufferBuilder& fbb) : size(0), offset(0) {
|
||||
const uint8_t* releasedBuf = fbb.ReleaseRaw(size, offset);
|
||||
|
||||
if (releasedBuf == nullptr || size == 0) {
|
||||
buffer = nullptr;
|
||||
delete[] releasedBuf;
|
||||
throw std::invalid_argument("Cannot construct wrapper from empty FlatBufferBuilder");
|
||||
} else {
|
||||
buffer = CopyBuffer(releasedBuf, size);
|
||||
}
|
||||
@@ -62,7 +81,7 @@ public:
|
||||
explicit Wrapper(uint8_t* allocatedBytes, const size_t offset, const size_t size)
|
||||
: size(size),
|
||||
offset(offset),
|
||||
buffer(CopyBuffer(allocatedBytes, size)) {}
|
||||
buffer(CopyValidatedBuffer(allocatedBytes, offset, size)) {}
|
||||
|
||||
Wrapper() : size(0), offset(0), buffer(nullptr) {}
|
||||
Wrapper(Wrapper&& movedFrom) noexcept
|
||||
@@ -76,17 +95,10 @@ public:
|
||||
|
||||
auto operator=(const Wrapper& other) -> Wrapper& {
|
||||
if (&other != this) {
|
||||
if (other.size > size) {
|
||||
if (buffer != nullptr) {
|
||||
WrapperAllocator::GetDefaultAllocator()->deallocate(buffer, size);
|
||||
}
|
||||
buffer = CopyBuffer(other.buffer, other.size);
|
||||
} else if (buffer != nullptr && other.buffer != nullptr) {
|
||||
std::memcpy(buffer, other.buffer, other.size);
|
||||
}
|
||||
|
||||
size = other.size;
|
||||
offset = other.offset;
|
||||
Wrapper copy(other);
|
||||
std::swap(size, copy.size);
|
||||
std::swap(offset, copy.offset);
|
||||
std::swap(buffer, copy.buffer);
|
||||
}
|
||||
|
||||
return *this;
|
||||
@@ -110,10 +122,22 @@ public:
|
||||
// ReSharper disable once CppNonExplicitConversionOperator
|
||||
operator const FB*() const { return Get(); } // NOLINT(*-explicit-constructor)
|
||||
|
||||
auto Get() const -> const FB* { return flatbuffers::GetRoot<FB>(buffer + offset); }
|
||||
auto Get() -> FB* { return flatbuffers::GetMutableRoot<FB>(buffer + offset); }
|
||||
auto operator->() const -> const FB* { return flatbuffers::GetRoot<FB>(buffer + offset); }
|
||||
auto operator->() -> FB* { return flatbuffers::GetMutableRoot<FB>(buffer + offset); }
|
||||
auto Get() const -> const FB* {
|
||||
if (buffer == nullptr) { return nullptr; }
|
||||
return flatbuffers::GetRoot<FB>(buffer + offset);
|
||||
}
|
||||
auto Get() -> FB* {
|
||||
if (buffer == nullptr) { return nullptr; }
|
||||
return flatbuffers::GetMutableRoot<FB>(buffer + offset);
|
||||
}
|
||||
auto operator->() const -> const FB* {
|
||||
if (const auto* root = Get(); root != nullptr) { return root; }
|
||||
throw std::logic_error("Cannot dereference an empty wrapper");
|
||||
}
|
||||
auto operator->() -> FB* {
|
||||
if (auto* root = Get(); root != nullptr) { return root; }
|
||||
throw std::logic_error("Cannot dereference an empty wrapper");
|
||||
}
|
||||
|
||||
[[nodiscard]] auto SaveTo(const std::string& path) const -> bool {
|
||||
return FilesystemUtils::AtomicallySaveToPath(path, ToByteVector());
|
||||
@@ -123,6 +147,7 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] auto ToByteString() const -> string {
|
||||
if (buffer == nullptr || size == 0) { return {}; }
|
||||
string result;
|
||||
result.reserve(sizeof(size_t) + size);
|
||||
result.append(reinterpret_cast<const char*>(&offset), sizeof(size_t));
|
||||
@@ -138,25 +163,23 @@ public:
|
||||
}
|
||||
static auto FromByteString(const string& str) -> Wrapper {
|
||||
if (str.size() < sizeof(size_t)) {
|
||||
return Wrapper(); // Return empty wrapper for invalid data
|
||||
throw std::invalid_argument("Cannot construct wrapper from truncated bytes");
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
std::memcpy(&offset, str.data(), sizeof(size_t));
|
||||
|
||||
size_t size = str.size() - sizeof(size_t);
|
||||
auto toReturn = Wrapper(
|
||||
return Wrapper(
|
||||
const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(str.data())) + sizeof(size_t),
|
||||
offset,
|
||||
size);
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto ToByteVector() const -> byte_vector { return byte_vector(ToByteString()); }
|
||||
static auto FromByteVector(const byte_vector& bv) -> Wrapper {
|
||||
if (bv.size() < sizeof(size_t)) {
|
||||
return Wrapper(); // Return empty wrapper for invalid data
|
||||
throw std::invalid_argument("Cannot construct wrapper from truncated bytes");
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
@@ -164,9 +187,7 @@ public:
|
||||
|
||||
size_t size = bv.size() - sizeof(size_t);
|
||||
// Note: CopyBuffer will handle const_cast safely by making a copy
|
||||
auto toReturn = Wrapper(const_cast<uint8_t*>(bv.data()) + sizeof(size_t), offset, size);
|
||||
|
||||
return toReturn;
|
||||
return Wrapper(const_cast<uint8_t*>(bv.data()) + sizeof(size_t), offset, size);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,29 @@
|
||||
|
||||
namespace shardok::fb {
|
||||
|
||||
using namespace net::eagle0::shardok::storage::fb;
|
||||
constexpr float kExplicitMoraleSentinel = -1.0f;
|
||||
|
||||
using flatbuffers::FlatBufferBuilder;
|
||||
using flatbuffers::Offset;
|
||||
using net::eagle0::shardok::storage::fb::AlliedPlayer;
|
||||
using net::eagle0::shardok::storage::fb::CreateGameStatusDirect;
|
||||
using net::eagle0::shardok::storage::fb::EndGameCondition;
|
||||
using net::eagle0::shardok::storage::fb::GameStateBuilder;
|
||||
using net::eagle0::shardok::storage::fb::HexMapDirection_NORTHEAST;
|
||||
using net::eagle0::shardok::storage::fb::PlayerInfoBuilder;
|
||||
using net::eagle0::shardok::storage::fb::Profession_NECROMANCER;
|
||||
using net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT;
|
||||
using net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT;
|
||||
using net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT;
|
||||
using net::eagle0::shardok::storage::fb::VictoryCondition;
|
||||
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES;
|
||||
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_LAST_ALLIANCE_STANDING;
|
||||
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_LAST_PLAYER_STANDING;
|
||||
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_MOST_TROOPS_STANDING;
|
||||
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS;
|
||||
using net::eagle0::shardok::storage::fb::WeatherConditions_CLOUDS;
|
||||
using net::eagle0::shardok::storage::fb::Wind;
|
||||
namespace GameStatusNs = net::eagle0::shardok::storage::fb::GameStatus_;
|
||||
using GameStatusProto = net::eagle0::shardok::common::GameStatus;
|
||||
|
||||
const std::vector<PlayerId> kWinningPids{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
|
||||
@@ -136,7 +158,7 @@ auto SetupInitialGameState(
|
||||
EndGameCondition endGameCondition;
|
||||
auto gameStatusOffset = CreateGameStatusDirect(
|
||||
fbb,
|
||||
GameStatus_::State_SET_UP,
|
||||
GameStatusNs::State_SET_UP,
|
||||
nullptr,
|
||||
&kWinningPids,
|
||||
&endGameCondition);
|
||||
@@ -171,7 +193,8 @@ auto SetupInitialGameState(
|
||||
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
|
||||
if (modifiedUnit.can_archery()) {
|
||||
modifiedUnit.mutate_volleys_remaining(
|
||||
int32_t(modifiedUnit.battalion().armament() *
|
||||
static_cast<int32_t>(
|
||||
modifiedUnit.battalion().armament() *
|
||||
settings.Backing().armament_to_volleys()) +
|
||||
settings.GetBattalionType(modifiedUnit.battalion().type())->bonusVolleys);
|
||||
}
|
||||
@@ -179,8 +202,11 @@ auto SetupInitialGameState(
|
||||
RequiredMonthlyFood(modifiedUnit, settings) *
|
||||
foodPercentByPid[modifiedUnit.player_id()]);
|
||||
auto baseMorale = BaseMorale(modifiedUnit);
|
||||
auto startingMorale = modifiedUnit.battalion().base_morale() == kExplicitMoraleSentinel
|
||||
? modifiedUnit.battalion().morale()
|
||||
: baseMorale;
|
||||
modifiedUnit.mutable_battalion().mutate_base_morale(baseMorale);
|
||||
modifiedUnit.mutable_battalion().mutate_morale(baseMorale);
|
||||
modifiedUnit.mutable_battalion().mutate_morale(startingMorale);
|
||||
|
||||
if (modifiedUnit.status() !=
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT) {
|
||||
@@ -293,9 +319,9 @@ auto ToPlayerInfoProto(const PlayerInfo* fbPI) -> PlayerInfoProto {
|
||||
auto ToWeatherProto(const Weather* fbW) -> net::eagle0::shardok::common::Weather {
|
||||
net::eagle0::shardok::common::Weather protoWeather;
|
||||
protoWeather.set_conditions(
|
||||
(net::eagle0::shardok::common::Weather_Conditions)fbW->conditions());
|
||||
static_cast<net::eagle0::shardok::common::Weather_Conditions>(fbW->conditions()));
|
||||
protoWeather.mutable_wind()->set_direction(
|
||||
(net::eagle0::shardok::common::HexMapDirection)fbW->wind().direction());
|
||||
static_cast<net::eagle0::shardok::common::HexMapDirection>(fbW->wind().direction()));
|
||||
protoWeather.mutable_wind()->set_speed_in_mph(fbW->wind().speed_in_mph());
|
||||
|
||||
return protoWeather;
|
||||
|
||||
@@ -13,20 +13,28 @@
|
||||
namespace shardok::fb {
|
||||
|
||||
using TerrainProto = net::eagle0::shardok::common::Terrain;
|
||||
using net::eagle0::shardok::storage::fb::Coords;
|
||||
using net::eagle0::shardok::storage::fb::CreateStartingPositionList;
|
||||
using net::eagle0::shardok::storage::fb::CreateStartingPositionListDirect;
|
||||
using net::eagle0::shardok::storage::fb::HexMapBuilder;
|
||||
using net::eagle0::shardok::storage::fb::MonthlyWeather;
|
||||
using net::eagle0::shardok::storage::fb::SingleModifier;
|
||||
using net::eagle0::shardok::storage::fb::StartingPositionList;
|
||||
using net::eagle0::shardok::storage::fb::Terrain;
|
||||
using net::eagle0::shardok::storage::fb::TileModifier;
|
||||
namespace TerrainNs = net::eagle0::shardok::storage::fb::Terrain_;
|
||||
|
||||
using namespace net::eagle0::shardok::storage::fb;
|
||||
|
||||
auto ConvertType(const net::eagle0::shardok::common::Terrain_Type protoType) -> Terrain_::Type {
|
||||
using namespace Terrain_;
|
||||
auto ConvertType(const net::eagle0::shardok::common::Terrain_Type protoType) -> TerrainNs::Type {
|
||||
switch (protoType) {
|
||||
case net::eagle0::shardok::common::Terrain_Type_CITY: return Type_CITY;
|
||||
case net::eagle0::shardok::common::Terrain_Type_FOREST: return Type_FOREST;
|
||||
case net::eagle0::shardok::common::Terrain_Type_HILL: return Type_HILL;
|
||||
case net::eagle0::shardok::common::Terrain_Type_MOUNTAIN: return Type_MOUNTAIN;
|
||||
case net::eagle0::shardok::common::Terrain_Type_PLAINS: return Type_PLAINS;
|
||||
case net::eagle0::shardok::common::Terrain_Type_RIVER: return Type_RIVER;
|
||||
case net::eagle0::shardok::common::Terrain_Type_STILL_WATER: return Type_STILL_WATER;
|
||||
case net::eagle0::shardok::common::Terrain_Type_SWAMP: return Type_SWAMP;
|
||||
case net::eagle0::shardok::common::Terrain_Type_CITY: return TerrainNs::Type_CITY;
|
||||
case net::eagle0::shardok::common::Terrain_Type_FOREST: return TerrainNs::Type_FOREST;
|
||||
case net::eagle0::shardok::common::Terrain_Type_HILL: return TerrainNs::Type_HILL;
|
||||
case net::eagle0::shardok::common::Terrain_Type_MOUNTAIN: return TerrainNs::Type_MOUNTAIN;
|
||||
case net::eagle0::shardok::common::Terrain_Type_PLAINS: return TerrainNs::Type_PLAINS;
|
||||
case net::eagle0::shardok::common::Terrain_Type_RIVER: return TerrainNs::Type_RIVER;
|
||||
case net::eagle0::shardok::common::Terrain_Type_STILL_WATER:
|
||||
return TerrainNs::Type_STILL_WATER;
|
||||
case net::eagle0::shardok::common::Terrain_Type_SWAMP: return TerrainNs::Type_SWAMP;
|
||||
|
||||
case net::eagle0::shardok::common::Terrain_Type_UNKNOWN:
|
||||
case net::eagle0::shardok::common::Terrain_Type_NONE:
|
||||
@@ -208,7 +216,7 @@ auto ConvertHexMapProto(
|
||||
}
|
||||
|
||||
auto CopyStartingPositionsList(FlatBufferBuilder& fbb, const StartingPositionList* spl) {
|
||||
std::vector<Coords> coordsVec = {};
|
||||
std::vector<Coords> coordsVec{};
|
||||
|
||||
if (spl && spl->positions()) {
|
||||
coordsVec.reserve(spl->positions()->size());
|
||||
@@ -232,7 +240,7 @@ auto CopyHexMap(const HexMap* hexMap) -> HexMapW {
|
||||
for (const auto* w : *hexMap->monthly_weather()) weatherVec.push_back(*w);
|
||||
const auto weatherOffset = fbb.CreateVectorOfStructs(weatherVec);
|
||||
|
||||
std::vector<Offset<StartingPositionList>> asplVec = {};
|
||||
std::vector<Offset<StartingPositionList>> asplVec{};
|
||||
asplVec.reserve(hexMap->attacker_starting_positions()->size());
|
||||
for (const auto* spl : *hexMap->attacker_starting_positions()) {
|
||||
asplVec.push_back(CopyStartingPositionsList(fbb, spl));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_library")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
@@ -28,6 +28,14 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "coords_set_benchmark",
|
||||
srcs = ["CoordsSetBenchmark.cpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [":coords_set"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "hex_map_direction",
|
||||
hdrs = ["HexMapDirection.hpp"],
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace std {
|
||||
template<>
|
||||
struct hash<net::eagle0::shardok::common::Coords> {
|
||||
[[nodiscard]] auto operator()(const net::eagle0::shardok::common::Coords &c) const -> size_t {
|
||||
return size_t(c.row() << 16 | c.column());
|
||||
return static_cast<size_t>(c.row() << 16 | c.column());
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
#define EAGLE0_SHARDOK_LIBRARY_MAP_COORDS_SET_HPP
|
||||
|
||||
#include <bit>
|
||||
#include <functional>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -17,7 +20,11 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
#ifndef NDEBUG
|
||||
constexpr bool kCoordsSetIndexChecks = true;
|
||||
#else
|
||||
constexpr bool kCoordsSetIndexChecks = false;
|
||||
#endif
|
||||
|
||||
constexpr size_t STANDARD_SIZE = size_t{12} * 14;
|
||||
|
||||
@@ -28,13 +35,28 @@ class CoordsSet {
|
||||
private:
|
||||
BackingBitset store;
|
||||
|
||||
int mapWidth;
|
||||
int mapHeight;
|
||||
uint8_t columnCount;
|
||||
uint8_t rowCount;
|
||||
|
||||
uint32_t indexCount;
|
||||
[[maybe_unused]] uint16_t indexCount;
|
||||
|
||||
void AssertCoordsInBounds([[maybe_unused]] const int row, [[maybe_unused]] const int column)
|
||||
const {
|
||||
if constexpr (kCoordsSetIndexChecks) {
|
||||
assert(row >= 0);
|
||||
assert(column >= 0);
|
||||
assert(row < rowCount);
|
||||
assert(column < columnCount);
|
||||
}
|
||||
}
|
||||
|
||||
void AssertIndexInBounds([[maybe_unused]] const uint32_t idx) const {
|
||||
if constexpr (kCoordsSetIndexChecks) { assert(idx < indexCount); }
|
||||
}
|
||||
|
||||
[[nodiscard]] auto Index(const int row, const int column) const -> int {
|
||||
return (row * mapWidth) + column;
|
||||
AssertCoordsInBounds(row, column);
|
||||
return (row * columnCount) + column;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto Index(const Coords& c) const -> int { return Index(c.row(), c.column()); }
|
||||
@@ -49,18 +71,25 @@ private:
|
||||
};
|
||||
|
||||
void Remove(uint32_t idx) {
|
||||
if (kCoordsSetIndexChecks) { assert(idx < indexCount); }
|
||||
AssertIndexInBounds(idx);
|
||||
store.Unset(idx);
|
||||
}
|
||||
[[nodiscard]] auto Contains(uint32_t idx) const -> bool {
|
||||
if (kCoordsSetIndexChecks) { assert(idx < indexCount); }
|
||||
AssertIndexInBounds(idx);
|
||||
return store.IsSet(idx);
|
||||
}
|
||||
|
||||
public:
|
||||
CoordsSet(int w, int h) : mapWidth(w), mapHeight(h), indexCount(w * h) {
|
||||
if (kCoordsSetIndexChecks) {
|
||||
assert(static_cast<size_t>(w) * static_cast<size_t>(h) < STANDARD_SIZE);
|
||||
CoordsSet(const int w, const int h)
|
||||
: columnCount(static_cast<uint8_t>(w)),
|
||||
rowCount(static_cast<uint8_t>(h)),
|
||||
indexCount(static_cast<uint16_t>(w * h)) {
|
||||
if constexpr (kCoordsSetIndexChecks) {
|
||||
assert(w >= 0);
|
||||
assert(h >= 0);
|
||||
assert(w <= std::numeric_limits<uint8_t>::max());
|
||||
assert(h <= std::numeric_limits<uint8_t>::max());
|
||||
assert(static_cast<size_t>(w) * static_cast<size_t>(h) <= STANDARD_SIZE);
|
||||
}
|
||||
clear();
|
||||
}
|
||||
@@ -74,15 +103,20 @@ public:
|
||||
auto operator=(const CoordsSet& rhs) -> CoordsSet& = default;
|
||||
auto operator=(CoordsSet&& rhs) -> CoordsSet& = default;
|
||||
|
||||
auto operator==(const CoordsSet& rhs) const -> bool {
|
||||
if (mapWidth != rhs.mapWidth) return false;
|
||||
if (mapHeight != rhs.mapHeight) return false;
|
||||
[[nodiscard]] auto operator==(const CoordsSet& rhs) const -> bool {
|
||||
if (columnCount != rhs.columnCount) return false;
|
||||
if (rowCount != rhs.rowCount) return false;
|
||||
return store == rhs.store;
|
||||
}
|
||||
|
||||
void Add(const Coords& c) { AddIndex(Index(c)); }
|
||||
void Add(const int row, const int column) { AddIndex(Index(row, column)); }
|
||||
void AddIndex(const uint32_t idx) { store.Set(idx); }
|
||||
void Add(const Coords& c) { AddMapIndexUnchecked(Index(c)); }
|
||||
void Add(const int row, const int column) { AddMapIndexUnchecked(Index(row, column)); }
|
||||
|
||||
// For callers that already have row * column_count + column.
|
||||
void AddMapIndexUnchecked(const uint32_t idx) {
|
||||
AssertIndexInBounds(idx);
|
||||
store.Set(idx);
|
||||
}
|
||||
|
||||
// Always succeeds
|
||||
void Remove(const Coords& c) { Remove(Index(c)); }
|
||||
@@ -101,13 +135,15 @@ public:
|
||||
|
||||
[[nodiscard]] auto empty() const -> bool { return store.empty(); }
|
||||
|
||||
[[nodiscard]] auto ColumnCount() const -> int { return columnCount; }
|
||||
|
||||
// Should only be used for debugging.
|
||||
[[nodiscard]] auto as_vector() const -> std::vector<Coords> {
|
||||
return std::vector<Coords>{this->begin(), this->end()};
|
||||
}
|
||||
|
||||
auto operator+=(const CoordsSet& rhs) -> CoordsSet& {
|
||||
if (mapWidth != rhs.mapWidth || mapHeight != rhs.mapHeight) {
|
||||
if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
|
||||
throw MismatchException("mismatched sizes");
|
||||
}
|
||||
|
||||
@@ -115,14 +151,14 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto operator+(const CoordsSet& rhs) const -> CoordsSet {
|
||||
[[nodiscard]] auto operator+(const CoordsSet& rhs) const -> CoordsSet {
|
||||
CoordsSet cs = *this;
|
||||
cs += rhs;
|
||||
return cs;
|
||||
}
|
||||
|
||||
auto operator-=(const CoordsSet& rhs) -> CoordsSet& {
|
||||
if (mapWidth != rhs.mapWidth || mapHeight != rhs.mapHeight) {
|
||||
if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
|
||||
throw MismatchException("mismatched sizes");
|
||||
}
|
||||
|
||||
@@ -130,15 +166,25 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto operator-(const CoordsSet& rhs) const -> CoordsSet {
|
||||
[[nodiscard]] auto operator-(const CoordsSet& rhs) const -> CoordsSet {
|
||||
CoordsSet cs = *this;
|
||||
cs -= rhs;
|
||||
return cs;
|
||||
}
|
||||
|
||||
void Filter(const std::function<bool(int, int)>& f) {
|
||||
for (uint32_t i = 0; i < indexCount; ++i) {
|
||||
if (Contains(i) && !f(i / mapWidth, i % mapWidth)) { Remove(i); }
|
||||
template<class Func>
|
||||
void Filter(Func&& f) {
|
||||
if (size() * 4 > indexCount) {
|
||||
for (uint32_t i = 0; i < indexCount; ++i) {
|
||||
if (Contains(i) && !f(i / columnCount, i % columnCount)) { Remove(i); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto iter = store.begin(); iter != store.end();) {
|
||||
const uint32_t i = static_cast<uint32_t>(*iter);
|
||||
++iter;
|
||||
if (!f(i / columnCount, i % columnCount)) { Remove(i); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,9 +192,9 @@ public:
|
||||
public:
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using value_type = Coords;
|
||||
using difference_type = int;
|
||||
using pointer = Coords*;
|
||||
using reference = Coords&;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using pointer = void;
|
||||
using reference = Coords;
|
||||
|
||||
private:
|
||||
BackingBitset::Iter iterator;
|
||||
@@ -158,15 +204,28 @@ public:
|
||||
public:
|
||||
Iter(const CoordsSet& cs, BackingBitset::Iter iter) : iterator(iter), cs(cs) {}
|
||||
|
||||
auto operator==(const Iter& rhs) const -> bool { return iterator == rhs.iterator; }
|
||||
auto operator!=(const Iter& rhs) const -> bool { return iterator != rhs.iterator; }
|
||||
void operator++() { ++iterator; }
|
||||
[[nodiscard]] auto operator==(const Iter& rhs) const -> bool {
|
||||
return iterator == rhs.iterator;
|
||||
}
|
||||
[[nodiscard]] auto operator!=(const Iter& rhs) const -> bool {
|
||||
return iterator != rhs.iterator;
|
||||
}
|
||||
auto operator++() -> Iter& {
|
||||
++iterator;
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto operator*() const -> Coords {
|
||||
auto operator++(int) -> Iter {
|
||||
Iter previous = *this;
|
||||
++(*this);
|
||||
return previous;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto operator*() const -> Coords {
|
||||
const size_t index = *iterator;
|
||||
|
||||
const int row = index / cs.mapWidth;
|
||||
const int column = index % cs.mapWidth;
|
||||
const int row = static_cast<int>(index / static_cast<size_t>(cs.columnCount));
|
||||
const int column = static_cast<int>(index % static_cast<size_t>(cs.columnCount));
|
||||
return Coords(row, column);
|
||||
}
|
||||
};
|
||||
@@ -185,25 +244,21 @@ public:
|
||||
[[nodiscard]] auto end() const -> BackingBitset::Iter { return cs.store.end(); }
|
||||
};
|
||||
[[nodiscard]] auto indexIterator() const -> IndexIterator { return IndexIterator(*this); }
|
||||
|
||||
[[nodiscard]] auto ToString() const -> std::string {
|
||||
return std::string(reinterpret_cast<char const*>(&store), STANDARD_SIZE);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
namespace common {
|
||||
static inline auto Contains(const shardok::CoordsSet& container, const shardok::Coords& elt)
|
||||
-> bool {
|
||||
[[nodiscard]] static inline auto Contains(
|
||||
const shardok::CoordsSet& container,
|
||||
const shardok::Coords& elt) -> bool {
|
||||
return container.Contains(elt);
|
||||
}
|
||||
|
||||
template<class Func>
|
||||
void FilterInPlace(shardok::CoordsSet& container, Func fn) {
|
||||
for (const shardok::Coords& c : container) {
|
||||
if (!fn(c)) container.Remove(c);
|
||||
}
|
||||
container.Filter(
|
||||
[&fn](const int row, const int column) { return fn(shardok::Coords(row, column)); });
|
||||
}
|
||||
} // namespace common
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
constexpr int kColumnCount = 12;
|
||||
constexpr int kRowCount = 14;
|
||||
constexpr int kIndexCount = kColumnCount * kRowCount;
|
||||
|
||||
volatile uint64_t benchmarkSink = 0;
|
||||
|
||||
template<class T>
|
||||
void DoNotOptimize(const T& value) {
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
asm volatile("" : : "g"(&value) : "memory");
|
||||
#else
|
||||
benchmarkSink ^= reinterpret_cast<uintptr_t>(&value);
|
||||
#endif
|
||||
}
|
||||
|
||||
struct BenchmarkResult {
|
||||
std::string_view name;
|
||||
int iterations;
|
||||
std::chrono::nanoseconds elapsed;
|
||||
uint64_t checksum;
|
||||
};
|
||||
|
||||
void Consume(uint64_t checksum) { benchmarkSink = checksum; }
|
||||
|
||||
[[nodiscard]] auto MakePatternedCoordsSet(const int step, const int offset = 0) -> CoordsSet {
|
||||
CoordsSet coords(kColumnCount, kRowCount);
|
||||
for (int index = offset; index < kIndexCount; index += step) {
|
||||
coords.AddMapIndexUnchecked(static_cast<uint32_t>(index));
|
||||
}
|
||||
return coords;
|
||||
}
|
||||
|
||||
template<class Func>
|
||||
[[nodiscard]] auto RunBenchmark(std::string_view name, const int iterations, Func func)
|
||||
-> BenchmarkResult {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const uint64_t checksum = func(iterations);
|
||||
const auto end = std::chrono::steady_clock::now();
|
||||
Consume(checksum);
|
||||
return BenchmarkResult{
|
||||
name,
|
||||
iterations,
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(end - start),
|
||||
checksum};
|
||||
}
|
||||
|
||||
void PrintResult(const BenchmarkResult& result) {
|
||||
const double nsPerIteration =
|
||||
static_cast<double>(result.elapsed.count()) / static_cast<double>(result.iterations);
|
||||
std::cout << std::left << std::setw(28) << result.name << " iterations=" << std::right
|
||||
<< std::setw(10) << result.iterations << " ns/op=" << std::fixed
|
||||
<< std::setprecision(2) << std::setw(10) << nsPerIteration
|
||||
<< " checksum=" << result.checksum << '\n';
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkCopies(const int iterations) -> uint64_t {
|
||||
const CoordsSet source = MakePatternedCoordsSet(3);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet copy = source;
|
||||
DoNotOptimize(copy);
|
||||
checksum += copy.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkUnions(const int iterations) -> uint64_t {
|
||||
const CoordsSet left = MakePatternedCoordsSet(3);
|
||||
const CoordsSet right = MakePatternedCoordsSet(5, 1);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet combined = left;
|
||||
combined += right;
|
||||
DoNotOptimize(combined);
|
||||
checksum += combined.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkSubtractions(const int iterations) -> uint64_t {
|
||||
const CoordsSet full = MakePatternedCoordsSet(1);
|
||||
const CoordsSet removed = MakePatternedCoordsSet(4);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet remaining = full;
|
||||
remaining -= removed;
|
||||
DoNotOptimize(remaining);
|
||||
checksum += remaining.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkContains(const int iterations) -> uint64_t {
|
||||
const CoordsSet coords = MakePatternedCoordsSet(3);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
const int index = i % kIndexCount;
|
||||
checksum += coords.Contains(index / kColumnCount, index % kColumnCount) ? 1 : 0;
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkCoordinateIteration(const int iterations) -> uint64_t {
|
||||
const CoordsSet coords = MakePatternedCoordsSet(3);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
for (const auto& coord : coords) {
|
||||
checksum += static_cast<uint64_t>(coord.row() + coord.column());
|
||||
}
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkIndexIteration(const int iterations) -> uint64_t {
|
||||
const CoordsSet coords = MakePatternedCoordsSet(3);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
for (const auto index : coords.indexIterator()) { checksum += index; }
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkDenseFilter(const int iterations) -> uint64_t {
|
||||
const CoordsSet source = MakePatternedCoordsSet(1);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet filtered = source;
|
||||
filtered.Filter([](const int row, const int column) { return (row + column) % 3 == 0; });
|
||||
DoNotOptimize(filtered);
|
||||
checksum += filtered.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkSparseFilter(const int iterations) -> uint64_t {
|
||||
const CoordsSet source = MakePatternedCoordsSet(7);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet filtered = source;
|
||||
filtered.Filter([](const int row, const int column) { return (row + column) % 3 == 0; });
|
||||
DoNotOptimize(filtered);
|
||||
checksum += filtered.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace shardok
|
||||
|
||||
auto main() -> int {
|
||||
using shardok::BenchmarkContains;
|
||||
using shardok::BenchmarkCoordinateIteration;
|
||||
using shardok::BenchmarkCopies;
|
||||
using shardok::BenchmarkDenseFilter;
|
||||
using shardok::BenchmarkIndexIteration;
|
||||
using shardok::BenchmarkSparseFilter;
|
||||
using shardok::BenchmarkSubtractions;
|
||||
using shardok::BenchmarkUnions;
|
||||
using shardok::CoordsSet;
|
||||
using shardok::PrintResult;
|
||||
using shardok::RunBenchmark;
|
||||
|
||||
std::cout << "CoordsSet benchmark\n";
|
||||
std::cout << "sizeof(CoordsSet)=" << sizeof(CoordsSet)
|
||||
<< " alignof(CoordsSet)=" << alignof(CoordsSet) << '\n';
|
||||
|
||||
PrintResult(RunBenchmark("copy", 5'000'000, BenchmarkCopies));
|
||||
PrintResult(RunBenchmark("union", 3'000'000, BenchmarkUnions));
|
||||
PrintResult(RunBenchmark("subtraction", 3'000'000, BenchmarkSubtractions));
|
||||
PrintResult(RunBenchmark("contains", 20'000'000, BenchmarkContains));
|
||||
PrintResult(RunBenchmark("coord iteration", 1'000'000, BenchmarkCoordinateIteration));
|
||||
PrintResult(RunBenchmark("index iteration", 1'000'000, BenchmarkIndexIteration));
|
||||
PrintResult(RunBenchmark("dense filter", 500'000, BenchmarkDenseFilter));
|
||||
PrintResult(RunBenchmark("sparse filter", 500'000, BenchmarkSparseFilter));
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ using TileModifierProto = net::eagle0::shardok::common::TileModifier;
|
||||
|
||||
SingleModifier MakeSingleModifier(const std::optional<double>& integ = std::nullopt);
|
||||
|
||||
bool operator==(const SingleModifier& lhs, const SingleModifier& rhs);
|
||||
[[nodiscard]] bool operator==(const SingleModifier& lhs, const SingleModifier& rhs);
|
||||
|
||||
TileModifierProto MakeTileModifier(bool onFire = false);
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
namespace shardok {
|
||||
using TileModifierWithCoords = net::eagle0::shardok::common::TileModifierWithCoords;
|
||||
|
||||
auto MakeTmc(const Coords &coordinates, const TileModifierProto &modifier)
|
||||
[[nodiscard]] auto MakeTmc(const Coords &coordinates, const TileModifierProto &modifier)
|
||||
-> TileModifierWithCoords;
|
||||
|
||||
auto MakeTmc(
|
||||
[[nodiscard]] auto MakeTmc(
|
||||
const Coords &coordinates,
|
||||
const net::eagle0::shardok::storage::fb::TileModifier &modifier) -> TileModifierWithCoords;
|
||||
|
||||
|
||||
@@ -92,7 +92,8 @@ public:
|
||||
net::eagle0::shardok::storage::fb::WeatherConditions conditions) const -> int;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto GetGetter() const -> Getter { return Getter(*this); }
|
||||
[[nodiscard]] auto GetGetter() const & -> Getter { return Getter(*this); }
|
||||
[[nodiscard]] auto GetGetter() const && -> Getter = delete;
|
||||
|
||||
class Setter {
|
||||
private:
|
||||
@@ -118,7 +119,8 @@ public:
|
||||
void SetFirePropensityFromWeather(Weather::Conditions wc, int propensity);
|
||||
};
|
||||
|
||||
auto GetSetter() -> Setter { return Setter(*this); }
|
||||
[[nodiscard]] auto GetSetter() & -> Setter { return Setter(*this); }
|
||||
[[nodiscard]] auto GetSetter() && -> Setter = delete;
|
||||
|
||||
virtual ~GameSettings() = default;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace shardok {
|
||||
|
||||
class SettingsLoader {
|
||||
public:
|
||||
static auto LoadSettings() -> std::shared_ptr<GameSettings>;
|
||||
[[nodiscard]] static auto LoadSettings() -> std::shared_ptr<GameSettings>;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
#ifndef EAGLE0_SHARDOK_LIBRARY_UTIL_ACTION_RESULT_FLATBUFFER_HELPERS_HPP
|
||||
#define EAGLE0_SHARDOK_LIBRARY_UTIL_ACTION_RESULT_FLATBUFFER_HELPERS_HPP
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
|
||||
@@ -16,13 +19,30 @@ using ActionResult = net::eagle0::shardok::storage::ActionResult;
|
||||
using ResolvedUnit = net::eagle0::shardok::storage::ResolvedUnit;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
static_assert(std::is_trivially_copyable_v<Unit>);
|
||||
|
||||
[[nodiscard]] static inline auto UnitToBytes(const Unit& unit) -> std::string {
|
||||
std::string bytes(sizeof(Unit), '\0');
|
||||
std::memcpy(bytes.data(), &unit, sizeof(Unit));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline auto UnitFromBytes(const std::string& unitBytes) -> Unit {
|
||||
if (unitBytes.size() != sizeof(Unit)) {
|
||||
throw std::invalid_argument("Unexpected unit byte length");
|
||||
}
|
||||
|
||||
Unit unit{};
|
||||
std::memcpy(&unit, unitBytes.data(), sizeof(Unit));
|
||||
return unit;
|
||||
}
|
||||
|
||||
static inline void AddChangedUnit(ActionResult& result, const Unit& unit) {
|
||||
result.add_changed_units_fb(&unit, sizeof(Unit));
|
||||
}
|
||||
|
||||
static inline void AddUnitToResolved(ResolvedUnit& resolvedUnit, const Unit& unit) {
|
||||
*resolvedUnit.mutable_unit_bytes() =
|
||||
std::string(reinterpret_cast<const char*>(&unit), sizeof(Unit));
|
||||
*resolvedUnit.mutable_unit_bytes() = UnitToBytes(unit);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -6,6 +6,7 @@ cc_library(
|
||||
hdrs = ["ActionResultFlatbufferHelpers.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
|
||||
],
|
||||
|
||||
@@ -306,10 +306,9 @@ auto MakeActionResultFb(const ActionType type, const MeleeResultFb &results, con
|
||||
ar.mutable_actor()->set_value(results.attackerAfter.unit_id());
|
||||
ar.mutable_target_unit()->set_value(results.defenderAfter.unit_id());
|
||||
|
||||
AddChangedUnit(ar, results.attackerAfter);
|
||||
auto *attCU = reinterpret_cast<Unit *>(
|
||||
ar.mutable_changed_units_fb(ar.changed_units_fb_size() - 1)->data());
|
||||
if (resetZoc) attCU->mutate_has_moved_in_zoc(false);
|
||||
Unit attackerAfter = results.attackerAfter;
|
||||
if (resetZoc) attackerAfter.mutate_has_moved_in_zoc(false);
|
||||
AddChangedUnit(ar, attackerAfter);
|
||||
|
||||
AddChangedUnit(ar, results.defenderAfter);
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ const std::vector<CoordsSet> HexMapUtils::standardCoords =
|
||||
|
||||
auto HexMapUtils::ComputeAllStandardAdjacentCoords() -> std::vector<CoordsSet> {
|
||||
std::vector<CoordsSet> arr;
|
||||
arr.reserve(size_t(STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS));
|
||||
arr.reserve(static_cast<size_t>(STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS));
|
||||
|
||||
for (int index = 0; index < STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS; ++index) {
|
||||
arr.push_back(ComputeAdjacentCoords(
|
||||
@@ -127,25 +127,25 @@ auto HexMapUtils::ComputeAdjacentCoords(
|
||||
|
||||
// Row above
|
||||
if (origin.row() > 0) {
|
||||
cs.AddIndex(originIndex - columnCount);
|
||||
cs.AddMapIndexUnchecked(originIndex - columnCount);
|
||||
|
||||
if (origin.row() % 2 == 0) {
|
||||
if (origin.column() > 0) { cs.AddIndex(originIndex - columnCount - 1); }
|
||||
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex - columnCount - 1); }
|
||||
} else if (origin.column() < columnCount - 1) {
|
||||
cs.AddIndex(originIndex - columnCount + 1);
|
||||
cs.AddMapIndexUnchecked(originIndex - columnCount + 1);
|
||||
}
|
||||
}
|
||||
// This row
|
||||
if (origin.column() > 0) { cs.AddIndex(originIndex - 1); }
|
||||
if (origin.column() < columnCount - 1) { cs.AddIndex(originIndex + 1); }
|
||||
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex - 1); }
|
||||
if (origin.column() < columnCount - 1) { cs.AddMapIndexUnchecked(originIndex + 1); }
|
||||
// Row below
|
||||
if (origin.row() < rowCount - 1) {
|
||||
cs.AddIndex(originIndex + columnCount);
|
||||
cs.AddMapIndexUnchecked(originIndex + columnCount);
|
||||
|
||||
if (origin.row() % 2 == 0) {
|
||||
if (origin.column() > 0) { cs.AddIndex(originIndex + columnCount - 1); }
|
||||
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex + columnCount - 1); }
|
||||
} else if (origin.column() < columnCount - 1) {
|
||||
cs.AddIndex(originIndex + columnCount + 1);
|
||||
cs.AddMapIndexUnchecked(originIndex + columnCount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ struct alignas(16) AdjacentTile {
|
||||
|
||||
AdjacentTile(const Coords co, const HexMapDirection dir) : directionTo(dir), coords(co) {}
|
||||
|
||||
auto operator==(const AdjacentTile &rhs) const -> bool {
|
||||
[[nodiscard]] auto operator==(const AdjacentTile &rhs) const -> bool {
|
||||
return coords == rhs.coords && directionTo == rhs.directionTo;
|
||||
}
|
||||
};
|
||||
@@ -73,21 +73,21 @@ public:
|
||||
|
||||
[[nodiscard]] auto AllCastleCoords(const HexMap *hexMap) -> CoordsSet;
|
||||
|
||||
auto HasForestAccess(
|
||||
[[nodiscard]] auto HasForestAccess(
|
||||
const Coords &coords,
|
||||
const Units *units,
|
||||
const HexMap *hexMap,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
PlayerId player) -> bool;
|
||||
|
||||
auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
|
||||
auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
|
||||
[[nodiscard]] auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
|
||||
[[nodiscard]] auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
|
||||
// Overload for const HexMap - uses const_cast internally. Safe when the underlying buffer is
|
||||
// mutable.
|
||||
auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
|
||||
[[nodiscard]] auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
|
||||
|
||||
auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
|
||||
auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool;
|
||||
[[nodiscard]] auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
|
||||
[[nodiscard]] auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool;
|
||||
|
||||
[[nodiscard]] auto GetTilesAcrossWater(const HexMap *map, const Coords &coords) -> CoordsSet;
|
||||
|
||||
@@ -100,7 +100,8 @@ auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool
|
||||
[[nodiscard]] auto TwoAwayTilesUnblockedByMountains(const HexMap *map, const Coords &coords)
|
||||
-> CoordsSet;
|
||||
|
||||
static inline auto Occupant(const Units *units, const Coords &coords) -> const Unit * {
|
||||
[[nodiscard]] static inline auto Occupant(const Units *units, const Coords &coords)
|
||||
-> const Unit * {
|
||||
for (const auto &unit : *units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location() == coords) {
|
||||
@@ -111,7 +112,8 @@ static inline auto Occupant(const Units *units, const Coords &coords) -> const U
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static inline auto Occupant(const Units &units, const Coords &coords) -> const Unit * {
|
||||
[[nodiscard]] static inline auto Occupant(const Units &units, const Coords &coords)
|
||||
-> const Unit * {
|
||||
for (const auto &unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location() == coords) {
|
||||
@@ -122,7 +124,7 @@ static inline auto Occupant(const Units &units, const Coords &coords) -> const U
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static inline auto Occupants(
|
||||
[[nodiscard]] static inline auto Occupants(
|
||||
const std::vector<const Unit *> &units,
|
||||
const int rowCount,
|
||||
const int columnCount) -> std::vector<const Unit *> {
|
||||
@@ -138,7 +140,7 @@ static inline auto Occupants(
|
||||
return positions;
|
||||
}
|
||||
|
||||
static inline auto Occupants(
|
||||
[[nodiscard]] static inline auto Occupants(
|
||||
const flatbuffers::Vector<const Unit *> &units,
|
||||
const int rowCount,
|
||||
const int columnCount) -> std::vector<const Unit *> {
|
||||
@@ -154,8 +156,9 @@ static inline auto Occupants(
|
||||
return positions;
|
||||
}
|
||||
|
||||
static inline auto Occupant(const std::vector<const Unit *> &units, const Coords &coords)
|
||||
-> const Unit * {
|
||||
[[nodiscard]] static inline auto Occupant(
|
||||
const std::vector<const Unit *> &units,
|
||||
const Coords &coords) -> const Unit * {
|
||||
for (const auto &unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location() == coords) {
|
||||
@@ -167,7 +170,7 @@ static inline auto Occupant(const std::vector<const Unit *> &units, const Coords
|
||||
}
|
||||
|
||||
template<class UnitContainer>
|
||||
auto Occupant(const UnitContainer &units, const Coords &coords)
|
||||
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords)
|
||||
-> std::optional<typename UnitContainer::value_type> {
|
||||
for (const auto &unit : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
@@ -177,7 +180,8 @@ auto Occupant(const UnitContainer &units, const Coords &coords)
|
||||
}
|
||||
|
||||
template<class U>
|
||||
auto Occupant(const std::map<UnitId, U> &units, const Coords &coords) -> std::optional<U> {
|
||||
[[nodiscard]] auto Occupant(const std::map<UnitId, U> &units, const Coords &coords)
|
||||
-> std::optional<U> {
|
||||
for (const auto &[unitId, unit] : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
}
|
||||
@@ -186,7 +190,7 @@ auto Occupant(const std::map<UnitId, U> &units, const Coords &coords) -> std::op
|
||||
}
|
||||
|
||||
template<class U>
|
||||
auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
|
||||
[[nodiscard]] auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
|
||||
-> std::optional<U> {
|
||||
for (const auto &[unitId, unit] : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
@@ -196,7 +200,7 @@ auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coo
|
||||
}
|
||||
|
||||
template<class U, class UnitContainer>
|
||||
auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
|
||||
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
|
||||
for (const auto &unit : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
}
|
||||
@@ -204,7 +208,7 @@ auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static inline auto FriendlyOccupant(
|
||||
[[nodiscard]] static inline auto FriendlyOccupant(
|
||||
const PlayerId pid,
|
||||
const Units *units,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
@@ -221,7 +225,7 @@ static inline auto FriendlyOccupant(
|
||||
}
|
||||
|
||||
template<class U, class UnitContainer>
|
||||
auto FriendlyOccupant(
|
||||
[[nodiscard]] auto FriendlyOccupant(
|
||||
PlayerId pid,
|
||||
const UnitContainer &units,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
@@ -237,14 +241,14 @@ auto FriendlyOccupant(
|
||||
return {};
|
||||
}
|
||||
|
||||
auto KnownEnemyOccupant(
|
||||
[[nodiscard]] auto KnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const Units *units,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> const Unit *;
|
||||
|
||||
template<class UnitContainer>
|
||||
auto KnownEnemyOccupant(
|
||||
[[nodiscard]] auto KnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const UnitContainer &units,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
@@ -277,31 +281,35 @@ auto KnownEnemyOccupant(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto KnownAdjacentEnemies(
|
||||
[[nodiscard]] auto KnownAdjacentEnemies(
|
||||
PlayerId playerId,
|
||||
const Units *units,
|
||||
const HexMap *map,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> std::vector<const Unit *>;
|
||||
|
||||
auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
|
||||
[[nodiscard]] auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
|
||||
-> HexMapDirectionsTo;
|
||||
|
||||
static inline auto DirectionsTo(const HexMap *map, const Coords &origin, const Coords &destination)
|
||||
-> HexMapDirectionsTo {
|
||||
[[nodiscard]] static inline auto DirectionsTo(
|
||||
const HexMap *map,
|
||||
const Coords &origin,
|
||||
const Coords &destination) -> HexMapDirectionsTo {
|
||||
return DirectionsTo(map->column_count(), origin, destination);
|
||||
}
|
||||
|
||||
auto MaybeTileInDirection(const HexMap *map, const Coords &inCoords, HexMapDirection orientation)
|
||||
-> std::optional<Coords>;
|
||||
[[nodiscard]] auto MaybeTileInDirection(
|
||||
const HexMap *map,
|
||||
const Coords &inCoords,
|
||||
HexMapDirection orientation) -> std::optional<Coords>;
|
||||
|
||||
auto MaybeOccupantInDirection(
|
||||
[[nodiscard]] auto MaybeOccupantInDirection(
|
||||
const HexMap *map,
|
||||
const Units *units,
|
||||
const Coords &coords,
|
||||
HexMapDirection direction) -> const Unit *;
|
||||
|
||||
auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;
|
||||
[[nodiscard]] auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;
|
||||
|
||||
void MutatingSetTileModifier(
|
||||
HexMapT *hexMap,
|
||||
@@ -318,9 +326,10 @@ void MutatingSetTileModifier(
|
||||
const CoordsProto &coords,
|
||||
const TileModifierProto &TileModifierProto);
|
||||
|
||||
auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;
|
||||
[[nodiscard]] auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;
|
||||
|
||||
auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units) -> bool;
|
||||
[[nodiscard]] auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units)
|
||||
-> bool;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ namespace shardok {
|
||||
using PlayerInfoFb = net::eagle0::shardok::storage::fb::PlayerInfo;
|
||||
using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
|
||||
auto PlayerIsDefender(const GameState *gameState, int playerId) -> bool;
|
||||
auto NextPlayerId(const GameState *gameState, PlayerId pid, bool allowUncontrolled = true)
|
||||
-> PlayerId;
|
||||
auto AlliedPids(const GameState *gameState, int playerId) -> std::vector<PlayerId>;
|
||||
[[nodiscard]] auto PlayerIsDefender(const GameState *gameState, int playerId) -> bool;
|
||||
[[nodiscard]] auto
|
||||
NextPlayerId(const GameState *gameState, PlayerId pid, bool allowUncontrolled = true) -> PlayerId;
|
||||
[[nodiscard]] auto AlliedPids(const GameState *gameState, int playerId) -> std::vector<PlayerId>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <ranges>
|
||||
|
||||
#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/view_filters/GameStateFilter.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateViewDiffer.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/action_type.pb.h"
|
||||
@@ -87,7 +88,7 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
|
||||
if (result.has_actor()) {
|
||||
const auto actorId = result.actor().value();
|
||||
for (const auto &unitData : result.changed_units_fb()) {
|
||||
const auto &unit = *reinterpret_cast<const Unit *>(unitData.data());
|
||||
const auto unit = UnitFromBytes(unitData);
|
||||
if (unit.unit_id() == actorId) {
|
||||
opponentKnowledgeOfActor = OpponentKnowledge(unit, askingPlayer);
|
||||
hiddenActor = unit.hidden();
|
||||
|
||||
@@ -13,6 +13,7 @@ cc_library(
|
||||
deps = [
|
||||
":unit_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_view_differ",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_cc_proto",
|
||||
|
||||
@@ -45,6 +45,7 @@ auto GameStateFilteredForPlayerWithAllies(
|
||||
|
||||
int reserveUnitCount = 0;
|
||||
int normalUnitCount = 0;
|
||||
int capturedUnitCount = 0;
|
||||
for (const auto& unit : *gameState->units()) {
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
@@ -56,8 +57,11 @@ auto GameStateFilteredForPlayerWithAllies(
|
||||
++normalUnitCount;
|
||||
break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT:
|
||||
++capturedUnitCount;
|
||||
break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
|
||||
@@ -73,6 +77,7 @@ auto GameStateFilteredForPlayerWithAllies(
|
||||
|
||||
gsv.mutable_reserve_units()->Reserve(reserveUnitCount);
|
||||
gsv.mutable_units()->Reserve(normalUnitCount);
|
||||
gsv.mutable_captured_units()->Reserve(capturedUnitCount);
|
||||
for (const auto& unit : *gameState->units()) {
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
@@ -85,8 +90,12 @@ auto GameStateFilteredForPlayerWithAllies(
|
||||
*gsv.add_units() = UnitFilteredForPlayer(settings, unit, askingPlayer, alliedPids);
|
||||
break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT:
|
||||
*gsv.add_captured_units() =
|
||||
UnitFilteredForPlayer(settings, unit, askingPlayer, alliedPids);
|
||||
break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
|
||||
|
||||
@@ -216,7 +216,8 @@ auto GameStateGuesser::GuessedState(
|
||||
|
||||
std::vector<UnitId> foundUnits{};
|
||||
std::vector<Unit> unitsVec{};
|
||||
const int knownUnitCount = gameStateView.units_size() + gameStateView.reserve_units_size();
|
||||
const int knownUnitCount = gameStateView.units_size() + gameStateView.reserve_units_size() +
|
||||
gameStateView.captured_units_size();
|
||||
foundUnits.reserve(knownUnitCount);
|
||||
unitsVec.reserve(knownUnitCount + 6);
|
||||
for (const auto &uv : gameStateView.units()) {
|
||||
@@ -257,6 +258,19 @@ auto GameStateGuesser::GuessedState(
|
||||
foundUnits.push_back(uv.unit_id());
|
||||
}
|
||||
|
||||
for (const auto &uv : gameStateView.captured_units()) {
|
||||
unitsVec.push_back(GuessedUnit(
|
||||
settings,
|
||||
uv,
|
||||
pid,
|
||||
FromCoordsProto(uv.location()),
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT,
|
||||
playerAverages));
|
||||
if (uv.player_id() == 1) foundDefender = true;
|
||||
if (uv.unit_id() > maxId) maxId = uv.unit_id();
|
||||
foundUnits.push_back(uv.unit_id());
|
||||
}
|
||||
|
||||
for (int i = 0; i < maxId; ++i) {
|
||||
auto entry = std::ranges::find(foundUnits, i);
|
||||
if (entry == std::end(foundUnits)) {
|
||||
@@ -283,6 +297,8 @@ auto GameStateGuesser::GuessedState(
|
||||
|
||||
newUnit.mutate_unit_id(maxId + 1);
|
||||
newUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
newUnit.mutate_has_attached_hero(false);
|
||||
newUnit.mutate_commanding_unit_id(-1);
|
||||
|
||||
for (const auto &startPosProto :
|
||||
gameStateView.hex_map().defender_starting_positions().positions()) {
|
||||
|
||||
@@ -62,6 +62,12 @@ auto MakeGameStateViewDiff(const GameStateView &before, const GameStateView &aft
|
||||
diff.mutable_changed_reserve_units(),
|
||||
diff.mutable_removed_reserve_unit_ids(),
|
||||
changed);
|
||||
PopulateRepeatedUnitDiffs(
|
||||
before.captured_units(),
|
||||
after.captured_units(),
|
||||
diff.mutable_changed_captured_units(),
|
||||
diff.mutable_removed_captured_unit_ids(),
|
||||
changed);
|
||||
|
||||
// tile modifiers
|
||||
const int afterRowCount = after.hex_map().row_count();
|
||||
@@ -70,7 +76,8 @@ auto MakeGameStateViewDiff(const GameStateView &before, const GameStateView &aft
|
||||
|
||||
for (int tileIndex = 0; tileIndex < afterCount; ++tileIndex) {
|
||||
Coords coords =
|
||||
Coords(int8_t(tileIndex / afterColumnCount), int8_t(tileIndex % afterColumnCount));
|
||||
Coords(static_cast<int8_t>(tileIndex / afterColumnCount),
|
||||
static_cast<int8_t>(tileIndex % afterColumnCount));
|
||||
const auto afterModifier = after.hex_map().terrain(tileIndex).modifier();
|
||||
|
||||
if (tileIndex >= before.hex_map().terrain_size()) {
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace shardok {
|
||||
using net::eagle0::shardok::api::GameStateView;
|
||||
using net::eagle0::shardok::api::GameStateViewDiff;
|
||||
|
||||
auto MakeGameStateViewDiff(const GameStateView& before, const GameStateView& after)
|
||||
[[nodiscard]] auto MakeGameStateViewDiff(const GameStateView& before, const GameStateView& after)
|
||||
-> std::optional<GameStateViewDiff>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -57,6 +57,7 @@ cc_library(
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_exception",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/common:common_unit_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokException.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/victory_condition.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
@@ -71,13 +72,13 @@ auto PopulateGameOverResponse(
|
||||
|
||||
// copy in all the resolvedUnits that correspond to this player
|
||||
for (const auto &internalRu : resolvedUnits) {
|
||||
const auto *unit = reinterpret_cast<const Unit *>(internalRu.unit_bytes().data());
|
||||
if (!unit->has_attached_hero()) continue;
|
||||
const auto unit = UnitFromBytes(internalRu.unit_bytes());
|
||||
if (!unit.has_attached_hero()) continue;
|
||||
if (!IncludeUnitProtoInReturn(internalRu.status())) continue;
|
||||
if (unit->player_id() != shardokPid) continue;
|
||||
if (unit.player_id() != shardokPid) continue;
|
||||
|
||||
auto *ru = resolutionInfo->add_units();
|
||||
*ru->mutable_unit() = FromUnitFb(*unit);
|
||||
*ru->mutable_unit() = FromUnitFb(unit);
|
||||
ru->set_status(FromInternalStatus(internalRu.status()));
|
||||
if (internalRu.status() ==
|
||||
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_SECURED_UNIT) {
|
||||
|
||||
@@ -22,12 +22,12 @@ auto PopulateGameOverResponse(
|
||||
const std::vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
|
||||
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool;
|
||||
|
||||
auto IncludeUnitProtoInReturn(
|
||||
[[nodiscard]] auto IncludeUnitProtoInReturn(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool;
|
||||
|
||||
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
|
||||
[[nodiscard]] auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
|
||||
|
||||
auto FromInternalStatus(
|
||||
[[nodiscard]] auto FromInternalStatus(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
|
||||
-> net::eagle0::common::UnitStatus;
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ private:
|
||||
* Read auth token from a file, stripping whitespace.
|
||||
* Returns empty string if file doesn't exist or is empty.
|
||||
*/
|
||||
inline auto ReadAuthTokenFromFile(const std::string& filePath) -> std::string {
|
||||
[[nodiscard]] inline auto ReadAuthTokenFromFile(const std::string& filePath) -> std::string {
|
||||
if (filePath.empty()) { return ""; }
|
||||
|
||||
std::ifstream file(filePath);
|
||||
|
||||
@@ -26,10 +26,10 @@ public:
|
||||
[[nodiscard]] auto what() const noexcept -> const char* override { return description.c_str(); }
|
||||
};
|
||||
|
||||
auto LoadMap(const std::string& mapName) -> HexMapProto;
|
||||
[[nodiscard]] auto LoadMap(const std::string& mapName) -> HexMapProto;
|
||||
|
||||
auto LoadMapFromBytes(const std::string& mapBytes) -> HexMapProto;
|
||||
[[nodiscard]] auto LoadMapFromBytes(const std::string& mapBytes) -> HexMapProto;
|
||||
|
||||
auto GetMapNames() -> std::vector<std::string>;
|
||||
[[nodiscard]] auto GetMapNames() -> std::vector<std::string>;
|
||||
|
||||
#endif // EAGLE0_SHARDOK_UTIL_MAP_LOADER_HPP
|
||||
|
||||
@@ -22,11 +22,8 @@ namespace eagle {
|
||||
|
||||
public void SetGoToBattleAction(Action<string> action) { _goToBattleAction = action; }
|
||||
|
||||
public void UpdateBattles(
|
||||
IList<ShardokBattleView> battles,
|
||||
IGameModel model,
|
||||
int battleDay,
|
||||
string fightableGameId) {
|
||||
public void
|
||||
UpdateBattles(IList<ShardokBattleView> battles, IGameModel model, int battleDay) {
|
||||
dayLabel.text = $"Day {battleDay}";
|
||||
|
||||
// Clear stale children left over from scene/prefab (e.g. after reload)
|
||||
@@ -49,7 +46,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
for (int i = 0; i < battles.Count; i++) {
|
||||
_rows[i].Populate(battles[i], model, _goToBattleAction, fightableGameId);
|
||||
_rows[i].Populate(battles[i], model, _goToBattleAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-18
@@ -22,11 +22,8 @@ namespace eagle {
|
||||
|
||||
private bool _registeredAsFightButton;
|
||||
|
||||
public void Populate(
|
||||
ShardokBattleView battle,
|
||||
IGameModel model,
|
||||
Action<string> goToBattleAction,
|
||||
string fightableGameId) {
|
||||
public void
|
||||
Populate(ShardokBattleView battle, IGameModel model, Action<string> goToBattleAction) {
|
||||
// Province name
|
||||
if (model.Provinces.TryGetValue(battle.DefenderProvince, out var pv) &&
|
||||
model.PlayerId.HasValue) {
|
||||
@@ -64,7 +61,7 @@ namespace eagle {
|
||||
outcomeLabel.color =
|
||||
isAlliedWin ? new Color(0.2f, 0.8f, 0.2f) : new Color(0.8f, 0.2f, 0.2f);
|
||||
|
||||
actionButton.gameObject.SetActive(false);
|
||||
SetActionButtonVisible(false);
|
||||
} else {
|
||||
thermometer.gameObject.SetActive(true);
|
||||
outcomeLabel.gameObject.SetActive(false);
|
||||
@@ -100,32 +97,36 @@ namespace eagle {
|
||||
|
||||
actionButton.onClick.RemoveAllListeners();
|
||||
var gameId = battle.ShardokGameId;
|
||||
bool modelReady = model.ShardokGameModels.ContainsKey(gameId);
|
||||
|
||||
if (isSelf) {
|
||||
bool isFightable = gameId == fightableGameId;
|
||||
actionButton.gameObject.SetActive(true);
|
||||
actionButton.interactable = isFightable;
|
||||
actionButtonLabel.text = isFightable ? "Fight!" : "Waiting...";
|
||||
if (isFightable) {
|
||||
bool canFight = battle.CanFight && modelReady;
|
||||
SetActionButtonVisible(true);
|
||||
actionButton.interactable = canFight;
|
||||
actionButtonLabel.text =
|
||||
modelReady ? battle.CanFight ? "Fight!" : "Waiting..." : "Loading...";
|
||||
if (canFight) {
|
||||
actionButton.onClick.AddListener(() => goToBattleAction?.Invoke(gameId));
|
||||
}
|
||||
SetFightButtonTutorialRegistration(isFightable);
|
||||
SetFightButtonTutorialRegistration(canFight);
|
||||
} else if (isAllied) {
|
||||
// ShardokBattles is populated when a NewBattle eagle update arrives, but the
|
||||
// matching ShardokGameModel isn't created until the first action-result
|
||||
// response for that battle reaches us. During that gap, GoToBattle would
|
||||
// throw on Model.ShardokGameModels[gameId]; gate the button on the model
|
||||
// existing so the click can never land in the gap.
|
||||
bool modelReady = model.ShardokGameModels.ContainsKey(gameId);
|
||||
actionButton.gameObject.SetActive(true);
|
||||
actionButton.interactable = modelReady;
|
||||
actionButtonLabel.text = modelReady ? "Observe" : "Loading...";
|
||||
if (modelReady) {
|
||||
bool canObserve = battle.CanObserve && modelReady;
|
||||
SetActionButtonVisible(true);
|
||||
actionButton.interactable = canObserve;
|
||||
actionButtonLabel.text = modelReady
|
||||
? battle.CanObserve ? "Observe" : "Waiting..."
|
||||
: "Loading...";
|
||||
if (canObserve) {
|
||||
actionButton.onClick.AddListener(() => goToBattleAction?.Invoke(gameId));
|
||||
}
|
||||
SetFightButtonTutorialRegistration(false);
|
||||
} else {
|
||||
actionButton.gameObject.SetActive(false);
|
||||
SetActionButtonVisible(false);
|
||||
SetFightButtonTutorialRegistration(false);
|
||||
}
|
||||
}
|
||||
@@ -133,6 +134,17 @@ namespace eagle {
|
||||
if (hasWinner) { SetFightButtonTutorialRegistration(false); }
|
||||
}
|
||||
|
||||
private void SetActionButtonVisible(bool visible) {
|
||||
actionButton.gameObject.SetActive(true);
|
||||
actionButton.interactable = visible;
|
||||
actionButton.targetGraphic.enabled = visible;
|
||||
actionButtonLabel.enabled = visible;
|
||||
if (!visible) {
|
||||
actionButton.onClick.RemoveAllListeners();
|
||||
actionButtonLabel.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
private void SetFightButtonTutorialRegistration(bool shouldRegister) {
|
||||
var registry = TutorialManager.Instance?.TargetRegistry;
|
||||
if (registry == null) return;
|
||||
|
||||
+99
-15
@@ -37,7 +37,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: -2, y: 0}
|
||||
m_SizeDelta: {x: -14, y: 0}
|
||||
m_Pivot: {x: 0, y: 1}
|
||||
--- !u!222 &8488118418423736121
|
||||
CanvasRenderer:
|
||||
@@ -1859,12 +1859,12 @@ RectTransform:
|
||||
- {fileID: 5557391494505736092}
|
||||
- {fileID: 6313361240299796939}
|
||||
- {fileID: 5570932315451852925}
|
||||
m_Father: {fileID: 2429126807788525227}
|
||||
m_Father: {fileID: 910000000000000004}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 217.5914, y: -8}
|
||||
m_SizeDelta: {x: 173.1829, y: 202}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &5247279438940124088
|
||||
MonoBehaviour:
|
||||
@@ -2268,14 +2268,13 @@ RectTransform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 8329760851148613071}
|
||||
- {fileID: 760782539024697097}
|
||||
- {fileID: 910000000000000004}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -20}
|
||||
m_SizeDelta: {x: 0, y: -40}
|
||||
m_AnchoredPosition: {x: 0, y: -26}
|
||||
m_SizeDelta: {x: 0, y: -52}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8056181741204377687
|
||||
CanvasRenderer:
|
||||
@@ -2334,6 +2333,70 @@ MonoBehaviour:
|
||||
maxLabel: {fileID: 2383775986439252}
|
||||
maxEvenLabel: {fileID: 2632144799086272744}
|
||||
maxEvenButton: {fileID: 8627715712575757530}
|
||||
--- !u!1 &910000000000000003
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 910000000000000004}
|
||||
- component: {fileID: 910000000000000005}
|
||||
m_Layer: 0
|
||||
m_Name: Content
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &910000000000000004
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 910000000000000003}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 8329760851148613071}
|
||||
- {fileID: 760782539024697097}
|
||||
m_Father: {fileID: 2429126807788525227}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: -10, y: -10}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &910000000000000005
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 910000000000000003}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
|
||||
m_Padding:
|
||||
m_Left: 0
|
||||
m_Right: 0
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 4
|
||||
m_Spacing: 10
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 1
|
||||
m_ChildControlHeight: 1
|
||||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!1 &6366334277554995035
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2344,6 +2407,7 @@ GameObject:
|
||||
m_Component:
|
||||
- component: {fileID: 8329760851148613071}
|
||||
- component: {fileID: 531306430670215360}
|
||||
- component: {fileID: 910000000000000002}
|
||||
m_Layer: 0
|
||||
m_Name: Arm Troops Table
|
||||
m_TagString: Untagged
|
||||
@@ -2365,13 +2429,33 @@ RectTransform:
|
||||
m_Children:
|
||||
- {fileID: 1213384902002452052}
|
||||
- {fileID: 2133514339961208833}
|
||||
m_Father: {fileID: 2429126807788525227}
|
||||
m_Father: {fileID: 910000000000000004}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0}
|
||||
m_AnchorMax: {x: 0.5, y: 1}
|
||||
m_AnchoredPosition: {x: -79, y: 0}
|
||||
m_SizeDelta: {x: 395, y: -20}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &910000000000000002
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6366334277554995035}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 600
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 800
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: 1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &531306430670215360
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
+28
-28
@@ -1198,8 +1198,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 5, y: -17.5}
|
||||
m_SizeDelta: {x: 24, y: 31}
|
||||
m_AnchoredPosition: {x: 5, y: -16}
|
||||
m_SizeDelta: {x: 32, y: 32}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &1363051944519128355
|
||||
CanvasRenderer:
|
||||
@@ -1249,12 +1249,12 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 24
|
||||
m_MinHeight: 24
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_MinWidth: 32
|
||||
m_MinHeight: 32
|
||||
m_PreferredWidth: 32
|
||||
m_PreferredHeight: 32
|
||||
m_FlexibleWidth: 0
|
||||
m_FlexibleHeight: 1
|
||||
m_FlexibleHeight: 0
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &1708500215346604881
|
||||
GameObject:
|
||||
@@ -2278,8 +2278,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 5, y: -17.5}
|
||||
m_SizeDelta: {x: 24, y: 31}
|
||||
m_AnchoredPosition: {x: 5, y: -16}
|
||||
m_SizeDelta: {x: 32, y: 32}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &1265686771602852415
|
||||
CanvasRenderer:
|
||||
@@ -2329,12 +2329,12 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 24
|
||||
m_MinHeight: 24
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_MinWidth: 32
|
||||
m_MinHeight: 32
|
||||
m_PreferredWidth: 32
|
||||
m_PreferredHeight: 32
|
||||
m_FlexibleWidth: 0
|
||||
m_FlexibleHeight: 1
|
||||
m_FlexibleHeight: 0
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &3265081575184054094
|
||||
GameObject:
|
||||
@@ -4955,8 +4955,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 5, y: -17.5}
|
||||
m_SizeDelta: {x: 24, y: 31}
|
||||
m_AnchoredPosition: {x: 5, y: -16}
|
||||
m_SizeDelta: {x: 32, y: 32}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &3184427525695078776
|
||||
CanvasRenderer:
|
||||
@@ -5006,12 +5006,12 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 24
|
||||
m_MinHeight: 24
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_MinWidth: 32
|
||||
m_MinHeight: 32
|
||||
m_PreferredWidth: 32
|
||||
m_PreferredHeight: 32
|
||||
m_FlexibleWidth: 0
|
||||
m_FlexibleHeight: 1
|
||||
m_FlexibleHeight: 0
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &7946146202987969918
|
||||
GameObject:
|
||||
@@ -5048,8 +5048,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 5, y: -17.5}
|
||||
m_SizeDelta: {x: 24, y: 31}
|
||||
m_AnchoredPosition: {x: 5, y: -16}
|
||||
m_SizeDelta: {x: 32, y: 32}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &7764612421372900914
|
||||
CanvasRenderer:
|
||||
@@ -5099,12 +5099,12 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 24
|
||||
m_MinHeight: 24
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_MinWidth: 32
|
||||
m_MinHeight: 32
|
||||
m_PreferredWidth: 32
|
||||
m_PreferredHeight: 32
|
||||
m_FlexibleWidth: 0
|
||||
m_FlexibleHeight: 1
|
||||
m_FlexibleHeight: 0
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &8017289266862744017
|
||||
GameObject:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user