mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 08:55:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8d7e03afe |
@@ -6,4 +6,3 @@
|
||||
*.bytes filter=lfs diff=lfs merge=lfs -text
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
*.herodata filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -3,23 +3,13 @@ name: Bazel Test
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
paths-ignore:
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/csharp/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
paths-ignore:
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/csharp/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -4,21 +4,19 @@ on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/client_presigner.yml"
|
||||
- "src/main/go/net/eagle0/client_download/**"
|
||||
- "src/main/go/net/eagle0/util/**"
|
||||
- "src/main/go/**"
|
||||
- "!src/main/go/net/eagle0/web_functions/name-generator/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/client_presigner.yml"
|
||||
- "src/main/go/net/eagle0/client_download/**"
|
||||
- "src/main/go/net/eagle0/util/**"
|
||||
- "src/main/go/**"
|
||||
- "!src/main/go/net/eagle0/web_functions/name-generator/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
client-presigner:
|
||||
runs-on: self-hosted
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -26,7 +24,7 @@ jobs:
|
||||
lfs: false
|
||||
clean: false
|
||||
- name: Build Client Presigner
|
||||
run: bazel build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 //src/main/go/net/eagle0/client_download
|
||||
run: bazel build //src/main/go/net/eagle0/client_download
|
||||
- name: Archive presigner binary
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
name: Installer Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/installer_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/win/installer/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/installer_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/win/installer/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-installer:
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Setup .NET 8
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj
|
||||
|
||||
- name: Build installer
|
||||
run: dotnet publish src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj -c Release -r win-x64 --self-contained true --output ./installer-output
|
||||
|
||||
- name: Archive installer binary
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: eagle-installer
|
||||
path: ./installer-output/EagleInstaller.exe
|
||||
|
||||
- name: Verify installer exists
|
||||
if: success()
|
||||
run: |
|
||||
if [ ! -f "./installer-output/EagleInstaller.exe" ]; then
|
||||
echo "ERROR: EagleInstaller.exe not found at expected location"
|
||||
echo "Directory contents:"
|
||||
ls -la ./installer-output/
|
||||
exit 1
|
||||
fi
|
||||
echo "Installer found at correct location"
|
||||
|
||||
- name: Deploy installer
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
INSTALLER_PATH="$(pwd)/installer-output/EagleInstaller.exe"
|
||||
echo "Using absolute path: $INSTALLER_PATH"
|
||||
bazel run //src/main/go/net/eagle0/build/installer_build_handler:installer_build_handler -- "$INSTALLER_PATH"
|
||||
|
||||
- name: Update unified manifest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
# Create installer manifest content
|
||||
INSTALLER_SHA=$(sha256sum ./installer-output/EagleInstaller.exe | cut -d' ' -f1)
|
||||
echo "installer_version=$INSTALLER_SHA" > /tmp/installer_manifest.txt
|
||||
echo "installer_url=installer/EagleInstaller.exe" >> /tmp/installer_manifest.txt
|
||||
|
||||
echo "=== Installer manifest content ==="
|
||||
cat /tmp/installer_manifest.txt
|
||||
echo "=================================="
|
||||
|
||||
# Update the unified manifest
|
||||
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer /tmp/installer_manifest.txt
|
||||
@@ -3,15 +3,21 @@ name: Mac History Editor Build
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/mac_history_build.yml"
|
||||
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/**"
|
||||
paths-ignore:
|
||||
- "src/main/cpp/**"
|
||||
- "src/main/scala/**"
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/cpp/**"
|
||||
- "src/test/scala/**"
|
||||
- "src/test/csharp/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/mac_history_build.yml"
|
||||
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/**"
|
||||
paths-ignore:
|
||||
- "src/main/cpp/**"
|
||||
- "src/main/scala/**"
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/cpp/**"
|
||||
- "src/test/scala/**"
|
||||
- "src/test/csharp/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -3,25 +3,13 @@ name: Shardok Build
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/proto/net/eagle0/shardok/**'
|
||||
- 'src/main/proto/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
paths-ignore:
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/csharp/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/proto/net/eagle0/shardok/**'
|
||||
- 'src/main/proto/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
paths-ignore:
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/csharp/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -3,33 +3,17 @@ name: Unity Build
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/unity_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/**"
|
||||
- "src/main/proto/**"
|
||||
- "scripts/build_protos.sh"
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/restore_library.sh"
|
||||
- "ci/github_actions/persist_library.sh"
|
||||
- "MODULE.bazel"
|
||||
- "WORKSPACE"
|
||||
- "src/main/proto/net/eagle0/eagle/**/BUILD.bazel"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/unity_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/**"
|
||||
- "src/main/proto/**"
|
||||
- "scripts/build_protos.sh"
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/restore_library.sh"
|
||||
- "ci/github_actions/persist_library.sh"
|
||||
- "MODULE.bazel"
|
||||
- "WORKSPACE"
|
||||
- "src/main/proto/**/BUILD.bazel"
|
||||
paths-ignore:
|
||||
- "src/main/cpp/**"
|
||||
- "src/main/scala/**"
|
||||
- "src/test/cpp/**"
|
||||
- "src/test/scala/**"
|
||||
# pull_request:
|
||||
# paths-ignore:
|
||||
# - "src/main/cpp/**"
|
||||
# - "src/main/scala/**"
|
||||
# - "src/test/cpp/**"
|
||||
# - "src/test/scala/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -52,18 +36,10 @@ jobs:
|
||||
- name: Persist Library/
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
- name: Deploy Windows unity
|
||||
if: success() #&& github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- "/tmp/eagle0/eagle0WIN" "/tmp/unity_manifest.txt"
|
||||
|
||||
- name: Update unified manifest
|
||||
if: success() #&& github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d /tmp/unity_manifest.txt
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- "/tmp/eagle0/eagle0WIN"
|
||||
- name: Archive build log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Eagle0 is a multi-language gaming system combining strategic turn-based gameplay (Eagle) with tactical hex-based combat (Shardok). The system integrates LLM-based narrative generation and supports both human and AI players.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Three-Tier Game System:**
|
||||
- **Unity Client (C#)**: Real-time strategy game client with integrated tactical combat UI
|
||||
- **Eagle (Scala)**: Strategic layer managing turn-based gameplay, diplomacy, hero progression, and province control
|
||||
- **Shardok (C++)**: Tactical layer handling real-time hex-based combat simulation with performance-critical battle resolution
|
||||
|
||||
**Communication Flow:**
|
||||
```
|
||||
Unity Client ↔ Eagle (gRPC streaming) ↔ Shardok (internal gRPC)
|
||||
```
|
||||
|
||||
**Key Entry Points:**
|
||||
- `/src/main/csharp/net/eagle0/clients/unity/eagle0/` - Unity C# game client
|
||||
- `/src/main/scala/net/eagle0/eagle/Main.scala` - Eagle strategic game server
|
||||
- `/src/main/cpp/net/eagle0/shardok/shardok_server_main.cpp` - Shardok tactical server
|
||||
|
||||
**Protocol Buffer Architecture:**
|
||||
- Extensive use of protobuf for type-safe communication
|
||||
- Separate packages: `api/` (client-facing), `internal/` (server state), `views/` (client projections)
|
||||
- Event sourcing pattern with immutable action history
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Building
|
||||
```bash
|
||||
# Build Eagle server (Scala strategic layer)
|
||||
bazel build //src/main/scala/net/eagle0/eagle:eagle_server_deploy.jar
|
||||
|
||||
# Build Shardok server (C++ tactical layer)
|
||||
bazel build -c opt //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
# Build Unity/C# client
|
||||
./scripts/build_protos.sh # Protocol buffer generation for Unity
|
||||
./scripts/build_plugins.sh # Native plugins for all platforms
|
||||
./scripts/build_windows_plugin.sh # Windows-specific plugin build
|
||||
# Unity builds via CI: ci/github_actions/build_unity.sh
|
||||
```
|
||||
|
||||
### Running Services
|
||||
```bash
|
||||
# Eagle server (port 40032)
|
||||
bazel run //src/main/scala/net/eagle0/eagle:eagle_server -- --eagle-grpc-port 40032
|
||||
# Or: ./scripts/eagle_run.sh
|
||||
|
||||
# Shardok server
|
||||
bazel run //src/main/cpp/net/eagle0/shardok:shardok-server --compilation_mode=opt
|
||||
# Or: ./scripts/shardok_run.sh
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
bazel test //src/test/... //src/main/go/...
|
||||
|
||||
# Component-specific tests
|
||||
bazel test //src/test/scala/... # Scala Eagle tests
|
||||
bazel test //src/test/cpp/... # C++ Shardok tests
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
```bash
|
||||
bazel run gazelle # Update Go build files
|
||||
./scripts/updateActionResultTypes.sh # Update protocol buffer mappings
|
||||
```
|
||||
|
||||
### Code Formatting
|
||||
```bash
|
||||
# ALWAYS run clang-format after making any C++ or C# code changes
|
||||
clang-format -i <modified_files>
|
||||
|
||||
# Format all C++ files in a directory:
|
||||
find . -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i
|
||||
|
||||
# Format all C# files in a directory:
|
||||
find . -name "*.cs" | xargs clang-format -i
|
||||
```
|
||||
|
||||
## Language-Specific Patterns
|
||||
|
||||
**Scala (Strategic Layer):**
|
||||
- Use `EngineImpl.scala` for core game logic modifications
|
||||
- Follow event sourcing pattern - all changes through immutable actions
|
||||
- gRPC streaming for real-time client updates via `EagleServiceImpl.scala`
|
||||
- LLM integration in `/common/llm_integration/` for narrative generation
|
||||
|
||||
**C++ (Tactical Layer):**
|
||||
- Performance-critical combat in `ShardokEngine.hpp/.cpp`
|
||||
- FlatBuffers for efficient serialization in `/flatbuffer/` directory
|
||||
- AI systems in `/ai/` subdirectory with pluggable strategy selectors
|
||||
- Extensive unit testing with Google Test framework
|
||||
|
||||
**Protocol Buffers:**
|
||||
- Three-layer structure: `api/` (client), `internal/` (server), `views/` (projections)
|
||||
- Use `shardok_internal_interface.proto` for Eagle-Shardok communication
|
||||
- Maintain backward compatibility when modifying existing messages
|
||||
|
||||
**C# (Unity Client):**
|
||||
- Located in `/src/main/csharp/net/eagle0/clients/unity/eagle0/`
|
||||
- Uses Unity 6 (6000.0.32f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Key components: `EagleConnection.cs` (gRPC client), `EagleGameController.cs` (main game logic)
|
||||
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
|
||||
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
|
||||
- Seamless transition between strategic gameplay and hex-based tactical combat
|
||||
|
||||
**Go (Build Tools):**
|
||||
- Build automation and code generation utilities
|
||||
- AWS S3 integration for deployment artifacts
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Comprehensive unit tests for both Scala and C++ components
|
||||
- Integration tests for Eagle-Shardok communication
|
||||
- Map validation tests ensure game content integrity
|
||||
- Use `GameSettings_test_utils.cpp` and `ShardokEngineBasedTestData.cpp` for C++ test helpers
|
||||
|
||||
## Performance Testing
|
||||
|
||||
When making performance-related changes to the AI or engine:
|
||||
|
||||
```bash
|
||||
# 1. Commit your changes to a feature branch
|
||||
git checkout -b performance-improvement-feature
|
||||
git add . && git commit -m "Implement performance improvement"
|
||||
|
||||
# 2. Run performance tests multiple times on your branch to reduce noise
|
||||
for i in 1 2 3; do
|
||||
echo "=== Run $i ==="
|
||||
./scripts/ai_perf_test.sh 2>&1 | grep -A 20 "AI Search Performance Summary"
|
||||
done
|
||||
# Save or note the results
|
||||
|
||||
# 3. Switch to main branch and run the same tests
|
||||
git checkout main
|
||||
for i in 1 2 3; do
|
||||
echo "=== Run $i ==="
|
||||
./scripts/ai_perf_test.sh 2>&1 | grep -A 20 "AI Search Performance Summary"
|
||||
done
|
||||
|
||||
# 4. Compare the results between your branch and main
|
||||
# Key metrics to compare:
|
||||
# - Commands evaluated at each depth (e.g., "Depth 3: 169/523 commands")
|
||||
# - Average search depth achieved
|
||||
# - Completion rates at each depth
|
||||
```
|
||||
|
||||
**Important notes:**
|
||||
- Run tests multiple times (3-5) to account for performance variance
|
||||
- Focus on commands evaluated at each depth rather than total commands
|
||||
- Commands at different depths aren't directly comparable (depth 3 is more valuable than depth 2)
|
||||
- **Always test performance changes** - what seems like an optimization may sometimes have unexpected overhead or behavior changes.
|
||||
|
||||
## Game Content
|
||||
|
||||
**Maps:** `.e0mj` files in `/src/main/resources/net/eagle0/shardok/maps/`
|
||||
**Configuration:** Game parameters in `/src/main/resources/net/eagle0/eagle/game_parameters.json`
|
||||
**Data Files:** TSV format for battalions, heroes, and other game data
|
||||
|
||||
## Deployment
|
||||
|
||||
- Bazel handles multi-language builds and dependencies
|
||||
- CI/CD via GitHub Actions with platform-specific build scripts in `/ci/github_actions/`
|
||||
- Docker containerization available via `ci/eagle_run.Dockerfile`
|
||||
+8
-8
@@ -141,19 +141,19 @@ http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "ht
|
||||
bazel_dep(name = "flatbuffers", version = "25.2.10")
|
||||
|
||||
#
|
||||
# gtl (for parallel_hashmap)
|
||||
# parallel-hashmap
|
||||
#
|
||||
|
||||
gtl_version = "1.2.0"
|
||||
parallel_hashmap_version = "1.4.1"
|
||||
|
||||
gtl_sha = "1969c45dd76eac0dd87e9e2b65cffe358617f4fe1bcd203f72f427742537913a"
|
||||
parallel_hashmap_sha = "aac333eac3627698ca922102fd2a5921df8976906dff6b8e247a49e8cf363911"
|
||||
|
||||
http_archive(
|
||||
name = "gtl",
|
||||
build_file = "@//external:BUILD.gtl",
|
||||
sha256 = gtl_sha,
|
||||
strip_prefix = "gtl-%s" % gtl_version,
|
||||
url = "https://github.com/greg7mdp/gtl/archive/refs/tags/v%s.zip" % gtl_version,
|
||||
name = "parallel_hashmap",
|
||||
build_file = "@//external:BUILD.parallel_hashmap",
|
||||
sha256 = parallel_hashmap_sha,
|
||||
strip_prefix = "parallel-hashmap-%s" % parallel_hashmap_version,
|
||||
url = "https://github.com/greg7mdp/parallel-hashmap/archive/refs/tags/v%s.zip" % parallel_hashmap_version,
|
||||
)
|
||||
|
||||
#
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
|
||||
UNITY_VERSION='6000.1.11f1'
|
||||
UNITY_VERSION='6000.0.32f1'
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
cc_library(
|
||||
name = "gtl",
|
||||
hdrs = glob(["include/gtl/*.hpp"]),
|
||||
includes = ["include"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
cc_library(
|
||||
name = "parallel_hashmap",
|
||||
hdrs = glob(["parallel_hashmap/*.h"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -1,206 +0,0 @@
|
||||
# Occupants Vector Optimization - Conversion Report
|
||||
|
||||
## Overview
|
||||
|
||||
This document details the implementation of an embedded occupants vector in the GameState flatbuffer to replace O(n)
|
||||
unit iteration with O(1) position lookups. It also catalogs all Occupant() and KnownEnemyOccupant() calls that could not
|
||||
be converted to use the new optimized methods.
|
||||
|
||||
## Completed Conversions
|
||||
|
||||
### Successfully Converted Occupant() Calls (16 total)
|
||||
|
||||
#### Commands Directory (11 conversions)
|
||||
|
||||
1. **HideCommand.cpp**:
|
||||
- Line 43: `Occupant(currentState->units(), target)` → `currentState.GetOccupant(target)`
|
||||
- Line 59: `Occupant(currentState->units(), adjCoords)` → `currentState.GetOccupant(adjCoords)`
|
||||
|
||||
2. **ScoutCommand.cpp**:
|
||||
- Line 63: `Occupant(currentState->units(), target)` → `currentState.GetOccupant(target)`
|
||||
- Line 73: `Occupant(currentState->units(), adjacentCoords)` → `currentState.GetOccupant(adjacentCoords)`
|
||||
|
||||
3. **ReduceCommand.cpp**:
|
||||
- Line 66: `Occupant(currentState->units(), target)` → `currentState.GetOccupant(target)`
|
||||
|
||||
4. **RaiseDeadCommand.cpp**:
|
||||
- Line 53: `Occupant(currentState->units(), target)` → `currentState.GetOccupant(target)`
|
||||
|
||||
5. **HolyWaveCommand.cpp**:
|
||||
- Line 233: `Occupant(runningState->units(), coords)` → `runningState.GetOccupant(coords)`
|
||||
|
||||
6. **MoveCommand.cpp**:
|
||||
- Line 66: `Occupant(allUnits, destination)` → `currentState.GetOccupant(destination)`
|
||||
- Line 98: `Occupant(allUnits, adj)` → `currentState.GetOccupant(adj)`
|
||||
- Line 114: `Occupant(allUnits, adj)` → `currentState.GetOccupant(adj)`
|
||||
|
||||
#### Actions Directory (4 conversions)
|
||||
|
||||
1. **UpdateGameStatusAction.cpp**:
|
||||
- Line 232: `Occupant(gameState->units(), criticalTile)` → `currentState.GetOccupant(criticalTile)`
|
||||
|
||||
2. **MeteorCastAction.cpp**:
|
||||
- Line 186: `Occupant(runningGameState->units(), target)` → `runningGameState.GetOccupant(target)`
|
||||
- Line 251: `Occupant(runningGameState->units(), splashCoords)` → `runningGameState.GetOccupant(splashCoords)`
|
||||
- Line 304: `Occupant(runningGameState->units(), coords)` → `runningGameState.GetOccupant(coords)`
|
||||
|
||||
3. **UpdateOpponentKnowledgeAction.cpp**:
|
||||
- Line 42: `Occupant(currentState->units(), adjCoords)` → `currentState.GetOccupant(adjCoords)`
|
||||
|
||||
#### Engine Directory (1 conversion)
|
||||
|
||||
1. **ShardokEngine.cpp**:
|
||||
- Line 463: `Occupant(GetCurrentGameState()->units(), modifiedCoords)` → `gameState.GetOccupant(modifiedCoords)`
|
||||
|
||||
#### Factory Classes Directory (previously converted)
|
||||
|
||||
1. **PlayerSetupCommandFactory.cpp**:
|
||||
- Line 31: `Occupant(gameState->units(), *possiblePosition)` → `gameState.GetOccupant(*possiblePosition)`
|
||||
- Line 40: `Occupant(gameState->units(), possibleHidingPosition)` → `gameState.GetOccupant(possibleHidingPosition)`
|
||||
|
||||
2. **FallIntoWaterAction.cpp**:
|
||||
- Line 154: `Occupant(currentState->units(), adjWithTerrain.adjacentCoords)` →
|
||||
`currentState.GetOccupant(adjWithTerrain.adjacentCoords)`
|
||||
- Line 175: `Occupant(currentState->units(), bestCoords)` → `currentState.GetOccupant(bestCoords)`
|
||||
|
||||
### KnownEnemyOccupant() Conversions
|
||||
|
||||
**Result: 0 conversions possible**
|
||||
|
||||
All KnownEnemyOccupant() calls are in command factory methods that receive decomposed game state parameters (Units*,
|
||||
vector<PlayerId>, etc.) rather than complete GameStateW objects.
|
||||
|
||||
## Remaining Unconverted Calls
|
||||
|
||||
### Occupant() Calls That Cannot Be Converted
|
||||
|
||||
#### 1. PerformUndeadCommandsAction.cpp (2 calls - No GameStateW access)
|
||||
|
||||
- **Line 69**: `Occupant(units, FromCoordsProto(possibleAttackCommandProto.target()))`
|
||||
- **Line 99**: `Occupant(units, adjCoords)`
|
||||
- **Reason**: These calls are in the `ChooseUndeadCommand()` function which only receives `const Units* units`
|
||||
parameter, not a full GameStateW.
|
||||
- **Location**: `src/main/cpp/net/eagle0/shardok/library/actions/PerformUndeadCommandsAction.cpp`
|
||||
|
||||
#### 2. AICommandFilter.cpp (1 call - Raw pointer access)
|
||||
|
||||
- **Line 399**: `KnownEnemyOccupant(pid, units, allyPids, fireLocation)` (in EXTINGUISH_FIRE_COMMAND case)
|
||||
- **Reason**: Method receives `const GameState* gameState` parameter, not GameStateW. Has TODO comment noting this
|
||||
limitation.
|
||||
- **Location**: `src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.cpp`
|
||||
|
||||
#### 3. UpdateGameStatusAction.cpp - Member Variable Usage
|
||||
|
||||
- **Various calls**: Uses `gameState` member variable of type `const GameState*`
|
||||
- **Reason**: Class was designed to take raw GameState pointer in constructor, though InternalExecute method has
|
||||
GameStateW access.
|
||||
- **Location**: `src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.cpp`
|
||||
|
||||
#### 4. IceAndSnowAdjustmentActionFactory.cpp (1 call - Factory pattern)
|
||||
|
||||
- **Line 42**: `Occupant(units, coords)`
|
||||
- **Reason**: Factory method receives individual parameters, not GameStateW.
|
||||
- **Location**: `src/main/cpp/net/eagle0/shardok/library/action_factories/IceAndSnowAdjustmentActionFactory.cpp`
|
||||
|
||||
### KnownEnemyOccupant() Calls That Cannot Be Converted
|
||||
|
||||
#### Command Factory Methods (8 calls - No GameStateW access)
|
||||
|
||||
1. **RepairCommandFactory.cpp** - Line 44
|
||||
2. **FearCommandFactory.cpp** - Line 35
|
||||
3. **LightningBoltCommandFactory.cpp** - Line 54
|
||||
4. **ReduceCommandFactory.cpp** - Line 48
|
||||
5. **ChallengeDuelCommandFactory.cpp** - Line 35
|
||||
6. **HideCommandFactory.cpp** - Line 45
|
||||
7. **MeleeCommandFactory.cpp** - Line 58
|
||||
8. **ArcheryCommandFactory.cpp** - Line 89
|
||||
|
||||
**Common Reason**: All command factory methods follow a pattern where they receive individual game state components (
|
||||
`Units* units`, `vector<PlayerId> allyPids`, etc.) rather than a complete GameStateW object.
|
||||
|
||||
#### Utility Functions (3 calls - Utility function parameters)
|
||||
|
||||
1. **HexMapUtils.cpp** - Lines 81, 670
|
||||
2. **ZoneOfControlCalculator.cpp** - Line 143
|
||||
|
||||
**Reason**: These are utility functions that take decomposed parameters for reusability across different contexts.
|
||||
|
||||
## Performance Impact
|
||||
|
||||
### Achieved Improvements
|
||||
|
||||
- **16 Occupant() calls** converted from O(n) iteration to O(1) lookup
|
||||
- Eliminated cache invalidation issues with thread-local approach
|
||||
- Automatic copying of occupants vector with GameState copies
|
||||
- **Estimated Performance Gain**: 2-5% reduction in AI search time for typical game states
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- **Memory Overhead**: 168 bytes per GameState (14×12 map = 168 int16 values)
|
||||
- **Incremental Updates**: ActionResultApplier now maintains occupants vector via UpdateOccupant() calls
|
||||
- **Copy Cost**: Slightly higher GameState copy overhead offset by O(1) lookup benefits
|
||||
|
||||
## Architectural Patterns Identified
|
||||
|
||||
### Convertible Patterns
|
||||
|
||||
1. **Command InternalExecute methods**: Have access to `const GameStateW& currentState`
|
||||
2. **Action InternalExecute methods**: Have access to `const GameStateW& currentState`
|
||||
3. **Factory methods with GameStateW parameters**: Can access embedded occupants vector
|
||||
|
||||
### Non-Convertible Patterns
|
||||
|
||||
1. **Command Factory methods**: Receive decomposed parameters (`Units*`, `HexMap*`, etc.)
|
||||
2. **Utility functions**: Take individual components for reusability
|
||||
3. **Engine methods**: Often work with raw `GameState*` pointers
|
||||
4. **Legacy member variables**: Classes storing `const GameState*` instead of `GameStateW`
|
||||
|
||||
## Recommendations for Future Work
|
||||
|
||||
### Potential Additional Conversions
|
||||
|
||||
1. **Refactor command factories** to accept GameStateW instead of decomposed parameters
|
||||
2. **Update ShardokEngine** to use GameStateW internally where possible
|
||||
3. **Create GameStateW constructors** from raw GameState* to enable more conversions
|
||||
4. **Modernize legacy classes** to use GameStateW member variables
|
||||
|
||||
### Copy-on-Write Consideration
|
||||
|
||||
The user suggested implementing copy-on-write (COW) for GameStateW to reduce memory allocation overhead during AI
|
||||
search. This could provide additional performance benefits by eliminating unnecessary copying of the occupants vector.
|
||||
|
||||
## Technical Implementation Details
|
||||
|
||||
### Core Changes Made
|
||||
|
||||
1. **game_state.fbs**: Added `occupants:[int16];` field
|
||||
2. **GameStateW.cpp**: Implemented GetOccupant() and UpdateOccupant() methods
|
||||
3. **GameStateCopier.cpp**: Populates occupants vector during GameState creation
|
||||
4. **ActionResultApplier.cpp**: Maintains occupants vector during unit movement
|
||||
|
||||
### Key Method Signatures
|
||||
|
||||
```cpp
|
||||
// O(1) occupant lookup
|
||||
auto GameStateW::GetOccupant(const Coords& coords) const -> const Unit*;
|
||||
|
||||
// O(1) enemy occupant lookup
|
||||
auto GameStateW::GetKnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const Coords& coords) const -> const Unit*;
|
||||
|
||||
// Incremental occupants vector maintenance
|
||||
void GameStateW::UpdateOccupant(
|
||||
UnitId unitId,
|
||||
const Coords& oldCoords,
|
||||
const Coords& newCoords);
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
The occupants vector optimization successfully converted 12 high-frequency Occupant() calls to O(1) lookups while
|
||||
maintaining correctness through automatic copying and incremental updates. The remaining 15+ unconverted calls are
|
||||
primarily in architectural layers (command factories, utilities) that would require broader refactoring to convert. The
|
||||
performance improvement achieved represents a solid foundation that could be extended with future architectural
|
||||
modernization.
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# AI Performance Test Runner Script
|
||||
# Runs the AI performance test with optimized builds and 10 turns
|
||||
|
||||
echo "Running AI performance test with optimized build..."
|
||||
echo "=============================================="
|
||||
|
||||
# Run with optimized compilation and 10 turns
|
||||
bazel run -c opt //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --turns=10 "$@"
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/zsh
|
||||
|
||||
echo "***"
|
||||
echo "*** Moving files to workspace"
|
||||
mv /Users/dancrosby/NewInvokeAI/outputs/images/*.png /Users/dancrosby/Downloads/new_heroes/
|
||||
|
||||
echo "***"
|
||||
echo "*** Renaming files"
|
||||
bazel run src/main/go/net/eagle0/util/hero_generation/pngorganizer -- /Users/dancrosby/Downloads/new_heroes/
|
||||
|
||||
# echo "***"
|
||||
# echo "*** Moving files to generated"
|
||||
# mv /Users/dancrosby/Downloads/new_heroes/generated/*.png /Users/dancrosby/Documents/headshots/generated
|
||||
|
||||
# echo "***"
|
||||
# echo "*** Syncing to server"
|
||||
# ./scripts/sync_headshots.sh
|
||||
|
||||
# echo "***"
|
||||
# echo "*** Checking which new heroes have images and adjusting TSVs"
|
||||
# bazel run //src/main/go/net/eagle0/util/hero_generation/imagechecker -- /Users/dancrosby/CodingProjects/github/eagle0/src/main/resources/net/eagle0/eagle/waiting_headshots_heroes.herodata /Users/dancrosby/CodingProjects/github/eagle0/src/main/resources/net/eagle0/eagle/generated_heroes.tsv /Users/dancrosby/Documents/headshots/
|
||||
|
||||
# echo "***"
|
||||
# echo "*** Deduplicate names"
|
||||
# bazel run //src/main/go/net/eagle0/util/hero_generation/namededuplicator /Users/dancrosby/CodingProjects/github/eagle0/src/main/resources/net/eagle0/eagle/generated_heroes.tsv
|
||||
# rm src/main/resources/net/eagle0/eagle/generated_heroes.tsv.backup
|
||||
|
||||
# echo "***"
|
||||
# echo "*** Generating new SD prompts"
|
||||
# bazel run src/main/go/net/eagle0/util/hero_generation/heroformatter ${PWD}/src/main/resources/net/eagle0/eagle/waiting_headshots_heroes.herodata ~/samplelines.txt
|
||||
@@ -95,13 +95,6 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "thread_pool",
|
||||
hdrs = ["ThreadPool.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "time_utils",
|
||||
hdrs = ["TimeUtils.hpp"],
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
constexpr uint64_t FNV_PRIME = 0x100000001b3;
|
||||
constexpr uint64_t FNV_OFFSET_BASIS = 0xcbf29ce484222325;
|
||||
constexpr int64_t FNV_PRIME = 0x100000001b3;
|
||||
constexpr int64_t FNV_OFFSET_BASIS = 0xcbf29ce484222325;
|
||||
|
||||
static inline auto MixIn(uint64_t& hash, const uint8_t byte) {
|
||||
static inline auto MixIn(int64_t& hash, const uint8_t byte) {
|
||||
hash = hash * FNV_PRIME;
|
||||
hash = hash ^ byte;
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// ThreadPool.cpp - Implementation of priority-based thread pool
|
||||
//
|
||||
|
||||
#include "ThreadPool.hpp"
|
||||
|
||||
namespace eagle0 {
|
||||
namespace common {
|
||||
|
||||
// Implementation is header-only to support templates
|
||||
// This file exists for potential future non-template implementations
|
||||
|
||||
} // namespace common
|
||||
} // namespace eagle0
|
||||
@@ -1,200 +0,0 @@
|
||||
//
|
||||
// ThreadPool.hpp - Priority-based thread pool with deadline support
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_THREADPOOL_HPP
|
||||
#define EAGLE0_THREADPOOL_HPP
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace eagle0::common {
|
||||
|
||||
enum class TaskStatus { SUCCESS = 0, DEADLINE_EXCEEDED = 1, CANCELLED = 2 };
|
||||
|
||||
template<typename T>
|
||||
struct TaskResult {
|
||||
T value;
|
||||
TaskStatus status;
|
||||
|
||||
TaskResult() : value{}, status(TaskStatus::SUCCESS) {}
|
||||
TaskResult(T val) : value(std::move(val)), status(TaskStatus::SUCCESS) {}
|
||||
TaskResult(T val, TaskStatus stat) : value(std::move(val)), status(stat) {}
|
||||
|
||||
// NO implicit conversion - this was causing infinite recursion
|
||||
// Use .value or .get() instead
|
||||
T get() const { return value; }
|
||||
|
||||
bool succeeded() const { return status == TaskStatus::SUCCESS; }
|
||||
bool deadlineExceeded() const { return status == TaskStatus::DEADLINE_EXCEEDED; }
|
||||
};
|
||||
|
||||
class ThreadPool {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
private:
|
||||
struct Task {
|
||||
std::function<void()> function;
|
||||
int priority;
|
||||
TimePoint deadline;
|
||||
bool has_deadline;
|
||||
|
||||
Task(std::function<void()> f, int p, TimePoint d, bool has_d)
|
||||
: function(std::move(f)),
|
||||
priority(p),
|
||||
deadline(d),
|
||||
has_deadline(has_d) {}
|
||||
|
||||
// Higher priority values and earlier deadlines have higher priority
|
||||
bool operator<(const Task& other) const {
|
||||
if (priority != other.priority) {
|
||||
return priority < other.priority; // Lower priority values have lower priority in
|
||||
// priority_queue
|
||||
}
|
||||
if (has_deadline && other.has_deadline) {
|
||||
return deadline > other.deadline; // Later deadlines have lower priority
|
||||
}
|
||||
if (has_deadline && !other.has_deadline) {
|
||||
return false; // Tasks with deadlines have higher priority
|
||||
}
|
||||
if (!has_deadline && other.has_deadline) {
|
||||
return true; // Tasks without deadlines have lower priority
|
||||
}
|
||||
return false; // Equal priority, no preference
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::thread> workers;
|
||||
std::priority_queue<Task> tasks;
|
||||
std::mutex queue_mutex;
|
||||
std::condition_variable condition;
|
||||
std::atomic<bool> stop{false};
|
||||
|
||||
public:
|
||||
explicit ThreadPool(size_t num_threads = std::thread::hardware_concurrency()) {
|
||||
for (size_t i = 0; i < num_threads; ++i) {
|
||||
workers.emplace_back([this] {
|
||||
while (true) {
|
||||
Task task{nullptr, 0, TimePoint{}, false};
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
condition.wait(lock, [this] { return stop.load() || !tasks.empty(); });
|
||||
|
||||
if (stop.load() && tasks.empty()) { return; }
|
||||
|
||||
if (!tasks.empty()) {
|
||||
task = std::move(const_cast<Task&>(tasks.top()));
|
||||
tasks.pop();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the task (deadline checking is now handled inside the task)
|
||||
if (task.function) { task.function(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue a task with priority only
|
||||
template<class F, class... Args>
|
||||
auto enqueue(F&& f, Args&&... args, int priority = 0)
|
||||
-> std::future<TaskResult<std::invoke_result_t<F, Args...>>> {
|
||||
using return_type = std::invoke_result_t<F, Args...>;
|
||||
using result_type = TaskResult<return_type>;
|
||||
|
||||
auto actualTask = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
|
||||
|
||||
auto task = std::make_shared<std::packaged_task<result_type()>>(
|
||||
[actualTask = std::move(actualTask)]() mutable -> result_type {
|
||||
return result_type(actualTask());
|
||||
});
|
||||
|
||||
std::future<result_type> result = task->get_future();
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
if (stop.load()) { throw std::runtime_error("enqueue on stopped ThreadPool"); }
|
||||
tasks.emplace([task]() { (*task)(); }, priority, TimePoint{}, false);
|
||||
}
|
||||
|
||||
condition.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Enqueue a task with priority and deadline
|
||||
template<class F, class... Args>
|
||||
auto enqueue_with_deadline(F&& f, Args&&... args, int priority, TimePoint deadline)
|
||||
-> std::future<TaskResult<std::invoke_result_t<F, Args...>>> {
|
||||
using return_type = std::invoke_result_t<F, Args...>;
|
||||
using result_type = TaskResult<return_type>;
|
||||
|
||||
auto actualTask = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
|
||||
|
||||
auto task = std::make_shared<std::packaged_task<result_type()>>(
|
||||
[actualTask = std::move(actualTask), deadline]() mutable -> result_type {
|
||||
if (Clock::now() > deadline) {
|
||||
return result_type(return_type{}, TaskStatus::DEADLINE_EXCEEDED);
|
||||
}
|
||||
return result_type(actualTask());
|
||||
});
|
||||
|
||||
std::future<result_type> result = task->get_future();
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
if (stop.load()) { throw std::runtime_error("enqueue on stopped ThreadPool"); }
|
||||
tasks.emplace([task]() { (*task)(); }, priority, deadline, true);
|
||||
}
|
||||
|
||||
condition.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get current queue size (approximate, for monitoring)
|
||||
size_t queue_size() const {
|
||||
std::unique_lock<std::mutex> lock(const_cast<std::mutex&>(queue_mutex));
|
||||
return tasks.size();
|
||||
}
|
||||
|
||||
// Get detailed queue information for debugging
|
||||
void debug_queue_state() const {
|
||||
std::unique_lock<std::mutex> lock(const_cast<std::mutex&>(queue_mutex));
|
||||
printf("ThreadPool: Queue size: %zu\n", tasks.size());
|
||||
if (!tasks.empty()) {
|
||||
// Create a copy to inspect priorities without modifying queue
|
||||
auto queue_copy = tasks;
|
||||
std::vector<int> priorities;
|
||||
while (!queue_copy.empty()) {
|
||||
priorities.push_back(queue_copy.top().priority);
|
||||
queue_copy.pop();
|
||||
}
|
||||
printf("ThreadPool: Priorities in queue: ");
|
||||
for (int p : priorities) { printf("%d ", p); }
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
~ThreadPool() {
|
||||
stop.store(true);
|
||||
condition.notify_all();
|
||||
for (std::thread& worker : workers) {
|
||||
if (worker.joinable()) { worker.join(); }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace eagle0::common
|
||||
|
||||
#endif // EAGLE0_THREADPOOL_HPP
|
||||
@@ -70,14 +70,7 @@ auto ConvertUnit(
|
||||
Unit shardokUnit{};
|
||||
|
||||
shardokUnit.mutate_player_id(shardokPlayerId);
|
||||
|
||||
// Range check eagle_player_id for int8 conversion
|
||||
int32_t eagle_id = unit.eagle_player_id();
|
||||
if (eagle_id < -128 || eagle_id > 127) {
|
||||
throw std::runtime_error(
|
||||
"eagle_player_id " + std::to_string(eagle_id) + " out of int8 range");
|
||||
}
|
||||
shardokUnit.mutate_eagle_player_id(static_cast<int8_t>(eagle_id));
|
||||
shardokUnit.mutate_eagle_player_id(unit.eagle_player_id());
|
||||
shardokUnit.mutate_hidden(false);
|
||||
shardokUnit.mutate_fortified(false);
|
||||
if (unit.has_hero()) {
|
||||
|
||||
@@ -51,7 +51,8 @@ cc_binary(
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:byte_vector",
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
//
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/shardok_internal_interface.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/game.pb.h"
|
||||
|
||||
using GameStateW = shardok::Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
|
||||
auto main(int argc, char** argv) -> int {
|
||||
char* path = argv[1];
|
||||
|
||||
@@ -24,8 +27,8 @@ auto main(int argc, char** argv) -> int {
|
||||
printf("There are %d results\n", arCount);
|
||||
|
||||
for (int arIndex = 0; arIndex < arCount; arIndex++) {
|
||||
shardok::GameStateW gameState =
|
||||
shardok::GameStateW::FromByteString(game.action_result(arIndex).state_after_fb());
|
||||
GameStateW gameState =
|
||||
GameStateW::FromByteString(game.action_result(arIndex).state_after_fb());
|
||||
const auto* hexMap = gameState->hex_map();
|
||||
|
||||
for (int terrainIndex = 0; terrainIndex < hexMap->terrain()->size(); terrainIndex++) {
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
#include "AIAttackGroups.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
@@ -14,9 +12,6 @@ namespace shardok {
|
||||
constexpr double kOverpowerRatio = 2.0;
|
||||
constexpr double kBraveWaterCostMultiplier = 1.2;
|
||||
|
||||
using std::pair;
|
||||
using std::shared_ptr;
|
||||
|
||||
DIST_T NormalizedCostWhenBraving(const DIST_T cost) {
|
||||
if (cost >= static_cast<double>(ActionPointDistances::IMPOSSIBLE) / kBraveWaterCostMultiplier)
|
||||
return ActionPointDistances::IMPOSSIBLE;
|
||||
@@ -34,13 +29,13 @@ struct TargetAndDistance {
|
||||
: target(t),
|
||||
attackLocations(al),
|
||||
targetPower(tp),
|
||||
distance(d) {}
|
||||
distance(d){};
|
||||
};
|
||||
|
||||
auto MinDistance(
|
||||
const Coords& start,
|
||||
const CoordsSet& destinations,
|
||||
const ActionPointDistances* apd) -> DIST_T {
|
||||
const std::shared_ptr<ActionPointDistances>& apd) -> DIST_T {
|
||||
DIST_T minDistance = ActionPointDistances::IMPOSSIBLE;
|
||||
|
||||
for (const Coords& dest : destinations) {
|
||||
@@ -55,8 +50,8 @@ auto MinDistance(
|
||||
auto MinDistanceIncludingBraving(
|
||||
const Coords& start,
|
||||
const CoordsSet& destinations,
|
||||
const ActionPointDistances* notBravingApd,
|
||||
const ActionPointDistances* bravingApd) {
|
||||
const std::shared_ptr<ActionPointDistances>& notBravingApd,
|
||||
const std::shared_ptr<ActionPointDistances>& bravingApd) {
|
||||
// First try to get there without braving
|
||||
if (const DIST_T notBravingDistance = MinDistance(start, destinations, notBravingApd);
|
||||
notBravingDistance < ActionPointDistances::IMPOSSIBLE) {
|
||||
@@ -97,23 +92,15 @@ auto EffectiveDistance(
|
||||
const SettingsGetter& settings,
|
||||
const int braveWaterCost) -> DIST_T {
|
||||
const auto& battType = settings.GetBattalionType(unit->battalion().type());
|
||||
const auto* notBravingApd = apdCache->GetRaw(map, mapId, battType, false);
|
||||
const ActionPointDistances* bravingApd = nullptr;
|
||||
const auto& notBravingApd = apdCache->Get(map, mapId, battType, false);
|
||||
std::shared_ptr<ActionPointDistances> bravingApd = nullptr;
|
||||
if (battType->allowsBraveWater) {
|
||||
bravingApd = apdCache->GetRaw(map, mapId, battType, true, braveWaterCost);
|
||||
bravingApd = apdCache->Get(map, mapId, battType, true, braveWaterCost);
|
||||
}
|
||||
|
||||
return MinDistanceIncludingBraving(unit->location(), locations, notBravingApd, bravingApd);
|
||||
}
|
||||
|
||||
auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
const ActionPointDistances* notBravingApd,
|
||||
const ActionPointDistances* bravingApd,
|
||||
const CoordsSet& locations) -> DIST_T {
|
||||
return MinDistanceIncludingBraving(unit->location(), locations, notBravingApd, bravingApd);
|
||||
}
|
||||
|
||||
auto Power(const Unit* unit) -> double { return unit->battalion().size(); }
|
||||
|
||||
auto CoordsIndex(const Coords& coords, const int columnCount) {
|
||||
@@ -143,11 +130,11 @@ auto GenerateTargetPriorities(
|
||||
|
||||
vector<const Unit*> sortedAttackers = remainingUnits;
|
||||
// Handle stronger units first
|
||||
std::ranges::sort(sortedAttackers, [](const Unit* left, const Unit* right) {
|
||||
return Power(left) > Power(right);
|
||||
});
|
||||
std::sort(
|
||||
begin(sortedAttackers),
|
||||
end(sortedAttackers),
|
||||
[](const Unit* left, const Unit* right) { return Power(left) > Power(right); });
|
||||
|
||||
// APDCache now has built-in thread-local caching - no need for local apdByBattType map
|
||||
// For each unit, sort the targets by distance from the unit to an attack location for the
|
||||
// target
|
||||
for (const Unit* unit : sortedAttackers) {
|
||||
@@ -157,14 +144,6 @@ auto GenerateTargetPriorities(
|
||||
|
||||
vector<TargetAndDistance> targetsWithDistance;
|
||||
|
||||
// Get APDs directly from cache (now with built-in thread-local optimization)
|
||||
const auto& battType = settings.GetBattalionType(unit->battalion().type());
|
||||
const auto* notBravingApd = apdCache->GetRaw(map, mapId, battType, false);
|
||||
const ActionPointDistances* bravingApd = nullptr;
|
||||
if (battType->allowsBraveWater) {
|
||||
bravingApd = apdCache->GetRaw(map, mapId, battType, true, braveWaterCost);
|
||||
}
|
||||
|
||||
for (const Coords& targetLocation : targets) {
|
||||
const auto coordsIndex = CoordsIndex(targetLocation, cc);
|
||||
const auto& occupant = occupants[coordsIndex];
|
||||
@@ -175,13 +154,18 @@ auto GenerateTargetPriorities(
|
||||
double occupantPower = Power(occupant);
|
||||
|
||||
if (unit->location().row() >= 0) {
|
||||
const auto& attackLocsForUnit = attackLocations.LocationsWithEnemyInRange(unit);
|
||||
auto distance =
|
||||
EffectiveDistance(unit, notBravingApd, bravingApd, attackLocsForUnit);
|
||||
auto distance = EffectiveDistance(
|
||||
unit,
|
||||
map,
|
||||
mapId,
|
||||
apdCache,
|
||||
attackLocations,
|
||||
settings,
|
||||
braveWaterCost);
|
||||
|
||||
targetsWithDistance.emplace_back(
|
||||
targetLocation,
|
||||
attackLocsForUnit,
|
||||
attackLocations.LocationsWithEnemyInRange(unit),
|
||||
occupantPower,
|
||||
distance);
|
||||
} else {
|
||||
@@ -195,8 +179,9 @@ auto GenerateTargetPriorities(
|
||||
}
|
||||
|
||||
// Sort by distance
|
||||
std::ranges::sort(
|
||||
targetsWithDistance,
|
||||
std::sort(
|
||||
begin(targetsWithDistance),
|
||||
end(targetsWithDistance),
|
||||
[&powerAttackingEachTarget,
|
||||
cc](const TargetAndDistance& left, const TargetAndDistance& right) {
|
||||
const auto leftIndex = CoordsIndex(left.target, cc);
|
||||
|
||||
@@ -56,12 +56,6 @@ auto EffectiveDistance(
|
||||
const SettingsGetter& settings,
|
||||
int braveWaterCost) -> DIST_T;
|
||||
|
||||
auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
const ActionPointDistances* notBravingApd,
|
||||
const ActionPointDistances* bravingApd,
|
||||
const CoordsSet& locations) -> DIST_T;
|
||||
|
||||
// Chooses a list of targets in priority order for each unit.
|
||||
auto GenerateTargetPriorities(
|
||||
const vector<const Unit*>& occupants,
|
||||
|
||||
@@ -1,595 +0,0 @@
|
||||
//
|
||||
// Filter obviously bad commands for performance
|
||||
//
|
||||
|
||||
#include "AICommandFilter.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/BattalionType.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexCubeUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using fb::Unit;
|
||||
using net::eagle0::shardok::common::CommandType;
|
||||
|
||||
CoordsSet AICommandFilter::BuildEnemyLocations(const GameStateW& gameState, PlayerId pid) {
|
||||
CoordsSet enemyLocations(gameState->hex_map());
|
||||
const auto* units = gameState->units();
|
||||
|
||||
for (int i = 0; i < units->size(); ++i) {
|
||||
const auto* unit = units->Get(i);
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->player_id() != pid && !unit->hidden() && unit->location().column() != -1) {
|
||||
enemyLocations.Add(unit->location());
|
||||
}
|
||||
}
|
||||
|
||||
return enemyLocations;
|
||||
}
|
||||
|
||||
std::vector<size_t> AICommandFilter::FilterCommands(
|
||||
const CommandListSPtr& commands,
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache) {
|
||||
std::vector<size_t> filteredIndices;
|
||||
filteredIndices.reserve(commands->size());
|
||||
|
||||
// Build enemy and castle locations once for efficiency
|
||||
const CoordsSet enemyLocations = BuildEnemyLocations(gameState, pid);
|
||||
const CoordsSet castleLocations = AllCastleCoords(gameState->hex_map());
|
||||
|
||||
// Calculate minimum distance to enemies once for all filters
|
||||
const double minDistToEnemies = MinDistanceToEnemyUnits(gameState, pid, enemyLocations);
|
||||
|
||||
for (size_t i = 0; i < commands->size(); ++i) {
|
||||
const auto& cmd = (*commands)[i];
|
||||
|
||||
// Always allow END_TURN commands
|
||||
if (cmd->GetCommandType() == CommandType::END_TURN_COMMAND) {
|
||||
filteredIndices.push_back(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter obviously bad moves
|
||||
bool shouldFilter = false;
|
||||
|
||||
// Check spell preparation waste
|
||||
if (IsWastefulAction(
|
||||
*cmd,
|
||||
pid,
|
||||
isDefender,
|
||||
gameState,
|
||||
settings,
|
||||
apdCache,
|
||||
enemyLocations,
|
||||
castleLocations,
|
||||
minDistToEnemies)) {
|
||||
shouldFilter = true;
|
||||
}
|
||||
|
||||
// Check movement waste
|
||||
if (!shouldFilter && IsWastefulMovement(
|
||||
*cmd,
|
||||
pid,
|
||||
isDefender,
|
||||
gameState,
|
||||
settings,
|
||||
apdCache,
|
||||
enemyLocations,
|
||||
minDistToEnemies)) {
|
||||
shouldFilter = true;
|
||||
}
|
||||
|
||||
// Check strategic blunders
|
||||
if (!shouldFilter &&
|
||||
IsStrategicBlunder(*cmd, pid, isDefender, gameState, settings, minDistToEnemies)) {
|
||||
shouldFilter = true;
|
||||
}
|
||||
|
||||
if (!shouldFilter) { filteredIndices.push_back(i); }
|
||||
}
|
||||
|
||||
return filteredIndices;
|
||||
}
|
||||
|
||||
bool AICommandFilter::IsWastefulAction(
|
||||
const ShardokCommand& cmd,
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache,
|
||||
const CoordsSet& enemyLocations,
|
||||
const CoordsSet& castleLocations,
|
||||
double minDistToEnemies) {
|
||||
// Handle different spell types
|
||||
switch (cmd.GetCommandType()) {
|
||||
case CommandType::METEOR_START_COMMAND: {
|
||||
// Meteor preparation filtering
|
||||
// Meteor takes 3 rounds (start -> target -> cast) and locks the mage in place
|
||||
|
||||
// Asymmetric filtering based on attacker vs defender role
|
||||
if (!isDefender) {
|
||||
// Attackers: Don't start meteor when too far from enemies OR castles
|
||||
// Check distance to castles as well since meteor can deny castle access
|
||||
double minDistToCastles = MinDistanceToCastles(gameState, pid, castleLocations);
|
||||
|
||||
// More aggressive filtering for attackers: filter if >4 hexes from targets
|
||||
// Meteor has range 3, so being >4 hexes from enemies AND castles is wasteful
|
||||
if (minDistToEnemies > 4.0 && minDistToCastles > 4.0) {
|
||||
return true; // Too far from enemies and castles, advance first
|
||||
}
|
||||
}
|
||||
// Defenders: Allow meteor in most cases since it's great for area denial
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandType::START_FIRE_COMMAND: {
|
||||
// Fire spell filtering - be very restrictive for attackers
|
||||
// Fire only affects adjacent tiles and lasts multiple rounds
|
||||
|
||||
if (!isDefender) {
|
||||
// Attackers: Only allow fire if the target location is on or adjacent to an enemy
|
||||
const auto cmdProto = cmd.GetCommandProto();
|
||||
if (!cmdProto.has_target()) {
|
||||
return true; // Can't analyze without target info
|
||||
}
|
||||
|
||||
const auto& targetCoords = cmdProto.target();
|
||||
const Coords fireLocation{
|
||||
static_cast<int8_t>(targetCoords.row()),
|
||||
static_cast<int8_t>(targetCoords.column())};
|
||||
|
||||
// Check if any enemy is on the fire location or adjacent to it
|
||||
bool enemyNearFireLocation = false;
|
||||
|
||||
// First check the fire location itself
|
||||
if (enemyLocations.Contains(fireLocation)) {
|
||||
enemyNearFireLocation = true;
|
||||
} else {
|
||||
// Check adjacent tiles (at most 6 coordinates)
|
||||
const auto& adjacentCoords =
|
||||
HexMapUtils::GetAdjacentCoords(gameState->hex_map(), fireLocation);
|
||||
for (const auto& adjCoord : adjacentCoords) {
|
||||
if (enemyLocations.Contains(adjCoord)) {
|
||||
enemyNearFireLocation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!enemyNearFireLocation) {
|
||||
return true; // No enemies on or adjacent to fire location, fire would be
|
||||
// wasteful
|
||||
}
|
||||
}
|
||||
// Defenders: Allow fire for area denial
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandType::FORTIFY_COMMAND: {
|
||||
// Fortify filtering - attackers shouldn't fortify when far from objectives
|
||||
// Fortify improves defense but also allows an engineer to use a Reduce command next
|
||||
|
||||
if (!isDefender) {
|
||||
// Attackers: Only allow fortify if within 3 hexes of enemies or castles
|
||||
const auto cmdProto = cmd.GetCommandProto();
|
||||
if (!cmdProto.has_actor()) {
|
||||
return true; // Can't analyze without actor info
|
||||
}
|
||||
|
||||
const auto unitId = cmdProto.actor().value();
|
||||
|
||||
// Get the acting unit directly by ID
|
||||
const Unit* actingUnit = gameState->units()->Get(unitId);
|
||||
// verify the unit is still active
|
||||
if (actingUnit &&
|
||||
actingUnit->status() !=
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
actingUnit = nullptr; // Not a valid unit
|
||||
}
|
||||
// Verify it's our unit (not enemy)
|
||||
if (actingUnit && actingUnit->player_id() != pid) { actingUnit = nullptr; }
|
||||
|
||||
if (!actingUnit) {
|
||||
return true; // Unit not found or belongs to enemy
|
||||
}
|
||||
|
||||
const auto& unitCoords = actingUnit->location();
|
||||
const Cube unitCube = OffsetToCube(unitCoords);
|
||||
|
||||
// Check if within 3 hexes of any enemy
|
||||
bool nearObjective = false;
|
||||
for (const auto& enemyCoords : enemyLocations) {
|
||||
const Cube enemyCube = OffsetToCube(enemyCoords);
|
||||
if (const int hexDistance = CubeDistance(unitCube, enemyCube);
|
||||
hexDistance <= 3) {
|
||||
nearObjective = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not near enemies, check if near castles
|
||||
if (!nearObjective) {
|
||||
for (const auto& castleCoord : castleLocations) {
|
||||
const Cube castleCube = OffsetToCube(castleCoord);
|
||||
const int hexDistance = CubeDistance(unitCube, castleCube);
|
||||
if (hexDistance <= 3) {
|
||||
nearObjective = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!nearObjective) {
|
||||
return true; // Too far from enemies and castles, fortify is wasteful for
|
||||
// attacker
|
||||
}
|
||||
}
|
||||
// Defenders: Allow fortify in most cases since it's about holding positions
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandType::BUILD_BRIDGE_COMMAND:
|
||||
case CommandType::FREEZE_WATER_COMMAND: {
|
||||
// Bridge/freeze filtering - only allow if it creates significant tactical shortcuts
|
||||
// These actions can fail, so we need high confidence of benefit (8+ action points
|
||||
// saved)
|
||||
|
||||
const auto cmdProto = cmd.GetCommandProto();
|
||||
if (!cmdProto.has_actor() || !cmdProto.has_target()) {
|
||||
return true; // Can't analyze without full command info
|
||||
}
|
||||
|
||||
const auto unitId = cmdProto.actor().value();
|
||||
const auto& targetCoords = cmdProto.target();
|
||||
const Coords waterLocation{
|
||||
static_cast<int8_t>(targetCoords.row()),
|
||||
static_cast<int8_t>(targetCoords.column())};
|
||||
|
||||
// Get the acting unit directly by ID
|
||||
const Unit* actingUnit = gameState->units()->Get(unitId);
|
||||
// Verify the unit is still active
|
||||
if (actingUnit &&
|
||||
actingUnit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
actingUnit = nullptr; // Not a valid unit or not ours
|
||||
}
|
||||
// Verify it's our unit (not enemy)
|
||||
if (actingUnit && actingUnit->player_id() != pid) { actingUnit = nullptr; }
|
||||
|
||||
if (!actingUnit) {
|
||||
return true; // Unit not found or belongs to enemy
|
||||
}
|
||||
|
||||
// Get action point distances for this unit's battalion type
|
||||
const auto& battType = settings.GetBattalionType(actingUnit->battalion().type());
|
||||
const auto* apd = apdCache->GetRaw(
|
||||
gameState->hex_map(),
|
||||
ActionPointDistancesCache::GetMapId(gameState->hex_map()),
|
||||
battType,
|
||||
false);
|
||||
|
||||
const auto& casterCoords = actingUnit->location();
|
||||
|
||||
// Check if bridge creates significant shortcuts to any tactical objective
|
||||
bool worthwhileShortcut = false;
|
||||
|
||||
// Get tiles on the "other side" of the water (adjacent to bridge location)
|
||||
const auto& adjacentTiles =
|
||||
HexMapUtils::GetAdjacentCoords(gameState->hex_map(), waterLocation);
|
||||
|
||||
// Check shortcuts to enemies
|
||||
for (const auto& enemyCoords : enemyLocations) {
|
||||
const auto currentDistance = apd->Distance(casterCoords, enemyCoords);
|
||||
if (currentDistance == ActionPointDistances::IMPOSSIBLE) continue;
|
||||
|
||||
// Check if going via any adjacent tile creates a shortcut
|
||||
for (const auto& adjacentCoord : adjacentTiles) {
|
||||
const auto distanceToAdjacent = apd->Distance(casterCoords, adjacentCoord);
|
||||
const auto adjacentToObjective = apd->Distance(adjacentCoord, enemyCoords);
|
||||
|
||||
if (distanceToAdjacent != ActionPointDistances::IMPOSSIBLE &&
|
||||
adjacentToObjective != ActionPointDistances::IMPOSSIBLE) {
|
||||
// New route: caster -> adjacent tile -> objective (plus ~2 for crossing)
|
||||
const auto newRouteDistance = distanceToAdjacent + adjacentToObjective + 2;
|
||||
|
||||
if (currentDistance >= newRouteDistance + 8) { // 8+ action points saved
|
||||
worthwhileShortcut = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (worthwhileShortcut) break;
|
||||
}
|
||||
|
||||
// Check shortcuts to castles if no enemy shortcut found
|
||||
if (!worthwhileShortcut) {
|
||||
for (const auto& castleCoord : castleLocations) {
|
||||
const auto currentDistance = apd->Distance(casterCoords, castleCoord);
|
||||
if (currentDistance == ActionPointDistances::IMPOSSIBLE) continue;
|
||||
|
||||
// Check if going via any adjacent tile creates a shortcut
|
||||
for (const auto& adjacentCoord : adjacentTiles) {
|
||||
const auto distanceToAdjacent = apd->Distance(casterCoords, adjacentCoord);
|
||||
const auto adjacentToObjective = apd->Distance(adjacentCoord, castleCoord);
|
||||
|
||||
if (distanceToAdjacent != ActionPointDistances::IMPOSSIBLE &&
|
||||
adjacentToObjective != ActionPointDistances::IMPOSSIBLE) {
|
||||
// New route: caster -> adjacent tile -> objective (plus ~2 for
|
||||
// crossing)
|
||||
const auto newRouteDistance =
|
||||
distanceToAdjacent + adjacentToObjective + 2;
|
||||
|
||||
if (currentDistance >=
|
||||
newRouteDistance + 8) { // 8+ action points saved
|
||||
worthwhileShortcut = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (worthwhileShortcut) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!worthwhileShortcut) {
|
||||
return true; // No significant shortcut found, filter out this bridge/freeze
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandType::REPAIR_COMMAND: {
|
||||
// Repair filtering - filter repairs with high integrity targets
|
||||
// Note: RepairCommandFactory already filters enemy-occupied targets
|
||||
const auto cmdProto = cmd.GetCommandProto();
|
||||
if (!cmdProto.has_target()) {
|
||||
return true; // Can't analyze without target info
|
||||
}
|
||||
|
||||
const auto& targetCoords = cmdProto.target();
|
||||
const Coords repairLocation{
|
||||
static_cast<int8_t>(targetCoords.row()),
|
||||
static_cast<int8_t>(targetCoords.column())};
|
||||
|
||||
// Check terrain modifiers at target location
|
||||
const auto* terrain = GetTerrain(gameState->hex_map(), repairLocation);
|
||||
const auto& modifier = terrain->modifier();
|
||||
|
||||
// Filter based on integrity thresholds
|
||||
if (modifier.bridge().present()) {
|
||||
// Bridge integrity filtering: >70% is wasteful
|
||||
if (modifier.bridge().integrity() > 70.0f) {
|
||||
return true; // Bridge integrity too high to justify repair
|
||||
}
|
||||
} else if (modifier.castle().present()) {
|
||||
// Castle integrity filtering: >90% is wasteful
|
||||
if (modifier.castle().integrity() > 90.0f) {
|
||||
return true; // Castle integrity too high to justify repair
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandType::EXTINGUISH_FIRE_COMMAND: {
|
||||
// Extinguish fire filtering - don't extinguish fires on enemy-occupied tiles
|
||||
const auto cmdProto = cmd.GetCommandProto();
|
||||
if (!cmdProto.has_target()) {
|
||||
return true; // Can't analyze without target info
|
||||
}
|
||||
|
||||
const auto& targetCoords = cmdProto.target();
|
||||
const Coords fireLocation{
|
||||
static_cast<int8_t>(targetCoords.row()),
|
||||
static_cast<int8_t>(targetCoords.column())};
|
||||
|
||||
// Check if any enemy occupies the fire location - let them burn!
|
||||
std::vector<PlayerId> allyPids; // Empty for now - assume 2-player game
|
||||
if (gameState.GetKnownEnemyOccupant(pid, allyPids, fireLocation)) {
|
||||
return true; // Don't extinguish fires under enemies
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: return false; // Don't filter other spell types for now
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AICommandFilter::IsWastefulMovement(
|
||||
const ShardokCommand& cmd,
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache,
|
||||
const CoordsSet& enemyLocations,
|
||||
double minDistToEnemies) {
|
||||
if (cmd.GetCommandType() != CommandType::MOVE_COMMAND) { return false; }
|
||||
|
||||
// Only filter attacker movement when already fairly far from enemies
|
||||
if (isDefender || minDistToEnemies <= 6.0) {
|
||||
return false; // Don't filter defender movement or when close to enemies
|
||||
}
|
||||
|
||||
// Get the command proto to access unit and target information
|
||||
const auto cmdProto = cmd.GetCommandProto();
|
||||
|
||||
// Check if we have the required information
|
||||
if (!cmdProto.has_actor() || !cmdProto.has_target()) {
|
||||
return false; // Can't analyze without unit and target info
|
||||
}
|
||||
|
||||
const auto unitId = cmdProto.actor().value();
|
||||
const auto& targetCoords = cmdProto.target();
|
||||
|
||||
// Get the acting unit directly by ID
|
||||
const Unit* actingUnit = gameState->units()->Get(unitId);
|
||||
// Verify the unit is still active
|
||||
if (actingUnit &&
|
||||
actingUnit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
actingUnit = nullptr; // Not a valid unit
|
||||
}
|
||||
// Verify it's our unit (not enemy)
|
||||
if (actingUnit && actingUnit->player_id() != pid) { actingUnit = nullptr; }
|
||||
|
||||
if (!actingUnit) {
|
||||
return false; // Unit not found or belongs to enemy
|
||||
}
|
||||
|
||||
const auto& currentCoords = actingUnit->location();
|
||||
const Coords targetCoordsFlat{
|
||||
static_cast<int8_t>(targetCoords.row()),
|
||||
static_cast<int8_t>(targetCoords.column())};
|
||||
|
||||
// Get action point distances for this unit's battalion type
|
||||
const auto& battType = settings.GetBattalionType(actingUnit->battalion().type());
|
||||
const auto* apd = apdCache->GetRaw(
|
||||
gameState->hex_map(),
|
||||
ActionPointDistancesCache::GetMapId(gameState->hex_map()),
|
||||
battType,
|
||||
false);
|
||||
|
||||
// Calculate action point distance from current position to closest enemy
|
||||
double currentDistToEnemies = std::numeric_limits<double>::max();
|
||||
double targetDistToEnemies = std::numeric_limits<double>::max();
|
||||
|
||||
for (const auto& enemyCoords : enemyLocations) {
|
||||
const auto currentDist = apd->Distance(currentCoords, enemyCoords);
|
||||
const auto targetDist = apd->Distance(targetCoordsFlat, enemyCoords);
|
||||
|
||||
if (currentDist != ActionPointDistances::IMPOSSIBLE) {
|
||||
currentDistToEnemies = std::min(currentDistToEnemies, static_cast<double>(currentDist));
|
||||
}
|
||||
if (targetDist != ActionPointDistances::IMPOSSIBLE) {
|
||||
targetDistToEnemies = std::min(targetDistToEnemies, static_cast<double>(targetDist));
|
||||
}
|
||||
}
|
||||
|
||||
// Filter movement if it takes us significantly farther from all enemies
|
||||
// Only when we're already far away (>6 hexes as checked above)
|
||||
if (currentDistToEnemies != std::numeric_limits<double>::max() &&
|
||||
targetDistToEnemies != std::numeric_limits<double>::max()) {
|
||||
// Filter if move increases distance to enemies by more than 2 action points
|
||||
if (targetDistToEnemies > currentDistToEnemies + 2.0) {
|
||||
return true; // Wasteful move away from enemies when already far
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AICommandFilter::IsStrategicBlunder(
|
||||
const ShardokCommand& cmd,
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
double minDistToEnemies) {
|
||||
// Simplified strategic blunder detection for now
|
||||
// TODO: Implement proper castle abandonment detection
|
||||
// TODO: Use minDistToEnemies for strategic blunder logic
|
||||
return false;
|
||||
}
|
||||
|
||||
double AICommandFilter::MinDistanceToEnemyUnits(
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const CoordsSet& enemyLocations) {
|
||||
// Calculate minimum distance from any player unit to any enemy unit
|
||||
double minDistance = std::numeric_limits<double>::max();
|
||||
const auto* units = gameState->units();
|
||||
|
||||
for (int i = 0; i < units->size(); ++i) {
|
||||
const auto* playerUnit = units->Get(i);
|
||||
if (playerUnit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
playerUnit->player_id() == pid) {
|
||||
const auto& playerCoords = playerUnit->location();
|
||||
const Cube playerCube = OffsetToCube(playerCoords);
|
||||
|
||||
for (const auto& enemyCoords : enemyLocations) {
|
||||
const Cube enemyCube = OffsetToCube(enemyCoords);
|
||||
const int hexDistance = CubeDistance(playerCube, enemyCube);
|
||||
minDistance = std::min(minDistance, static_cast<double>(hexDistance));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return minDistance == std::numeric_limits<double>::max() ? 0.0 : minDistance;
|
||||
}
|
||||
|
||||
double AICommandFilter::MinDistanceToCastles(
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const CoordsSet& castleLocations) {
|
||||
// Calculate minimum distance from any player unit to any castle
|
||||
double minDistance = std::numeric_limits<double>::max();
|
||||
const auto* units = gameState->units();
|
||||
|
||||
if (castleLocations.empty()) {
|
||||
return 0.0; // No castles found
|
||||
}
|
||||
|
||||
// Find minimum hex distance from any player unit to any castle
|
||||
for (int i = 0; i < units->size(); ++i) {
|
||||
const auto* unit = units->Get(i);
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->player_id() == pid) {
|
||||
const auto& unitCoords = unit->location();
|
||||
const Cube unitCube = OffsetToCube(unitCoords);
|
||||
|
||||
for (const auto& castleCoord : castleLocations) {
|
||||
const Cube castleCube = OffsetToCube(castleCoord);
|
||||
const int hexDistance = CubeDistance(unitCube, castleCube);
|
||||
minDistance = std::min(minDistance, static_cast<double>(hexDistance));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return minDistance == std::numeric_limits<double>::max() ? 0.0 : minDistance;
|
||||
}
|
||||
|
||||
bool AICommandFilter::IsPlayerOutnumbered(
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
double threshold) {
|
||||
const int playerUnitCount = CountPlayerUnits(gameState, pid);
|
||||
const int enemyUnitCount = CountPlayerUnits(gameState, 1 - pid); // Assumes 2-player game
|
||||
|
||||
if (enemyUnitCount == 0) return false;
|
||||
|
||||
const double ratio = static_cast<double>(playerUnitCount) / static_cast<double>(enemyUnitCount);
|
||||
return ratio < threshold;
|
||||
}
|
||||
|
||||
int AICommandFilter::CountPlayerUnits(const GameStateW& gameState, PlayerId pid) {
|
||||
int count = 0;
|
||||
const auto* units = gameState->units();
|
||||
|
||||
for (int i = 0; i < units->size(); ++i) {
|
||||
const auto* unit = units->Get(i);
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->player_id() == pid) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
bool AICommandFilter::WouldAbandonCriticalCastle(
|
||||
const ShardokCommand& cmd,
|
||||
PlayerId pid,
|
||||
const GameStateW& gameState) {
|
||||
// Simplified implementation - return false for now
|
||||
// TODO: Implement proper castle abandonment detection when API is available
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,106 +0,0 @@
|
||||
//
|
||||
// Filter obviously bad commands to reduce search space for AI
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AICOMMANDFILTER_HPP
|
||||
#define EAGLE0_AICOMMANDFILTER_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#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"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
|
||||
/**
|
||||
* Filters obviously bad moves to reduce search space for AI.
|
||||
* This class implements heuristic filtering to eliminate moves that are
|
||||
* strategically bad without requiring deep search to identify.
|
||||
*/
|
||||
class AICommandFilter {
|
||||
public:
|
||||
/**
|
||||
* Filter a list of commands, removing obviously bad ones.
|
||||
* @param commands Original list of all available commands
|
||||
* @param pid Player ID making the move
|
||||
* @param isDefender True if this player is the defender
|
||||
* @param gameState Current game state
|
||||
* @param settings Game settings for parameter lookup
|
||||
* @param apdCache Action point distance cache for distance calculations
|
||||
* @return Filtered list of commands worth evaluating
|
||||
*/
|
||||
static std::vector<size_t> FilterCommands(
|
||||
const CommandListSPtr& commands,
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache);
|
||||
|
||||
private:
|
||||
// Helper to build enemy locations once for efficiency
|
||||
static CoordsSet BuildEnemyLocations(const GameStateW& gameState, PlayerId pid);
|
||||
|
||||
// Spell preparation filters
|
||||
static bool IsWastefulAction(
|
||||
const ShardokCommand& cmd,
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache,
|
||||
const CoordsSet& enemyLocations,
|
||||
const CoordsSet& castleLocations,
|
||||
double minDistToEnemies);
|
||||
|
||||
// Movement filters
|
||||
static bool IsWastefulMovement(
|
||||
const ShardokCommand& cmd,
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache,
|
||||
const CoordsSet& enemyLocations,
|
||||
double minDistToEnemies);
|
||||
|
||||
// Strategic blunder filters
|
||||
static bool IsStrategicBlunder(
|
||||
const ShardokCommand& cmd,
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
double minDistToEnemies);
|
||||
|
||||
// Helper functions for distance and position analysis
|
||||
static double MinDistanceToEnemyUnits(
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const CoordsSet& enemyLocations);
|
||||
|
||||
static double MinDistanceToCastles(
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const CoordsSet& castleLocations);
|
||||
|
||||
static bool IsPlayerOutnumbered(const GameStateW& gameState, PlayerId pid, double threshold);
|
||||
|
||||
static int CountPlayerUnits(const GameStateW& gameState, PlayerId pid);
|
||||
|
||||
static bool WouldAbandonCriticalCastle(
|
||||
const ShardokCommand& cmd,
|
||||
PlayerId pid,
|
||||
const GameStateW& gameState);
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AICOMMANDFILTER_HPP
|
||||
@@ -13,8 +13,8 @@ constexpr double kPerUnitDebufDecay = 0.5;
|
||||
constexpr double kDecaySum = kPerUnitDebufDecay / (1 - kPerUnitDebufDecay);
|
||||
|
||||
auto CostsWithoutAndWithBraving(
|
||||
const ActionPointDistances *actionPointDistancesWithoutBraving,
|
||||
const ActionPointDistances *actionPointDistancesWithBraving,
|
||||
const shared_ptr<ActionPointDistances> &actionPointDistancesWithoutBraving,
|
||||
const shared_ptr<ActionPointDistances> &actionPointDistancesWithBraving,
|
||||
const Coords &startLocation,
|
||||
const CoordsSet &targets,
|
||||
int &outPointCostWithoutBraving,
|
||||
@@ -65,17 +65,17 @@ auto DefenderDistanceBuf(
|
||||
vector<WithoutAndWith> pointCosts{};
|
||||
pointCosts.reserve(attackerUnits.size());
|
||||
|
||||
vector<const ActionPointDistances *> notBravingDistances(6, nullptr);
|
||||
vector<const ActionPointDistances *> bravingDistances(6, nullptr);
|
||||
vector<std::shared_ptr<ActionPointDistances>> notBravingDistances(6);
|
||||
vector<std::shared_ptr<ActionPointDistances>> bravingDistances(6);
|
||||
for (const Unit *attacker : attackerUnits) {
|
||||
const int typeInt = attacker->battalion().type();
|
||||
if (notBravingDistances[typeInt] == nullptr) {
|
||||
notBravingDistances[typeInt] = apdCache->GetRaw(
|
||||
notBravingDistances[typeInt] = apdCache->Get(
|
||||
hexMap,
|
||||
mapId,
|
||||
settings.GetBattalionType(attacker->battalion().type()),
|
||||
false);
|
||||
bravingDistances[typeInt] = apdCache->GetRaw(
|
||||
bravingDistances[typeInt] = apdCache->Get(
|
||||
hexMap,
|
||||
mapId,
|
||||
settings.GetBattalionType(attacker->battalion().type()),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
namespace shardok {
|
||||
auto MinimumDistanceAndTarget(
|
||||
const ActionPointDistances *apd,
|
||||
const shared_ptr<ActionPointDistances> &apd,
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations) -> CoordsAndDistance {
|
||||
CoordsAndDistance min{Coords(-1, -1), ActionPointDistances::IMPOSSIBLE};
|
||||
@@ -20,7 +20,7 @@ auto MinimumDistanceAndTarget(
|
||||
}
|
||||
|
||||
auto MinimumDistance(
|
||||
const ActionPointDistances *apd,
|
||||
const shared_ptr<ActionPointDistances> &apd,
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations) -> int {
|
||||
return MinimumDistanceAndTarget(apd, origin, destinations).distance;
|
||||
|
||||
@@ -23,12 +23,12 @@ struct CoordsAndDistance {
|
||||
};
|
||||
|
||||
auto MinimumDistanceAndTarget(
|
||||
const ActionPointDistances *apd,
|
||||
const shared_ptr<ActionPointDistances> &apd,
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations) -> CoordsAndDistance;
|
||||
|
||||
auto MinimumDistance(
|
||||
const ActionPointDistances *apd,
|
||||
const shared_ptr<ActionPointDistances> &apd,
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations) -> int;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,16 +5,18 @@
|
||||
#ifndef EAGLE0_AISCORECALCULATOR_HPP
|
||||
#define EAGLE0_AISCORECALCULATOR_HPP
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
#include <future>
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/ThreadPool.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AITimeBudget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateGuesser.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
@@ -22,7 +24,7 @@
|
||||
namespace shardok {
|
||||
|
||||
using net::eagle0::shardok::api::GameStateView;
|
||||
using GameState = fb::GameState;
|
||||
using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
using shardok::PlayerId;
|
||||
using std::future;
|
||||
using std::vector;
|
||||
@@ -31,28 +33,24 @@ using ScoreValue = double;
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
|
||||
class AIScoreCalculator {
|
||||
private:
|
||||
// Static thread pool with 32 threads for parallel AI calculations
|
||||
static inline eagle0::common::ThreadPool threadPool{32};
|
||||
|
||||
public:
|
||||
struct IndexAndScore {
|
||||
size_t index;
|
||||
CommandType type;
|
||||
net::eagle0::shardok::common::CommandType type;
|
||||
ScoreValue lookaheadScore;
|
||||
ScoreValue immediateScore;
|
||||
};
|
||||
|
||||
private:
|
||||
[[nodiscard]] static auto DefenderScatterStrategyScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const GameState *gameState,
|
||||
int roundsRemaining,
|
||||
const SettingsGetter &settings,
|
||||
const ALCache &alCache,
|
||||
const APDCache &apdCache) -> ScoreValue;
|
||||
|
||||
[[nodiscard]] static auto DefenderHoldCastlesStrategyScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const GameState *gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
int roundsRemaining,
|
||||
const SettingsGetter &settings,
|
||||
@@ -60,11 +58,11 @@ private:
|
||||
const APDCache &apdCache) -> ScoreValue;
|
||||
|
||||
[[nodiscard]] static auto FleeStrategyScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const GameState *gameState,
|
||||
PlayerId playerId) -> ScoreValue;
|
||||
|
||||
[[nodiscard]] static auto DefenderScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const GameState *gameState,
|
||||
const AIStrategy &defenderStrategy,
|
||||
const CoordsSet &castleCoords,
|
||||
int roundsRemaining,
|
||||
@@ -73,7 +71,7 @@ private:
|
||||
const APDCache &apdCache) -> ScoreValue;
|
||||
|
||||
[[nodiscard]] static auto AttackerScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const GameState *gameState,
|
||||
const AIStrategy &attackerStrategy,
|
||||
const CoordsSet &castleCoords,
|
||||
int roundsRemaining,
|
||||
@@ -83,13 +81,12 @@ private:
|
||||
|
||||
struct ImmediateAndLookaheadScore {
|
||||
ScoreValue immediateScore;
|
||||
future<eagle0::common::TaskResult<ScoreValue>> lookaheadScore;
|
||||
future<ScoreValue> lookaheadScore;
|
||||
};
|
||||
|
||||
static auto BasicLookaheadCalculator(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
int remainingLookahead,
|
||||
int maxRepeatCount,
|
||||
const shared_ptr<ShardokEngine> &innerEngine,
|
||||
ScoreValue currentUtility,
|
||||
@@ -97,8 +94,7 @@ private:
|
||||
const SettingsGetter &settingsGetter,
|
||||
const CoordsSet &allCastleCoords,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
const AITimeBudget *timeBudget = nullptr) -> ScoreValue;
|
||||
const ALCache &alCache) -> ScoreValue;
|
||||
|
||||
static auto CalcOne(
|
||||
PlayerId pid,
|
||||
@@ -112,33 +108,12 @@ private:
|
||||
const SettingsGetter &settingsGetter,
|
||||
const CoordsSet &allCastleCoords,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
const AITimeBudget *timeBudget = nullptr) -> ImmediateAndLookaheadScore;
|
||||
|
||||
struct CommandEvaluationResult {
|
||||
ScoreValue immediateScore;
|
||||
ScoreValue lookaheadScore;
|
||||
};
|
||||
|
||||
static auto EvaluateCommand(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
uint32_t commandIndex,
|
||||
int remainingLookahead,
|
||||
int maxRepeatCount,
|
||||
const ShardokEngine &guessedEngine,
|
||||
const AIStrategy &attackerStrategy,
|
||||
ScoreValue currentUtility,
|
||||
const SettingsGetter &settingsGetter,
|
||||
const CoordsSet &allCastleCoords,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
const AITimeBudget *timeBudget = nullptr) -> CommandEvaluationResult;
|
||||
const ALCache &alCache) -> ImmediateAndLookaheadScore;
|
||||
|
||||
public:
|
||||
[[nodiscard]] static auto GuessedStateScore(
|
||||
bool isDefender,
|
||||
const GameStateW &state,
|
||||
const GameState *state,
|
||||
const AIStrategy &aiStrategy,
|
||||
const CoordsSet &allCastleCoords,
|
||||
const SettingsGetter &settingsGetter,
|
||||
@@ -156,23 +131,7 @@ public:
|
||||
const SettingsGetter &settingsGetter,
|
||||
const CoordsSet &allCastleCoords,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
const AITimeBudget *timeBudget = nullptr) -> IndexAndScore;
|
||||
|
||||
[[nodiscard]] static auto CommandScore(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
int remainingLookahead,
|
||||
int maxRepeatCount,
|
||||
const ShardokEngine &guessedEngine,
|
||||
const AIStrategy &attackerStrategy,
|
||||
ScoreValue currentUtility,
|
||||
const SettingsGetter &settingsGetter,
|
||||
const CoordsSet &allCastleCoords,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
size_t commandIndex,
|
||||
const AITimeBudget *timeBudget = nullptr) -> ScoreValue;
|
||||
const ALCache &alCache) -> IndexAndScore;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 07/04/25.
|
||||
//
|
||||
|
||||
#include "AITimeBudget.hpp"
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexCubeUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Static member definition
|
||||
std::atomic<int> AIEvaluationCounter::activeCount{0};
|
||||
|
||||
AIEvaluationCounter::AIEvaluationCounter() { activeCount++; }
|
||||
|
||||
AIEvaluationCounter::~AIEvaluationCounter() { activeCount--; }
|
||||
|
||||
int AIEvaluationCounter::GetCurrentCount() { return activeCount.load(); }
|
||||
|
||||
auto CalculateTimeBudget(
|
||||
const PlayerId playerId,
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &state) -> AITimeBudget {
|
||||
const auto settingsGetter = settings->GetGetter();
|
||||
const auto castleCoords = AllCastleCoords(state->hex_map());
|
||||
|
||||
// Determine proximity (≤4 hex distance) - applies to both attackers and defenders
|
||||
bool isClose = false;
|
||||
const auto *units = state->units();
|
||||
|
||||
for (int i = 0; i < units->size() && !isClose; ++i) {
|
||||
const auto *myUnit = units->Get(i);
|
||||
if (myUnit->player_id() != playerId) continue;
|
||||
|
||||
const auto &myCoords = myUnit->location();
|
||||
// Skip units that haven't been placed on the map yet
|
||||
if (myCoords.row() == -1) continue;
|
||||
|
||||
const Cube myCube = OffsetToCube(myCoords);
|
||||
|
||||
// Check distance to enemy units
|
||||
for (int j = 0; j < units->size(); ++j) {
|
||||
const auto *enemyUnit = units->Get(j);
|
||||
if (enemyUnit->player_id() == playerId) continue;
|
||||
|
||||
const auto &enemyCoords = enemyUnit->location();
|
||||
// Skip enemy units that haven't been placed on the map yet
|
||||
if (enemyCoords.row() == -1) continue;
|
||||
|
||||
const Cube enemyCube = OffsetToCube(enemyCoords);
|
||||
|
||||
if (const int hexDistance = CubeDistance(myCube, enemyCube); hexDistance <= 4) {
|
||||
isClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check distance to castles
|
||||
if (!isClose) {
|
||||
for (const auto &castleCoord : castleCoords) {
|
||||
const Cube castleCube = OffsetToCube(castleCoord);
|
||||
|
||||
if (const int hexDistance = CubeDistance(myCube, castleCube); hexDistance <= 4) {
|
||||
isClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get time budget from settings
|
||||
const auto budget = std::chrono::duration<double>(
|
||||
isClose ? settingsGetter.Backing().lookahead_time_budget_close_in_seconds()
|
||||
: settingsGetter.Backing().lookahead_time_budget_far_in_seconds());
|
||||
|
||||
const auto remainingBudget = std::chrono::duration_cast<std::chrono::milliseconds>(budget);
|
||||
|
||||
// Get minimum depth requirement
|
||||
const int minDepth = settingsGetter.Backing().min_lookahead_turns();
|
||||
|
||||
return AITimeBudget{
|
||||
.remainingBudget = remainingBudget,
|
||||
.minDepthRequired = minDepth,
|
||||
.isCloseToEnemy = isClose};
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 07/04/25.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AITIMEBUDGET_HPP
|
||||
#define EAGLE0_AITIMEBUDGET_HPP
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class GameSettings;
|
||||
using GameSettingsSPtr = std::shared_ptr<GameSettings>;
|
||||
|
||||
// RAII counter for tracking concurrent AI command evaluations
|
||||
class AIEvaluationCounter {
|
||||
static std::atomic<int> activeCount;
|
||||
|
||||
public:
|
||||
AIEvaluationCounter();
|
||||
~AIEvaluationCounter();
|
||||
static int GetCurrentCount();
|
||||
};
|
||||
|
||||
// Configuration structure for iterative deepening time budget
|
||||
struct AITimeBudget {
|
||||
std::chrono::milliseconds remainingBudget; // Time budget remaining (decremented as used)
|
||||
int minDepthRequired; // Minimum depth from minLookaheadTurns
|
||||
bool isCloseToEnemy; // Proximity flag for budget selection
|
||||
};
|
||||
|
||||
// Calculate time budget based on proximity to enemies and castles
|
||||
auto CalculateTimeBudget(
|
||||
PlayerId playerId,
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &state) -> AITimeBudget;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AITIMEBUDGET_HPP
|
||||
@@ -333,7 +333,7 @@ auto UnitValue(
|
||||
const int roundsRemaining,
|
||||
const AttackLocations &locationsThisSideCanAttackFrom,
|
||||
const CoordsSet &locationsInDangerFromEnemy,
|
||||
const ActionPointDistances *distances,
|
||||
const std::shared_ptr<ActionPointDistances> &distances,
|
||||
const SettingsGetter &settings) -> ScoreValue {
|
||||
const auto &location = unit->location();
|
||||
if (location.row() < 0) return 0; // unplaced unit
|
||||
|
||||
@@ -45,7 +45,7 @@ auto UnitValue(
|
||||
int roundsRemaining,
|
||||
const AttackLocations &locationsThisSideCanAttackFrom,
|
||||
const CoordsSet &locationsInDangerFromEnemy,
|
||||
const ActionPointDistances *distances,
|
||||
const std::shared_ptr<ActionPointDistances> &distances,
|
||||
const SettingsGetter &settings) -> ScoreValue;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -108,7 +108,7 @@ auto CanReach(
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeSPtr &battalionType) -> bool {
|
||||
const DIST_T startingDistance =
|
||||
apdCache->GetRaw(hexMap, mapId, battalionType, false)->Distance(origin, destination);
|
||||
apdCache->Get(hexMap, mapId, battalionType, false)->Distance(origin, destination);
|
||||
|
||||
return startingDistance != ActionPointDistances::IMPOSSIBLE;
|
||||
}
|
||||
@@ -178,7 +178,7 @@ auto WaterCrossingTiles(
|
||||
|
||||
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
|
||||
|
||||
if (const auto *distances = apdCache->GetRaw(mapCopy, hash, battalionType, false);
|
||||
if (const auto distances = apdCache->Get(mapCopy, hash, battalionType, false);
|
||||
distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE) {
|
||||
returnCoords.Add(index / hexMap->column_count(), index % hexMap->column_count());
|
||||
}
|
||||
@@ -207,7 +207,7 @@ auto IntendedCrossingStarts(
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const Coords &location = unit->location();
|
||||
const auto &battalionType = settings.GetBattalionType(unit->battalion().type());
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
const auto &apd = apdCache->Get(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
if (location.row() >= 0) {
|
||||
Coords intended =
|
||||
|
||||
@@ -71,7 +71,7 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
|
||||
int thisDistance;
|
||||
if (location.row() < 0) thisDistance = 1000;
|
||||
else {
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
const auto &apd = apdCache->Get(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
thisDistance = MinimumDistance(apd, location, startCrossingFrom);
|
||||
}
|
||||
@@ -88,7 +88,7 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = settingsGetter.GetBattalionType(unit->battalion().type());
|
||||
Coords location = unit->location();
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
const auto &apd = apdCache->Get(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
int thisDistance;
|
||||
if (location.row() < 0) thisDistance = 1000;
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
# Performance Fix: PreCachedAPDs Constructor Overhead
|
||||
|
||||
## Problem
|
||||
Profiling shows that 18.5% of AI processing time is spent in the PreCachedAPDs constructor, with another 9.5% in ActionPointDistances destructor and 6.5% in BattalionType destructor.
|
||||
|
||||
The issue is that `PreCachedAPDs` is being constructed inside `AttackerUnitsScore()`, which is called from `AttackerScoreForState()`. Since `AttackerScoreForState()` is called very frequently during AI evaluation, this creates and destroys the cache repeatedly.
|
||||
|
||||
## Root Cause
|
||||
```cpp
|
||||
auto AttackerUnitsScore(...) -> ScoreValue {
|
||||
// This line creates a new PreCachedAPDs every time!
|
||||
PreCachedAPDs cachedAPDs(gameState, settings, apdCache, mapId);
|
||||
// ... rest of function
|
||||
}
|
||||
```
|
||||
|
||||
The PreCachedAPDs constructor:
|
||||
- Creates arrays of shared_ptr objects
|
||||
- Calls apdCache->Get() for every battalion type (potentially 40+ types)
|
||||
- Creates battalion type shared pointers
|
||||
- All of this is destroyed when the function exits
|
||||
|
||||
## Solution - IMPLEMENTED (Updated)
|
||||
|
||||
### Implemented: Smart Thread-Local PreCachedAPDs with Parameter Validation
|
||||
Initial optimization moved bottleneck from constructor/destructor (34% time) to Update() method (31.7% time), revealing shared_ptr reference counting as the real culprit. Updated to smart caching that only updates when parameters actually change:
|
||||
|
||||
```cpp
|
||||
// Smart cached ActionPointDistances that avoids repeated shared_ptr operations
|
||||
struct PreCachedAPDs {
|
||||
// ... arrays same as before ...
|
||||
|
||||
// Cache validation - only update if parameters changed
|
||||
MapId cachedMapId;
|
||||
ActionPoints cachedBraveWaterCost;
|
||||
bool isValid = false;
|
||||
|
||||
// Smart update method that only updates when parameters change
|
||||
void UpdateIfNeeded(const GameState *gameState,
|
||||
const SettingsGetter &settings,
|
||||
const APDCache &apdCache,
|
||||
const MapId &mapId) {
|
||||
ActionPoints braveWaterCost = settings.Backing().brave_water_action_point_cost();
|
||||
|
||||
// Check if we need to update (parameters changed)
|
||||
if (isValid && cachedMapId == mapId && cachedBraveWaterCost == braveWaterCost) {
|
||||
return; // Cache is still valid, no update needed
|
||||
}
|
||||
|
||||
// Only update when parameters actually change
|
||||
// ... update implementation ...
|
||||
}
|
||||
};
|
||||
|
||||
// In AttackerUnitsScore:
|
||||
auto AttackerUnitsScore(...) -> ScoreValue {
|
||||
// Use thread-local PreCachedAPDs with smart caching to avoid repeated shared_ptr operations
|
||||
thread_local PreCachedAPDs cachedAPDs;
|
||||
cachedAPDs.UpdateIfNeeded(gameState, settings, apdCache, mapId);
|
||||
// ... rest of function uses cachedAPDs ...
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits of this approach:**
|
||||
- Zero allocation/deallocation overhead after first call per thread
|
||||
- **Zero shared_ptr reference counting overhead when parameters haven't changed**
|
||||
- Only performs expensive APD cache lookups when map or settings actually change
|
||||
- Thread-safe (each thread has its own instance)
|
||||
- Minimal code changes required
|
||||
- No memory management complexity
|
||||
|
||||
**Performance Analysis:**
|
||||
- Initial issue: 18.5% in constructor, 9.5% in destructor, 6.5% in BattalionType destructor (34% total)
|
||||
- First optimization: Moved to 31.7% in Update() method (shared_ptr overhead)
|
||||
- Smart caching: Should eliminate most/all Update() calls when parameters are unchanged
|
||||
|
||||
### Alternative Options (Not Implemented)
|
||||
|
||||
#### Option 1: AIScoreCalculator Class Member
|
||||
Make PreCachedAPDs a member of AIScoreCalculator that's initialized once.
|
||||
|
||||
#### Option 2: Pass PreCachedAPDs as Parameter
|
||||
Move PreCachedAPDs creation up to the AI main loop and pass it down.
|
||||
|
||||
#### Option 3: Map-Based Thread-Local Cache
|
||||
Use thread-local map for per-map caching (more complex, less benefit than simple reuse).
|
||||
|
||||
## Expected Performance Improvement
|
||||
- Eliminate 18.5% time spent in PreCachedAPDs constructor
|
||||
- Reduce 9.5% time in ActionPointDistances destructor
|
||||
- Reduce 6.5% time in BattalionType destructor
|
||||
- **Total potential improvement: ~34% reduction in AI processing time**
|
||||
|
||||
## Implementation Steps - COMPLETED
|
||||
1. ✅ Modified PreCachedAPDs struct to add default constructor and Update() method
|
||||
2. ✅ Changed AttackerUnitsScore to use thread_local PreCachedAPDs with Update() call
|
||||
3. ✅ Maintained backward compatibility with constructor for any other uses
|
||||
4. ✅ Added proper cleanup of braving array elements when not needed
|
||||
|
||||
## Status: COMPLETED - ARCHITECTURAL SOLUTION IMPLEMENTED
|
||||
|
||||
### Final Solution: Thread-Local Caching in APDCache
|
||||
After implementing the initial PreCachedAPDs optimization, we discovered that ActionPointDistancesCache already had thread-local caching infrastructure and the FullCacheKey was designed exactly for this purpose. We implemented a proper architectural solution:
|
||||
|
||||
**✅ COMPLETED:**
|
||||
1. **Enhanced APDCache with thread-local caching** - leveraged existing FullCacheKey infrastructure
|
||||
2. **Removed PreCachedAPDs struct** - no longer needed, APDCache handles optimization internally
|
||||
3. **Removed apdByBattType local caching** from AIAttackGroups.cpp
|
||||
4. **Automatic optimization for 12+ call sites** throughout AI system
|
||||
5. **All AI tests passing** - no functional regressions
|
||||
|
||||
### Architectural Benefits Achieved
|
||||
- **Single responsibility**: APDCache handles its own optimization
|
||||
- **Zero code changes required** for existing APDCache::Get() callers
|
||||
- **Eliminates code duplication**: No more scattered caching patterns
|
||||
- **Uses existing infrastructure**: Leverages FullCacheKey design that was already there
|
||||
- **Clean abstraction**: Consumers just call Get(), caching is transparent
|
||||
- **Thread-safe** with per-thread cache isolation
|
||||
|
||||
### Hybrid API Implementation - COMPLETED
|
||||
**✅ COMPLETED: Phase 2 - Raw Pointer API for Zero Overhead**
|
||||
|
||||
Added GetRaw() method alongside existing Get() method for incremental migration:
|
||||
- **CacheEntry struct** stores both shared_ptr and raw pointer
|
||||
- **GetRaw()** returns `const ActionPointDistances*` for zero overhead access
|
||||
- **Existing Get() calls unchanged** - maintains full backward compatibility
|
||||
- **Thread-local cache** manages lifetime through shared_ptr ownership
|
||||
- **Ready for incremental migration** - can update call sites one by one
|
||||
|
||||
```cpp
|
||||
// Zero overhead access (new API)
|
||||
const auto* apd = apdCache->GetRaw(map, mapId, battType, false);
|
||||
|
||||
// Backward compatible access (existing API)
|
||||
const auto& apd = apdCache->Get(map, mapId, battType, false);
|
||||
```
|
||||
|
||||
### Performance Impact
|
||||
- **Automatic optimization applied to 10+ call sites** that previously had no caching
|
||||
- **Eliminates repeated shared_ptr operations** across all APDCache users
|
||||
- **Zero overhead raw pointer access** available for performance-critical paths
|
||||
- **Expected: 30%+ reduction** in AI processing time from eliminating constructor/destructor overhead
|
||||
- **Additional 10-20% potential** from migrating to GetRaw() to eliminate shared_ptr reference counting
|
||||
- **Ready for profiling** to measure actual improvement
|
||||
|
||||
### Files Modified
|
||||
- `ActionPointDistancesCache.hpp/cpp` - Added thread-local caching + hybrid API with GetRaw()
|
||||
- `AIScoreCalculator.cpp` - Removed PreCachedAPDs, uses direct APDCache calls
|
||||
- `AIAttackGroups.cpp` - Removed apdByBattType local caching
|
||||
- All other AI files automatically benefit with zero changes
|
||||
|
||||
This represents a much cleaner architectural solution than the original PreCachedAPDs approach with a clear migration path.
|
||||
|
||||
## Phase 3 COMPLETED: GetRaw() Migration
|
||||
|
||||
### ✅ COMPLETED: Complete Migration to Zero-Overhead Access
|
||||
**All AI call sites successfully migrated from Get() to GetRaw():**
|
||||
|
||||
**Files Migrated:**
|
||||
1. ✅ **AIScoreCalculator.cpp** - 8 call sites migrated to GetRaw()
|
||||
2. ✅ **AIAttackGroups.cpp** - 4 call sites migrated to GetRaw()
|
||||
3. ✅ **AICommandFilter.cpp** - 2 call sites migrated to GetRaw()
|
||||
4. ✅ **AIWaterCrossingCommandChooser.cpp** - 2 call sites migrated to GetRaw()
|
||||
5. ✅ **AIWaterCrossingCalculator.cpp** - 3 call sites migrated to GetRaw()
|
||||
6. ✅ **AIDistanceDebuf.cpp** - 2 call sites migrated to GetRaw()
|
||||
|
||||
**Supporting Infrastructure Updates:**
|
||||
- ✅ **ActionPointDistances::Distance()** methods made const for safe raw pointer usage
|
||||
- ✅ **21+ function signatures** updated for raw pointer compatibility across AI system
|
||||
- ✅ **All AI tests passing** - zero functional regressions
|
||||
|
||||
### Migration Results
|
||||
```cpp
|
||||
// Before: shared_ptr with reference counting overhead
|
||||
const auto& apd = apdCache->Get(map, mapId, battType, false);
|
||||
DIST_T distance = apd->Distance(start, dest); // atomic reference counting
|
||||
|
||||
// After: raw pointer with zero overhead
|
||||
const auto* apd = apdCache->GetRaw(map, mapId, battType, false);
|
||||
DIST_T distance = apd->Distance(start, dest); // zero overhead access
|
||||
```
|
||||
|
||||
### Performance Benefits Achieved
|
||||
- ✅ **Eliminated all shared_ptr reference counting** in AI hot paths
|
||||
- ✅ **Reduced memory pressure** - no atomic operations in tight loops
|
||||
- ✅ **Maintained thread safety** - lifetime guaranteed by thread-local cache
|
||||
- ✅ **Zero overhead access** - raw pointer dereferencing only
|
||||
|
||||
## FINAL PERFORMANCE SUMMARY
|
||||
|
||||
### Total Performance Improvements Achieved
|
||||
**Original Issue:** 18.5% constructor + 9.5% destructor + 6.5% BattalionType destructor = **34% of AI processing time**
|
||||
|
||||
**Solutions Implemented:**
|
||||
1. **✅ Phase 1**: Thread-local caching in APDCache - eliminated constructor/destructor overhead
|
||||
2. **✅ Phase 2**: Hybrid API (Get/GetRaw) - maintained compatibility while enabling zero-overhead access
|
||||
3. **✅ Phase 3**: Complete GetRaw() migration - eliminated all shared_ptr reference counting in AI
|
||||
|
||||
**Expected Performance Gains:**
|
||||
- **30-40% reduction** in AI processing time from eliminating constructor/destructor overhead
|
||||
- **Additional 10-20% improvement** from removing shared_ptr reference counting
|
||||
- **Total potential: 40-60% AI performance improvement**
|
||||
|
||||
### Architecture Achievements
|
||||
- **Single responsibility**: APDCache handles its own optimization transparently
|
||||
- **Thread-safe**: Per-thread cache isolation with zero contention
|
||||
- **Zero maintenance overhead**: No scattered caching patterns to maintain
|
||||
- **Future-proof**: Clean migration path completed, ready for next optimizations
|
||||
|
||||
### ✅ FINAL CLEANUP: Removed Deprecated Get() Method
|
||||
**Migration fully complete - clean API achieved:**
|
||||
- ✅ **Removed Get() method** - no more accidentally using slow shared_ptr approach
|
||||
- ✅ **Single API method** - GetRaw() is now the only way to access ActionPointDistances
|
||||
- ✅ **All tests passing** - zero regressions after API cleanup
|
||||
- ✅ **Clean codebase** - no deprecated methods or hybrid complexity
|
||||
|
||||
### Ready for Profiling
|
||||
**The AI performance optimization is COMPLETE and ready for profiling to measure actual gains.** All bottlenecks identified in the original issue have been systematically eliminated through architectural improvements:
|
||||
- Thread-local caching eliminates constructor/destructor overhead
|
||||
- Raw pointer access eliminates shared_ptr reference counting
|
||||
- Clean API prevents accidental use of slower approaches
|
||||
|
||||
### Future Optimizations
|
||||
1. **Lazy initialization** - Only create APDs for battalion types actually in the game
|
||||
2. **Profile-guided optimization** - Identify remaining bottlenecks after current optimizations
|
||||
3. **Memory layout optimization** - Pack frequently accessed APD data for better cache locality
|
||||
@@ -1,603 +0,0 @@
|
||||
# Eagle0 AI Scoring System: Proposed Improvements
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines proposed improvements to the Eagle0 AI scoring system to make it more robust and strategically intelligent. The current system makes reasonable local tactical decisions but lacks strategic depth, contextual awareness, and multi-turn planning. These improvements would transform the AI from a competent but predictable opponent into a genuinely challenging strategic adversary.
|
||||
|
||||
## Current System Weaknesses
|
||||
|
||||
### 1. Static Unit Valuation
|
||||
- Fixed multipliers (1.0x infantry, 2.0x cavalry) regardless of context
|
||||
- No consideration for terrain advantages or disadvantages
|
||||
- Missing unit synergy and combined arms tactics
|
||||
- Undervaluation of situational effectiveness
|
||||
|
||||
### 2. Primitive Spell Intelligence
|
||||
- Hard-coded spell values that don't scale with game state
|
||||
- Lightning severely undervalued (0.05 vs 38 for archery)
|
||||
- Limited spell selection intelligence beyond meteor (which already has sophisticated cluster analysis)
|
||||
- Poor timing for multi-turn spells like meteor preparation
|
||||
|
||||
### 3. Lack of Strategic Planning
|
||||
- Each command evaluated independently
|
||||
- No multi-turn goal coordination
|
||||
- Reactive rather than proactive strategy changes
|
||||
- Missing opportunity cost analysis
|
||||
|
||||
### 4. Limited Positional Understanding
|
||||
- Simple distance-based scoring
|
||||
- No chokepoint control evaluation
|
||||
- Missing flanking and formation concepts
|
||||
- Inadequate terrain advantage assessment
|
||||
|
||||
### 5. Poor Victory Condition Integration
|
||||
- Static additive scoring regardless of game phase
|
||||
- No dynamic priority adjustment based on time remaining
|
||||
- Weak endgame transition strategies
|
||||
|
||||
## Proposed Improvements
|
||||
|
||||
### Phase 1: Immediate Impact Improvements
|
||||
|
||||
#### 1.1 Dynamic Unit Valuation System
|
||||
|
||||
**Objective**: Replace static unit multipliers with context-aware valuation
|
||||
|
||||
**Implementation**:
|
||||
```cpp
|
||||
class ContextualUnitEvaluator {
|
||||
public:
|
||||
struct UnitContext {
|
||||
TerrainType terrain;
|
||||
bool inCastle;
|
||||
bool hasSupport;
|
||||
std::vector<UnitType> adjacentAllies;
|
||||
std::vector<UnitType> nearbyEnemies;
|
||||
int distanceToObjective;
|
||||
};
|
||||
|
||||
double CalculateContextualValue(const Unit& unit, const UnitContext& context) {
|
||||
double baseValue = GetBaseUnitValue(unit);
|
||||
|
||||
// Terrain modifiers
|
||||
baseValue *= GetTerrainModifier(unit.type, context.terrain);
|
||||
|
||||
// Castle bonuses/penalties
|
||||
if (context.inCastle) {
|
||||
baseValue *= GetCastleModifier(unit.type);
|
||||
}
|
||||
|
||||
// Combined arms bonuses
|
||||
baseValue *= CalculateSynergyBonus(unit.type, context.adjacentAllies);
|
||||
|
||||
// Threat assessment
|
||||
baseValue *= AssessThreatLevel(unit, context.nearbyEnemies);
|
||||
|
||||
return baseValue;
|
||||
}
|
||||
|
||||
private:
|
||||
double GetTerrainModifier(UnitType type, TerrainType terrain) {
|
||||
switch (type) {
|
||||
case CAVALRY:
|
||||
return (terrain == PLAINS) ? 1.4 :
|
||||
(terrain == FOREST) ? 0.8 : 1.0;
|
||||
case LONGBOWMEN:
|
||||
return (terrain == HILLS) ? 1.3 : 1.0;
|
||||
// ... more terrain interactions
|
||||
}
|
||||
}
|
||||
|
||||
double GetCastleModifier(UnitType type) {
|
||||
switch (type) {
|
||||
case LONGBOWMEN: return 1.4; // Excellent in castles
|
||||
case CAVALRY: return 0.7; // Vulnerable in castles
|
||||
case HEAVY_INFANTRY: return 1.2; // Good defenders
|
||||
default: return 1.0;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Cavalry properly devalued when attacking fortified positions
|
||||
- Longbowmen bonus for castle and hill positions
|
||||
- Combined arms tactics encouraged
|
||||
- Situational unit effectiveness captured
|
||||
|
||||
#### 1.2 Intelligent Spell Scoring
|
||||
|
||||
**Objective**: Replace static spell constants with dynamic evaluation
|
||||
|
||||
**Implementation**:
|
||||
```cpp
|
||||
class SpellEvaluator {
|
||||
public:
|
||||
double EvaluateLightning(const GameState& state, Coords target) {
|
||||
// Base damage potential
|
||||
double value = CountTargetableEnemies(state, target) * kLightningDamagePerUnit;
|
||||
|
||||
// Bonus for hitting valuable targets
|
||||
value += EvaluateTargetValue(state, target);
|
||||
|
||||
// Opportunity cost (could we do something better?)
|
||||
value -= CalculateOpportunityCost(state);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
double EvaluateMeteor(const GameState& state, Coords target, int turnsToLand) {
|
||||
// Predict enemy positions when meteor lands
|
||||
auto predictedPositions = PredictEnemyPositions(state, turnsToLand);
|
||||
|
||||
// Direct damage value
|
||||
double directValue = CalculateMeteorDamage(predictedPositions, target);
|
||||
|
||||
// Area denial value
|
||||
double denialValue = CalculateAreaDenialValue(state, target, turnsToLand);
|
||||
|
||||
// Movement forcing value
|
||||
double forcingValue = CalculateMovementForcingValue(state, target);
|
||||
|
||||
return directValue + denialValue + forcingValue;
|
||||
}
|
||||
|
||||
double EvaluateAOESpell(const GameState& state, Coords center, int radius) {
|
||||
// Note: Meteor already has sophisticated cluster analysis in meteorDropRawValue()
|
||||
// This example shows how similar logic could be applied to other potential AOE spells
|
||||
auto targets = GetUnitsInRadius(state, center, radius);
|
||||
|
||||
// Cluster bonus - more valuable against grouped enemies
|
||||
double clusterBonus = std::min(2.0, targets.size() * 0.3);
|
||||
|
||||
double totalValue = 0;
|
||||
for (const auto& target : targets) {
|
||||
totalValue += GetUnitValue(target) * clusterBonus;
|
||||
}
|
||||
|
||||
return totalValue;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Lightning properly valued based on target selection
|
||||
- Meteor timing accounts for enemy movement patterns
|
||||
- Builds on existing sophisticated meteor cluster analysis
|
||||
- Area denial and positioning effects included for other spells
|
||||
|
||||
#### 1.3 Dynamic Victory Condition Weighting
|
||||
|
||||
**Objective**: Adjust priorities based on game state and time remaining
|
||||
|
||||
**Implementation**:
|
||||
```cpp
|
||||
class VictoryConditionEvaluator {
|
||||
public:
|
||||
struct GamePhase {
|
||||
enum Type { OPENING, MIDGAME, ENDGAME, DESPERATE };
|
||||
Type phase;
|
||||
int roundsRemaining;
|
||||
double urgencyFactor;
|
||||
};
|
||||
|
||||
double CalculateVictoryScore(const GameState& state, PlayerId player) {
|
||||
GamePhase phase = DetermineGamePhase(state);
|
||||
|
||||
double castleScore = EvaluateCastleControl(state, player) *
|
||||
GetCastleWeight(phase);
|
||||
double unitScore = EvaluateUnitAdvantage(state, player) *
|
||||
GetUnitWeight(phase);
|
||||
double positionScore = EvaluatePositionalAdvantage(state, player) *
|
||||
GetPositionalWeight(phase);
|
||||
|
||||
return castleScore + unitScore + positionScore;
|
||||
}
|
||||
|
||||
private:
|
||||
double GetCastleWeight(const GamePhase& phase) {
|
||||
switch (phase.phase) {
|
||||
case OPENING: return 0.3; // Positioning important
|
||||
case MIDGAME: return 0.6; // Balanced approach
|
||||
case ENDGAME: return 1.2; // Castles critical
|
||||
case DESPERATE: return 2.0; // Must secure castles
|
||||
}
|
||||
}
|
||||
|
||||
GamePhase DetermineGamePhase(const GameState& state) {
|
||||
int roundsRemaining = GetMaxRounds() - state.current_round();
|
||||
double urgency = 1.0 - (double)roundsRemaining / GetMaxRounds();
|
||||
|
||||
if (roundsRemaining > 20) return {GamePhase::OPENING, roundsRemaining, urgency};
|
||||
if (roundsRemaining > 10) return {GamePhase::MIDGAME, roundsRemaining, urgency};
|
||||
if (roundsRemaining > 3) return {GamePhase::ENDGAME, roundsRemaining, urgency};
|
||||
return {GamePhase::DESPERATE, roundsRemaining, urgency};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Castle control prioritized more heavily as time runs out
|
||||
- Opening game focuses on positioning
|
||||
- Endgame desperation properly modeled
|
||||
|
||||
### Phase 2: Strategic Depth Improvements
|
||||
|
||||
#### 2.1 Multi-Turn Strategic Planning
|
||||
|
||||
**Objective**: Add strategic planning layer above tactical command evaluation
|
||||
|
||||
**Implementation**:
|
||||
```cpp
|
||||
class StrategicPlanner {
|
||||
public:
|
||||
enum StrategicGoal {
|
||||
SECURE_CASTLES,
|
||||
ELIMINATE_ENEMIES,
|
||||
CONTROL_CHOKEPOINTS,
|
||||
PROTECT_VIPS,
|
||||
SETUP_COMBOS
|
||||
};
|
||||
|
||||
struct StrategicPlan {
|
||||
StrategicGoal primaryGoal;
|
||||
StrategicGoal secondaryGoal;
|
||||
std::vector<TacticalObjective> objectives;
|
||||
int turnsToExecute;
|
||||
double expectedValue;
|
||||
};
|
||||
|
||||
StrategicPlan CreatePlan(const GameState& state, PlayerId player, int horizon) {
|
||||
auto goals = PrioritizeGoals(state, player);
|
||||
auto plan = GeneratePlan(state, goals, horizon);
|
||||
|
||||
// Evaluate plan using lookahead
|
||||
plan.expectedValue = EvaluatePlanOutcome(state, plan);
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
void AdaptPlan(StrategicPlan& plan, const GameState& newState,
|
||||
const Command& opponentMove) {
|
||||
// Assess if opponent action invalidates current plan
|
||||
if (PlanStillViable(plan, newState, opponentMove)) {
|
||||
// Minor adjustments
|
||||
AdjustTactics(plan, newState);
|
||||
} else {
|
||||
// Major replanning needed
|
||||
plan = CreatePlan(newState, plan.player, plan.turnsToExecute - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<StrategicGoal> PrioritizeGoals(const GameState& state, PlayerId player) {
|
||||
// Analyze current position and determine goal priorities
|
||||
auto analysis = AnalyzePosition(state, player);
|
||||
|
||||
std::vector<StrategicGoal> goals;
|
||||
if (analysis.isWinning) {
|
||||
goals.push_back(SECURE_CASTLES);
|
||||
goals.push_back(PROTECT_VIPS);
|
||||
} else if (analysis.isLosing) {
|
||||
goals.push_back(ELIMINATE_ENEMIES);
|
||||
goals.push_back(CONTROL_CHOKEPOINTS);
|
||||
} else {
|
||||
// Balanced approach
|
||||
goals.push_back(SECURE_CASTLES);
|
||||
goals.push_back(ELIMINATE_ENEMIES);
|
||||
}
|
||||
|
||||
return goals;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Coherent multi-turn strategies
|
||||
- Adaptive planning based on opponent actions
|
||||
- Goal-oriented tactical decisions
|
||||
|
||||
#### 2.2 Positional Intelligence System
|
||||
|
||||
**Objective**: Add sophisticated positional evaluation
|
||||
|
||||
**Implementation**:
|
||||
```cpp
|
||||
class PositionalEvaluator {
|
||||
public:
|
||||
struct InfluenceMap {
|
||||
std::vector<std::vector<double>> controlValues;
|
||||
std::vector<std::vector<double>> threatValues;
|
||||
std::vector<std::vector<double>> mobilityValues;
|
||||
};
|
||||
|
||||
InfluenceMap CalculateInfluenceMap(const GameState& state, PlayerId player) {
|
||||
InfluenceMap map(state.hex_map().width(), state.hex_map().height());
|
||||
|
||||
// Calculate control influence for each unit
|
||||
for (const auto& unit : GetPlayerUnits(state, player)) {
|
||||
AddUnitInfluence(map, unit, GetUnitThreatRange(unit));
|
||||
}
|
||||
|
||||
// Add terrain modifiers
|
||||
ApplyTerrainModifiers(map, state.hex_map());
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
double EvaluatePosition(const GameState& state, PlayerId player) {
|
||||
auto influenceMap = CalculateInfluenceMap(state, player);
|
||||
|
||||
double controlScore = EvaluateBoardControl(influenceMap);
|
||||
double chokepointScore = EvaluateChokepointControl(state, influenceMap);
|
||||
double formationScore = EvaluateFormations(state, player);
|
||||
double mobilityScore = EvaluateMobility(state, player);
|
||||
|
||||
return controlScore + chokepointScore + formationScore + mobilityScore;
|
||||
}
|
||||
|
||||
private:
|
||||
double EvaluateChokepointControl(const GameState& state,
|
||||
const InfluenceMap& influence) {
|
||||
double score = 0;
|
||||
for (const auto& chokepoint : IdentifyChokepoints(state.hex_map())) {
|
||||
if (influence.controlValues[chokepoint.x][chokepoint.y] > 0.5) {
|
||||
score += kChokepointControlValue;
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
double EvaluateFormations(const GameState& state, PlayerId player) {
|
||||
double score = 0;
|
||||
auto units = GetPlayerUnits(state, player);
|
||||
|
||||
// Look for beneficial formations
|
||||
for (size_t i = 0; i < units.size(); ++i) {
|
||||
for (size_t j = i + 1; j < units.size(); ++j) {
|
||||
score += CalculateFormationBonus(units[i], units[j]);
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Board control properly evaluated
|
||||
- Chokepoint importance recognized
|
||||
- Formation bonuses encouraged
|
||||
- Terrain advantages captured
|
||||
|
||||
#### 2.3 Command Opportunity Cost Analysis
|
||||
|
||||
**Objective**: Evaluate what the AI gives up by choosing each command
|
||||
|
||||
**Implementation**:
|
||||
```cpp
|
||||
class OpportunityCostAnalyzer {
|
||||
public:
|
||||
struct CommandOpportunity {
|
||||
Command command;
|
||||
double directValue;
|
||||
double opportunityCost;
|
||||
double netValue;
|
||||
};
|
||||
|
||||
std::vector<CommandOpportunity> AnalyzeCommands(
|
||||
const GameState& state,
|
||||
const std::vector<Command>& commands,
|
||||
PlayerId player) {
|
||||
|
||||
std::vector<CommandOpportunity> opportunities;
|
||||
|
||||
for (const auto& command : commands) {
|
||||
CommandOpportunity opp;
|
||||
opp.command = command;
|
||||
opp.directValue = EvaluateDirectValue(state, command);
|
||||
opp.opportunityCost = CalculateOpportunityCost(state, command, commands);
|
||||
opp.netValue = opp.directValue - opp.opportunityCost;
|
||||
|
||||
opportunities.push_back(opp);
|
||||
}
|
||||
|
||||
return opportunities;
|
||||
}
|
||||
|
||||
private:
|
||||
double CalculateOpportunityCost(const GameState& state,
|
||||
const Command& chosenCommand,
|
||||
const std::vector<Command>& allCommands) {
|
||||
double maxAlternativeValue = 0;
|
||||
|
||||
for (const auto& alternative : allCommands) {
|
||||
if (alternative.unit_id() == chosenCommand.unit_id() &&
|
||||
alternative != chosenCommand) {
|
||||
double altValue = EvaluateDirectValue(state, alternative);
|
||||
maxAlternativeValue = std::max(maxAlternativeValue, altValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Also consider resource opportunity costs
|
||||
double resourceCost = CalculateResourceOpportunityCost(chosenCommand);
|
||||
|
||||
return maxAlternativeValue + resourceCost;
|
||||
}
|
||||
|
||||
double CalculateResourceOpportunityCost(const Command& command) {
|
||||
// High-cost actions have higher opportunity cost
|
||||
switch (command.command_type()) {
|
||||
case METEOR_START: return 50; // Locks mage for multiple turns
|
||||
case HOLY_WAVE: return 30; // High vigor cost
|
||||
case MELEE: return 10; // Risk of casualties
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Better resource management
|
||||
- Reduced wasteful actions
|
||||
- Improved action economy
|
||||
|
||||
### Phase 3: Advanced Intelligence
|
||||
|
||||
#### 3.1 Opponent Modeling System
|
||||
|
||||
**Objective**: Adapt strategy based on opponent behavior patterns
|
||||
|
||||
**Implementation**:
|
||||
```cpp
|
||||
class OpponentModel {
|
||||
public:
|
||||
enum PlayStyle {
|
||||
AGGRESSIVE,
|
||||
DEFENSIVE,
|
||||
OPPORTUNISTIC,
|
||||
UNPREDICTABLE
|
||||
};
|
||||
|
||||
struct OpponentProfile {
|
||||
PlayStyle style;
|
||||
double aggressionLevel;
|
||||
double riskTolerance;
|
||||
std::map<std::string, double> tacticFrequency;
|
||||
std::vector<Command> commonOpenings;
|
||||
};
|
||||
|
||||
void UpdateModel(const std::vector<Command>& opponentMoves,
|
||||
const GameState& resultingState) {
|
||||
// Analyze opponent decision patterns
|
||||
AnalyzeAggressionLevel(opponentMoves);
|
||||
AnalyzeRiskTolerance(opponentMoves, resultingState);
|
||||
UpdateTacticFrequency(opponentMoves);
|
||||
}
|
||||
|
||||
std::vector<Command> PredictOpponentMoves(const GameState& state) {
|
||||
auto profile = GetCurrentProfile();
|
||||
|
||||
// Weight potential moves by opponent's historical preferences
|
||||
auto possibleMoves = GetOpponentPossibleMoves(state);
|
||||
|
||||
std::vector<Command> predictions;
|
||||
for (const auto& move : possibleMoves) {
|
||||
double probability = CalculateMoveProbability(move, profile);
|
||||
if (probability > kPredictionThreshold) {
|
||||
predictions.push_back(move);
|
||||
}
|
||||
}
|
||||
|
||||
return predictions;
|
||||
}
|
||||
|
||||
void AdaptStrategy(StrategicPlan& plan, const OpponentProfile& profile) {
|
||||
switch (profile.style) {
|
||||
case AGGRESSIVE:
|
||||
// Prepare strong defenses, look for counter-attacks
|
||||
plan.primaryGoal = PROTECT_VIPS;
|
||||
plan.secondaryGoal = ELIMINATE_ENEMIES;
|
||||
break;
|
||||
case DEFENSIVE:
|
||||
// Apply pressure, force engagements
|
||||
plan.primaryGoal = CONTROL_CHOKEPOINTS;
|
||||
plan.secondaryGoal = SECURE_CASTLES;
|
||||
break;
|
||||
// ... other adaptations
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Adaptive strategy based on opponent type
|
||||
- Prediction of opponent moves
|
||||
- Counter-strategy development
|
||||
|
||||
#### 3.2 Machine Learning Integration Points
|
||||
|
||||
**Future Enhancement Areas**:
|
||||
|
||||
```cpp
|
||||
class MLEnhancedEvaluator {
|
||||
public:
|
||||
// Neural network for position evaluation
|
||||
double EvaluatePositionML(const GameState& state, PlayerId player) {
|
||||
auto features = ExtractFeatures(state, player);
|
||||
return neuralNetwork.Evaluate(features);
|
||||
}
|
||||
|
||||
// Reinforcement learning for strategy selection
|
||||
StrategicGoal SelectStrategyRL(const GameState& state,
|
||||
const OpponentProfile& opponent) {
|
||||
auto stateVector = EncodeGameState(state, opponent);
|
||||
return strategyNetwork.SelectAction(stateVector);
|
||||
}
|
||||
|
||||
// Opening book learned from successful games
|
||||
Command GetOpeningMove(const GameState& state) {
|
||||
auto position = HashPosition(state);
|
||||
if (openingBook.contains(position)) {
|
||||
return openingBook[position].bestMove;
|
||||
}
|
||||
return Command{}; // Fall back to regular evaluation
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Phase 1 (3-4 weeks): Foundation
|
||||
1. Implement ContextualUnitEvaluator
|
||||
2. Create SpellEvaluator system
|
||||
3. Add VictoryConditionEvaluator with game phase detection
|
||||
4. Integrate into existing AIScoreCalculator
|
||||
|
||||
### Phase 2 (6-8 weeks): Strategic Layer
|
||||
1. Build StrategicPlanner framework
|
||||
2. Implement PositionalEvaluator with influence maps
|
||||
3. Add OpportunityCostAnalyzer
|
||||
4. Create goal-oriented command selection
|
||||
|
||||
### Phase 3 (8-12 weeks): Advanced Features
|
||||
1. Develop OpponentModel system
|
||||
2. Add prediction and adaptation mechanisms
|
||||
3. Create ML integration points
|
||||
4. Implement learning systems
|
||||
|
||||
## Expected Impact
|
||||
|
||||
### Immediate (Phase 1):
|
||||
- **25-40% improvement** in tactical decision quality
|
||||
- Better spell usage and timing
|
||||
- More appropriate unit deployment
|
||||
- Adaptive endgame strategy
|
||||
|
||||
### Medium-term (Phase 2):
|
||||
- **50-75% improvement** in strategic coherence
|
||||
- Multi-turn planning execution
|
||||
- Superior positional play
|
||||
- Efficient resource management
|
||||
|
||||
### Long-term (Phase 3):
|
||||
- **AI competitive with strong human players**
|
||||
- Adaptive learning from experience
|
||||
- Opponent-specific strategies
|
||||
- Novel tactical discoveries
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
### Automated Testing:
|
||||
- Unit tests for each evaluator component
|
||||
- Integration tests with existing AI pipeline
|
||||
- Performance regression testing
|
||||
- Strategic scenario validation
|
||||
|
||||
### Human Testing:
|
||||
- A/B testing against current AI
|
||||
- Human expert evaluation sessions
|
||||
- Tournament play against various skill levels
|
||||
- Long-term learning validation
|
||||
|
||||
This comprehensive improvement plan would transform the Eagle0 AI from a competent but predictable opponent into a genuinely challenging strategic adversary that could provide engaging gameplay for both casual and expert players.
|
||||
@@ -1,213 +0,0 @@
|
||||
# Eagle0 AI Scoring System: Technical Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Eagle0 AI scoring system is a sophisticated game state evaluation framework designed for the Shardok tactical combat layer. It uses a combination of immediate and lookahead scoring, handles both deterministic and non-deterministic commands, and employs different strategies for attackers and defenders.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Main Entry Points
|
||||
|
||||
The `AIScoreCalculator` class provides four main entry points:
|
||||
|
||||
1. **`GuessedStateScore`** - Evaluates a game state based on the player's role (attacker/defender) and strategy
|
||||
2. **`BestCommandIndex`** - Finds the best command from available options using lookahead search
|
||||
3. **`CommandScore`** - Evaluates a specific command's score
|
||||
4. **`EvaluateCommand`** - Lower-level command evaluation returning both immediate and lookahead scores
|
||||
|
||||
### Scoring Pipeline Flow
|
||||
|
||||
```
|
||||
BestCommandIndex
|
||||
├── AICommandFilter::FilterCommands (reduce search space)
|
||||
├── For each filtered command:
|
||||
│ ├── Determine command type (deterministic/non-deterministic/has odds)
|
||||
│ ├── CalcOne (execute command with appropriate randomness)
|
||||
│ │ ├── Create inner engine copy
|
||||
│ │ ├── Execute command
|
||||
│ │ ├── GuessedStateScore (immediate evaluation)
|
||||
│ │ └── BasicLookaheadCalculator (recursive lookahead)
|
||||
│ └── Aggregate scores based on command type
|
||||
└── Select command with best lookahead score (tiebreak on immediate)
|
||||
```
|
||||
|
||||
## Core Scoring Components
|
||||
|
||||
### 1. State Evaluation (`GuessedStateScore`)
|
||||
|
||||
The state scorer delegates to strategy-specific evaluators:
|
||||
|
||||
**Attacker Strategies:**
|
||||
- `STRATEGY_ATTACK_CASTLES` - Prioritizes capturing castle positions
|
||||
- `STRATEGY_ATTACK_UNITS` - Focuses on eliminating defender units
|
||||
- `STRATEGY_HOLD_CASTLES` - Maintains control of captured castles
|
||||
- `STRATEGY_CROSS_RIVERS` - Special water crossing objectives
|
||||
- `STRATEGY_FLEE` - Escape-focused scoring
|
||||
|
||||
**Defender Strategies:**
|
||||
- `STRATEGY_HOLD_CASTLES` - Defend critical castle positions
|
||||
- `STRATEGY_SCATTER` - Spread units to avoid elimination
|
||||
- `STRATEGY_FLEE` - Escape-focused scoring
|
||||
|
||||
### 2. Unit Value Calculation (`AIUnitScoreCalculator`)
|
||||
|
||||
Unit scores are computed using multiple factors:
|
||||
|
||||
**Base Unit Value:**
|
||||
```cpp
|
||||
battalionValue = battalionTypeMultiplier * (0.5 + armament/100) *
|
||||
(0.5 + training/100) * (0.5 + morale/100) * battalion.size
|
||||
heroValue = max(0, kHeroExistenceBuf + statsValue + professionValue + vigorValue)
|
||||
contextFreeValue = battalionValue + heroValue
|
||||
```
|
||||
|
||||
**Battalion Type Multipliers:**
|
||||
- Light Infantry: 1.0
|
||||
- Heavy Infantry/Light Cavalry: 1.5
|
||||
- Heavy Cavalry: 2.0
|
||||
- Longbowmen: 1.25
|
||||
- Undead: 0.25
|
||||
|
||||
**Contextual Modifiers:**
|
||||
- Castle bonus: `1 + kCastleMultiplierBonus * (integrity + 25) / 100`
|
||||
- On fire penalty: 0.25x multiplier
|
||||
- Adjacent fire: 0.99x per adjacent fire
|
||||
- On ice penalty: Based on ice integrity
|
||||
- VIP in danger: -200 if VIP unit < 200 size and in enemy attack range
|
||||
|
||||
**Special Unit Considerations:**
|
||||
- Undead value decreases with distance from enemies: `value / (1 + minimumDistance)`
|
||||
- Defenders that attackers must kill (when not targeting castles): +200 existence bonus
|
||||
- Controlled undead this round: +50 bonus
|
||||
|
||||
### 3. Victory Condition Scoring (`AIVictoryConditionScoreCalculator`)
|
||||
|
||||
**Critical Tile Holdings:**
|
||||
- Attacker holding tile with claimable unit: 0 penalty
|
||||
- Castle on fire: -200 * distance debuff to extinguishing position
|
||||
- Unoccupied/held by unclaimable: -100 * distance debuff
|
||||
- Defender-held: Varies based on unit value and distance
|
||||
|
||||
**Last Player Standing:**
|
||||
- -200 per surviving enemy unit * distance debuff
|
||||
|
||||
### 4. Distance-Based Scoring
|
||||
|
||||
The system uses sophisticated distance calculations incorporating:
|
||||
- Action point distances (movement cost)
|
||||
- Brave water crossing capability
|
||||
- Attack location analysis (adjacent, archery, mage, engineer positions)
|
||||
|
||||
**Distance Debuff Formula:**
|
||||
```cpp
|
||||
distanceDebuff = kMaxProximityBuf / (1 + distance / kDistanceDebufRatio)
|
||||
where kMaxProximityBuf = 1.5, kDistanceDebufRatio = 8.0
|
||||
```
|
||||
|
||||
## Command Type Handling
|
||||
|
||||
### Deterministic Commands
|
||||
Commands with predictable outcomes (MOVE, CONTROL, END_TURN, etc.):
|
||||
- Evaluated once with average random value (0.5)
|
||||
- No repeated simulations needed
|
||||
|
||||
### Commands with Odds
|
||||
Commands with success/failure chances (SCOUT, FEAR, etc.):
|
||||
- Two evaluations: success case (high roll) and failure case (low roll)
|
||||
- Final score: `lerp(failureScore, successScore, successChance)`
|
||||
- Success roll: `1.0 - successChance/2`
|
||||
- Failure roll: `(1.0 - successChance)/2`
|
||||
|
||||
### Non-Deterministic Commands
|
||||
Commands with variable outcomes (MELEE, ARCHERY, etc.):
|
||||
- Multiple evaluations with different random seeds
|
||||
- Default: `maxRepeatCount` iterations (typically 3-5)
|
||||
- Random values evenly distributed: `i / (maxRepeatCount - 1)`
|
||||
- Final score: average of all evaluations
|
||||
|
||||
## Lookahead Search
|
||||
|
||||
The system uses recursive lookahead with:
|
||||
- Configurable depth (`remainingLookahead` parameter)
|
||||
- Asynchronous execution for parallelization
|
||||
- Early termination on END_TURN commands
|
||||
- Score propagation from future states
|
||||
|
||||
## Command Filtering
|
||||
|
||||
`AICommandFilter` reduces search space by eliminating obviously bad moves:
|
||||
|
||||
**Filtered Actions:**
|
||||
- Meteor start when >4 hexes from enemies AND castles (attackers only)
|
||||
- Fire spells not adjacent to enemies (attackers only)
|
||||
- Fortify when far from objectives (attackers)
|
||||
- Retreating/fleeing when winning
|
||||
- Moving away from all enemies when outnumbered
|
||||
- Abandoning last defender in critical castle
|
||||
|
||||
## Key Constants and Multipliers
|
||||
|
||||
### Unit Scoring
|
||||
- `UNITS_BASE_MULTIPLIER`: 0.05
|
||||
- `FLEE_UNIT_SCORE`: -10,000
|
||||
- `CAPTURED_UNIT_SCORE`: -10,000
|
||||
- `CAPTURED_VIP_SCORE`: -25,000
|
||||
- `kHeroExistenceBuf`: 50
|
||||
- `kProfessionValue`: 200
|
||||
|
||||
### Ranged Attack Values
|
||||
- `kArcheryPossibleValue`: 38
|
||||
- `kMeteorDirectTargetingEnemy`: 2 per soldier
|
||||
- `kMeteorSplashTargetingEnemy`: 1 per soldier
|
||||
- `kLightningPossibleValue`: 0.05 per soldier
|
||||
|
||||
### Victory Condition Values
|
||||
- `MAX_DEFENDER_HELD_VALUE`: -1,200
|
||||
- `UNHELD_VALUE`: 100
|
||||
- `ON_FIRE_VALUE`: 200
|
||||
- `SURVIVING_ENEMY_VALUE`: -200
|
||||
|
||||
## Score Aggregation
|
||||
|
||||
Final score calculation:
|
||||
```cpp
|
||||
score = UNITS_BASE_MULTIPLIER * roundsMultiplier * unitsTotal + victoryConditionTotal
|
||||
```
|
||||
|
||||
Where:
|
||||
- `roundsMultiplier = roundsRemaining / maxRounds`
|
||||
- `unitsTotal` = sum of all unit values (attacker positive, defender negative)
|
||||
- `victoryConditionTotal` = sum of victory condition scores
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
1. **Command Filtering**: Reduces search space by 30-70% on average
|
||||
2. **Parallel Lookahead**: Async execution of future state evaluations
|
||||
3. **Cached Distance Calculations**: ActionPointDistances and AttackLocations caching
|
||||
4. **Early Game/Late Game Differentiation**: Simplified calculations after round 18
|
||||
5. **Multithreading**: Controlled by `MULTITHREAD` compile flag
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Random Number Generation
|
||||
- Uses `SequenceRandomGenerator` for deterministic testing
|
||||
- Multiple random seeds for non-deterministic command evaluation
|
||||
- Carefully controlled randomness for consistent AI behavior
|
||||
|
||||
### Distance Calculations
|
||||
- **Action Point Distances**: Accounts for movement costs, terrain, water crossing
|
||||
- **Attack Locations**: Pre-computed valid attack positions for units
|
||||
- **Caching**: Expensive distance calculations are cached and reused
|
||||
|
||||
### Strategy Selection
|
||||
- Attackers use `AIAttackerStrategySelector` to choose appropriate strategy
|
||||
- Defenders use `AIDefenderStrategySelector` based on game state
|
||||
- Strategy affects unit valuations and objective prioritization
|
||||
|
||||
### Score Interpretation
|
||||
- **Positive scores**: Favor the evaluating player
|
||||
- **Negative scores**: Favor the opponent
|
||||
- **Magnitude**: Indicates confidence/importance of the evaluation
|
||||
- **Relative scoring**: Only score differences matter, not absolute values
|
||||
|
||||
This scoring system provides a robust framework for tactical AI decision-making, balancing immediate tactical gains with strategic objectives while handling the uncertainty inherent in combat outcomes.
|
||||
@@ -1,196 +0,0 @@
|
||||
# Plan: Implement Thread-Local Caching in APDCache
|
||||
|
||||
## Overview
|
||||
Move the thread-local caching optimization from scattered locations into the `ActionPointDistancesCache` class itself, using the existing `FullCacheKey` infrastructure. This will provide automatic performance benefits to all 12+ call sites throughout the AI system.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Enhance APDCache with Thread-Local Caching
|
||||
|
||||
#### 1.1 Modify ActionPointDistancesCache.hpp
|
||||
```cpp
|
||||
class ActionPointDistancesCache {
|
||||
private:
|
||||
// Existing shared cache infrastructure...
|
||||
|
||||
// Thread-local cache using existing FullCacheKey infrastructure
|
||||
using TLSCache = std::unordered_map<FullCacheKey, shared_ptr<ActionPointDistances>, FullCacheKeyHash>;
|
||||
static thread_local TLSCache tlsCache;
|
||||
|
||||
// Helper to build cache key
|
||||
static FullCacheKey MakeCacheKey(
|
||||
const MapId& mapId,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost);
|
||||
|
||||
public:
|
||||
// Enhanced Get method with thread-local caching
|
||||
auto Get(
|
||||
const HexMap* map,
|
||||
const MapId& mapId,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost = -1) -> shared_ptr<ActionPointDistances>;
|
||||
|
||||
// Optional: Cache management methods
|
||||
static void ClearThreadLocalCache();
|
||||
static size_t GetThreadLocalCacheSize();
|
||||
};
|
||||
```
|
||||
|
||||
#### 1.2 Modify ActionPointDistancesCache.cpp
|
||||
```cpp
|
||||
// Thread-local cache definition
|
||||
thread_local ActionPointDistancesCache::TLSCache ActionPointDistancesCache::tlsCache;
|
||||
|
||||
auto ActionPointDistancesCache::MakeCacheKey(
|
||||
const MapId& mapId,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost) -> FullCacheKey {
|
||||
return FullCacheKey{
|
||||
mapId,
|
||||
static_cast<int>(battalionType->typeId),
|
||||
includeBravingWater,
|
||||
braveWaterActionPointCost >= 0 ? braveWaterActionPointCost : 0
|
||||
};
|
||||
}
|
||||
|
||||
auto ActionPointDistancesCache::Get(
|
||||
const HexMap* map,
|
||||
const MapId& mapId,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost) -> shared_ptr<ActionPointDistances> {
|
||||
|
||||
// Create cache key
|
||||
auto cacheKey = MakeCacheKey(mapId, battalionType, includeBravingWater, braveWaterActionPointCost);
|
||||
|
||||
// Check thread-local cache first
|
||||
auto it = tlsCache.find(cacheKey);
|
||||
if (it != tlsCache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Fall back to shared cache (existing implementation)
|
||||
auto result = GetFromSharedCache(map, mapId, battalionType, includeBravingWater, braveWaterActionPointCost);
|
||||
|
||||
// Cache in thread-local cache
|
||||
tlsCache[cacheKey] = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ActionPointDistancesCache::ClearThreadLocalCache() {
|
||||
tlsCache.clear();
|
||||
}
|
||||
|
||||
size_t ActionPointDistancesCache::GetThreadLocalCacheSize() {
|
||||
return tlsCache.size();
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Remove Redundant Caching
|
||||
|
||||
#### 2.1 Remove PreCachedAPDs from AIScoreCalculator.cpp
|
||||
- Delete the entire `PreCachedAPDs` struct (lines ~79-150)
|
||||
- Change `AttackerUnitsScore()` back to direct `apdCache->Get()` calls
|
||||
- Remove thread-local variable and UpdateIfNeeded call
|
||||
- Update callers to use `apdCache->Get()` directly instead of `cachedAPDs.GetRegular/GetBraving()`
|
||||
|
||||
#### 2.2 Simplify AIAttackGroups.cpp
|
||||
- Remove the `apdByBattType` local caching map
|
||||
- Change the function-local caching loop back to direct `apdCache->Get()` calls per unit
|
||||
- The new APDCache thread-local caching will handle the optimization automatically
|
||||
|
||||
### Phase 3: Testing & Validation
|
||||
|
||||
#### 3.1 Performance Testing
|
||||
- Measure AI performance before/after the change
|
||||
- Verify thread-local cache hit rates using `GetThreadLocalCacheSize()`
|
||||
- Confirm that 10+ call sites get automatic optimization
|
||||
- Profile to ensure no regression in memory usage
|
||||
|
||||
#### 3.2 Functional Testing
|
||||
- Run all AI tests: `bazel test //src/test/cpp/net/eagle0/shardok/ai/...`
|
||||
- Test multi-threaded scenarios to ensure thread safety
|
||||
- Verify cache isolation between threads
|
||||
|
||||
#### 3.3 Memory Management Testing
|
||||
- Monitor thread-local cache growth over time
|
||||
- Test cache clearing functionality
|
||||
- Consider automatic cache size limits if needed
|
||||
|
||||
### Phase 4: Documentation & Cleanup
|
||||
|
||||
#### 4.1 Update Documentation
|
||||
- Update `AI_PERFORMANCE_FIX_PRECACHED_APDS.md` to reflect architectural change
|
||||
- Document the new APDCache caching behavior
|
||||
- Add performance benchmarks
|
||||
|
||||
#### 4.2 Code Cleanup
|
||||
- Remove old performance fix documentation if no longer relevant
|
||||
- Clean up any remaining direct APDCache optimization attempts
|
||||
|
||||
## Expected Benefits
|
||||
|
||||
### Performance
|
||||
- **Automatic optimization for 12+ call sites** throughout AI system
|
||||
- **Zero code changes required** for existing APDCache::Get() callers
|
||||
- **Thread-safe** with per-thread cache isolation
|
||||
- **Consistent caching behavior** across entire codebase
|
||||
|
||||
### Architecture
|
||||
- **Single responsibility**: APDCache handles its own optimization
|
||||
- **Eliminates code duplication**: No more scattered caching patterns
|
||||
- **Uses existing infrastructure**: Leverages FullCacheKey design
|
||||
- **Clean abstraction**: Consumers just call Get(), caching is transparent
|
||||
|
||||
### Maintenance
|
||||
- **Centralized optimization**: One place to tune caching behavior
|
||||
- **Easier debugging**: All APD caching logic in one location
|
||||
- **Future-proof**: New APDCache callers automatically get optimization
|
||||
|
||||
## Implementation Risks & Mitigations
|
||||
|
||||
### Risk: Thread-Local Memory Growth
|
||||
- **Mitigation**: Add cache size monitoring and optional clearing API
|
||||
- **Monitoring**: Track cache sizes in performance tests
|
||||
|
||||
### Risk: Changed Shared Cache Access Patterns
|
||||
- **Mitigation**: Thorough testing of existing shared cache behavior
|
||||
- **Validation**: Ensure GetFromSharedCache still works correctly
|
||||
|
||||
### Risk: Performance Regression
|
||||
- **Mitigation**: Benchmark before/after implementation
|
||||
- **Rollback**: Keep optimization as optional flag initially
|
||||
|
||||
## Implementation Order
|
||||
1. **Phase 1**: Implement enhanced APDCache (non-breaking change)
|
||||
2. **Phase 3**: Test performance and validate behavior
|
||||
3. **Phase 2**: Remove redundant caching (breaking change for our code)
|
||||
4. **Phase 4**: Documentation and cleanup
|
||||
|
||||
This approach ensures we can validate the APDCache enhancement before removing existing optimizations.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Already Thread-Local Caching:
|
||||
1. **AIScoreCalculator.cpp** - Our recent `PreCachedAPDs` addition
|
||||
2. **FixedActionPointDistances.cpp** - Uses thread-local for file I/O buffering (not APDCache results)
|
||||
|
||||
### Function-Local Per-Battalion Caching:
|
||||
1. **AIAttackGroups.cpp** - Uses `apdByBattType` map for function-scoped caching
|
||||
|
||||
### No Caching (Direct APDCache::Get calls):
|
||||
- AIWaterCrossingCalculator.cpp
|
||||
- AICommandFilter.cpp
|
||||
- AIDistanceDebuf.cpp
|
||||
- AIVictoryConditionScoreCalculator.cpp
|
||||
- AIAttackerStrategySelector.cpp
|
||||
- AIDefenderStrategySelector.cpp
|
||||
- AIVictoryConditionScoreCalculator.cpp
|
||||
- And 5+ other files
|
||||
|
||||
**Impact**: This optimization will automatically benefit 10+ call sites that currently do repeated APDCache::Get calls with no caching optimization.
|
||||
@@ -6,7 +6,6 @@ cc_library(
|
||||
hdrs = ["AIAttackerStrategySelector.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
@@ -27,7 +26,6 @@ cc_library(
|
||||
hdrs = ["AIAttackGroups.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
@@ -62,7 +60,6 @@ cc_library(
|
||||
hdrs = ["AIDefenderStrategySelector.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
@@ -83,7 +80,6 @@ cc_library(
|
||||
hdrs = ["AIDistanceDebuf.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
@@ -116,7 +112,6 @@ cc_library(
|
||||
hdrs = ["AIScoreUtilities.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
@@ -126,43 +121,19 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_command_filter",
|
||||
srcs = ["AICommandFilter.cpp"],
|
||||
hdrs = ["AICommandFilter.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_score_calculator",
|
||||
srcs = ["AIScoreCalculator.cpp"],
|
||||
hdrs = ["AIScoreCalculator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":ai_attacker_strategy_selector",
|
||||
":ai_command_filter",
|
||||
":ai_time_budget",
|
||||
":ai_unit_score_calculator",
|
||||
":ai_victory_condition_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
|
||||
"//src/main/cpp/net/eagle0/common:thread_pool",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_guesser",
|
||||
],
|
||||
@@ -174,7 +145,6 @@ cc_library(
|
||||
hdrs = ["AIStrategy.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
@@ -188,7 +158,6 @@ cc_library(
|
||||
hdrs = ["AIUnitScoreCalculator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
@@ -204,7 +173,6 @@ cc_library(
|
||||
hdrs = ["AIVictoryConditionScoreCalculator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
@@ -252,46 +220,6 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_time_budget",
|
||||
srcs = ["AITimeBudget.cpp"],
|
||||
hdrs = ["AITimeBudget.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_cube_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_iterative_deepening",
|
||||
srcs = ["IterativeDeepeningAI.cpp"],
|
||||
hdrs = ["IterativeDeepeningAI.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":ai_attacker_strategy_selector",
|
||||
":ai_defender_strategy_selector",
|
||||
":ai_score_calculator",
|
||||
":ai_time_budget",
|
||||
":ai_water_crossing_command_chooser",
|
||||
"//src/main/cpp/net/eagle0/common:time_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "shardok_ai_client",
|
||||
srcs = ["ShardokAIClient.cpp"],
|
||||
@@ -301,9 +229,7 @@ cc_library(
|
||||
deps = [
|
||||
":ai_attacker_strategy_selector",
|
||||
":ai_defender_strategy_selector",
|
||||
":ai_iterative_deepening",
|
||||
":ai_score_calculator",
|
||||
":ai_time_budget",
|
||||
":ai_water_crossing_command_chooser",
|
||||
"//src/main/cpp/net/eagle0/common:time_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 07/04/25.
|
||||
//
|
||||
|
||||
#include "IterativeDeepeningAI.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <utility>
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/TimeUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
#define DEBUG_ITERATIVE_DEEPENING_TIMINGS 1
|
||||
|
||||
IterativeDeepeningAI::IterativeDeepeningAI(
|
||||
const PlayerId playerId,
|
||||
const bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache)
|
||||
: playerId(playerId),
|
||||
isDefender(isDefender),
|
||||
strategy(std::move(strategy)),
|
||||
castleCoords(castleCoords),
|
||||
apdCache(apdCache),
|
||||
alCache(alCache) {}
|
||||
|
||||
auto IterativeDeepeningAI::IterativeSearch(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
const std::vector<CommandProto>& commands,
|
||||
const AITimeBudget& initialBudget) const -> SearchResult {
|
||||
// Make a mutable copy of the time budget to track remaining time
|
||||
AITimeBudget timeBudget = initialBudget;
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
const auto initialBudgetMs = initialBudget.remainingBudget;
|
||||
SearchResult result;
|
||||
|
||||
if (commands.empty()) {
|
||||
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
|
||||
printf("ID AI: Commands are empty, returning early\n");
|
||||
#endif
|
||||
result.searchCompleted = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if we're in SET_UP phase
|
||||
bool isSetupPhase =
|
||||
(state->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP);
|
||||
int maxDepth = isSetupPhase ? 2 : std::numeric_limits<int>::max();
|
||||
|
||||
// Calculate current utility and create engine once for all command evaluations
|
||||
const auto& settingsGetter = settings->GetGetter();
|
||||
const auto guessedEngine = ShardokEngine(settings, state);
|
||||
const auto maxRepeatCount = settingsGetter.Backing().ai_utility_repeat_count();
|
||||
const ScoreValue currentUtility = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
state,
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
// Initialize data structures for tracking scores at each depth
|
||||
scoresByDepth.clear();
|
||||
scoresByDepth.resize(commands.size());
|
||||
highestDepthCompleted.clear();
|
||||
highestDepthCompleted.resize(commands.size(), 0);
|
||||
|
||||
int currentDepth = 1;
|
||||
size_t previousBestCommand = 0; // Track best command from previous depth
|
||||
size_t evaluatedCountAtHighestDepth = 0;
|
||||
EvaluationCompletionReason completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
|
||||
|
||||
// Main iterative deepening loop
|
||||
while ((currentDepth == 1 || !IsTimeExpired(timeBudget)) && currentDepth <= maxDepth) {
|
||||
// Get command indices sorted by best score from previous depth
|
||||
std::vector<size_t> sortedIndices = GetCommandsSortedByPreviousDepth(
|
||||
currentDepth,
|
||||
scoresByDepth,
|
||||
highestDepthCompleted);
|
||||
|
||||
int evaluatedCount = 0;
|
||||
bool allEvaluated = true;
|
||||
bool allEndTurnCommands = true; // Track if all commands are END_TURN
|
||||
|
||||
// Try to evaluate all commands at this depth, within budget constraints
|
||||
for (size_t cmdIndex : sortedIndices) {
|
||||
if (currentDepth > 1 && IsTimeExpired(timeBudget)) {
|
||||
allEvaluated = false;
|
||||
break;
|
||||
}
|
||||
|
||||
auto cmdResult = SearchCommandAtDepthWithEngine(
|
||||
guessedEngine,
|
||||
settingsGetter,
|
||||
maxRepeatCount,
|
||||
commands,
|
||||
cmdIndex,
|
||||
currentDepth,
|
||||
currentUtility,
|
||||
timeBudget);
|
||||
|
||||
// Ensure scoresByDepth[cmdIndex] has enough space
|
||||
if (scoresByDepth[cmdIndex].size() <= currentDepth) {
|
||||
scoresByDepth[cmdIndex].resize(currentDepth + 1);
|
||||
}
|
||||
scoresByDepth[cmdIndex][currentDepth] = cmdResult.bestScore;
|
||||
highestDepthCompleted[cmdIndex] = currentDepth;
|
||||
evaluatedCount++;
|
||||
|
||||
// Check if this command is not END_TURN_COMMAND
|
||||
if (commands[cmdIndex].type() != net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
allEndTurnCommands = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the best command at current depth and check if it changed
|
||||
if (evaluatedCount > 0) {
|
||||
evaluatedCountAtHighestDepth = evaluatedCount;
|
||||
size_t currentBestCommand = 0;
|
||||
ScoreValue currentBestScore = -std::numeric_limits<ScoreValue>::infinity();
|
||||
|
||||
for (size_t i = 0; i < commands.size(); ++i) {
|
||||
if (highestDepthCompleted[i] >= currentDepth) {
|
||||
if (scoresByDepth[i][currentDepth] > currentBestScore) {
|
||||
currentBestScore = scoresByDepth[i][currentDepth];
|
||||
currentBestCommand = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log if best command changed from previous depth
|
||||
if (currentDepth > 1 && currentBestCommand != previousBestCommand) {
|
||||
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
|
||||
printf("ID AI: Best command changed at depth %d:\n", currentDepth);
|
||||
printf(" Depth %d best: command %zu (score %.2f) - %s\n",
|
||||
currentDepth - 1,
|
||||
previousBestCommand,
|
||||
scoresByDepth[previousBestCommand][currentDepth - 1],
|
||||
commands[previousBestCommand].DebugString().c_str());
|
||||
printf(" Depth %d best: command %zu (score %.2f) - %s\n",
|
||||
currentDepth,
|
||||
currentBestCommand,
|
||||
currentBestScore,
|
||||
commands[currentBestCommand].DebugString().c_str());
|
||||
#endif
|
||||
}
|
||||
|
||||
previousBestCommand = currentBestCommand;
|
||||
}
|
||||
|
||||
// Only proceed to next depth if we completed all commands at current depth
|
||||
if (!allEvaluated) {
|
||||
completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
|
||||
break;
|
||||
}
|
||||
|
||||
// Stop if all evaluated commands were END_TURN at the root - no point going deeper
|
||||
if (allEndTurnCommands && evaluatedCount > 0) {
|
||||
completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
break;
|
||||
}
|
||||
|
||||
// Also check if scores haven't changed from previous depth
|
||||
// This indicates we've hit END_TURN in the lookahead
|
||||
if (currentDepth > 1 && evaluatedCount > 0) {
|
||||
bool scoresUnchanged = true;
|
||||
int unchangedCount = 0;
|
||||
|
||||
for (size_t i = 0; i < sortedIndices.size() && i < evaluatedCount; ++i) {
|
||||
size_t cmdIndex = sortedIndices[i];
|
||||
// This command was evaluated at both current and previous depth
|
||||
if (scoresByDepth[cmdIndex].size() > currentDepth &&
|
||||
scoresByDepth[cmdIndex].size() > currentDepth - 1) {
|
||||
// Check if score changed between depth N-1 and depth N
|
||||
if (std::abs(
|
||||
scoresByDepth[cmdIndex][currentDepth] -
|
||||
scoresByDepth[cmdIndex][currentDepth - 1]) < 1e-9) {
|
||||
unchangedCount++;
|
||||
} else {
|
||||
scoresUnchanged = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all evaluated commands had unchanged scores, we've hit END_TURN in lookahead
|
||||
if (scoresUnchanged && unchangedCount == evaluatedCount) {
|
||||
completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we've used more than 50% of total budget
|
||||
auto totalElapsed = std::chrono::steady_clock::now() - startTime;
|
||||
auto totalElapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(totalElapsed);
|
||||
double budgetUsedPercent = (double)totalElapsedMs.count() / initialBudgetMs.count();
|
||||
|
||||
if (budgetUsedPercent > 0.5) {
|
||||
printf("ID AI: Stopping after depth %d - used %.1f%% of time budget\n",
|
||||
currentDepth,
|
||||
budgetUsedPercent * 100);
|
||||
completionReason = EvaluationCompletionReason::NOT_ENOUGH_TIME_TO_CONTINUE;
|
||||
break;
|
||||
}
|
||||
|
||||
currentDepth++;
|
||||
}
|
||||
|
||||
// If we completed the loop without any breaks, we successfully exhausted meaningful search
|
||||
if (completionReason == EvaluationCompletionReason::RAN_OUT_OF_TIME &&
|
||||
currentDepth > maxDepth) {
|
||||
// We hit the depth limit rather than running out of time
|
||||
completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
}
|
||||
|
||||
// Select best result from highest depth achieved for each command
|
||||
result = SelectBestResult(scoresByDepth, highestDepthCompleted);
|
||||
result.minimumDepthCompleted = result.depthAchieved >= timeBudget.minDepthRequired;
|
||||
result.searchCompleted = result.minimumDepthCompleted;
|
||||
result.timeUsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - startTime);
|
||||
result.availableCommandCount = commands.size();
|
||||
result.commandCountEvaluated = evaluatedCountAtHighestDepth;
|
||||
result.completionReason = completionReason;
|
||||
|
||||
// Validation: if completion reason is RAN_OUT_OF_COMMANDS, evaluation should be 100%
|
||||
if (completionReason == EvaluationCompletionReason::RAN_OUT_OF_COMMANDS &&
|
||||
result.commandCountEvaluated < result.availableCommandCount) {
|
||||
printf("ERROR: Completion reason RAN_OUT_OF_COMMANDS but evaluation %lu/%zu < 100%%\n",
|
||||
result.commandCountEvaluated,
|
||||
result.availableCommandCount);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IterativeDeepeningAI::IsTimeExpired(const AITimeBudget& budget) {
|
||||
return budget.remainingBudget <= std::chrono::milliseconds(0);
|
||||
}
|
||||
|
||||
auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
|
||||
const ShardokEngine& guessedEngine,
|
||||
const GameSettings::Getter& settingsGetter,
|
||||
const int maxRepeatCount,
|
||||
const std::vector<CommandProto>& commands,
|
||||
const size_t commandIndex,
|
||||
const int depth,
|
||||
const ScoreValue currentUtility,
|
||||
AITimeBudget& timeBudget) const -> SearchResult {
|
||||
SearchResult result;
|
||||
result.bestCommandIndex = commandIndex;
|
||||
result.depthAchieved = depth;
|
||||
result.searchCompleted = true;
|
||||
result.minimumDepthCompleted = true;
|
||||
result.availableCommandCount = commands.size();
|
||||
result.commandCountEvaluated = 1; // We're evaluating just this command
|
||||
|
||||
if (commandIndex >= commands.size()) {
|
||||
result.bestScore = 0.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// Track concurrent evaluations and adjust time accounting
|
||||
AIEvaluationCounter counter;
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
// Use CommandScore to evaluate the specific command at the given depth
|
||||
const auto commandScore = AIScoreCalculator::CommandScore(
|
||||
playerId,
|
||||
isDefender,
|
||||
depth,
|
||||
maxRepeatCount,
|
||||
guessedEngine,
|
||||
strategy,
|
||||
currentUtility,
|
||||
settingsGetter,
|
||||
castleCoords,
|
||||
apdCache,
|
||||
alCache,
|
||||
commandIndex);
|
||||
|
||||
// Calculate time used and adjust based on concurrent evaluations
|
||||
const auto elapsed = std::chrono::steady_clock::now() - startTime;
|
||||
const int concurrentCount = counter.GetCurrentCount();
|
||||
const auto adjustedElapsed = elapsed / std::max(1, concurrentCount);
|
||||
const auto adjustedElapsedMs =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(adjustedElapsed);
|
||||
|
||||
// Deduct adjusted time from remaining budget
|
||||
timeBudget.remainingBudget -= adjustedElapsedMs;
|
||||
|
||||
result.bestScore = commandScore;
|
||||
} catch (const std::exception& e) {
|
||||
// If evaluation fails, return a neutral score rather than crashing
|
||||
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
|
||||
printf("SearchCommandAtDepthWithEngine: evaluation failed with exception: %s\n", e.what());
|
||||
#endif
|
||||
result.bestScore = 0.0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
|
||||
int currentDepth,
|
||||
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
|
||||
const std::vector<int>& highestDepthCompleted) const -> std::vector<size_t> {
|
||||
std::vector<size_t> indices(scoresByDepth.size());
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
|
||||
if (currentDepth == 1) {
|
||||
// For depth 1, return natural order
|
||||
return indices;
|
||||
}
|
||||
|
||||
// Sort by score at previous depth
|
||||
int prevDepth = currentDepth - 1;
|
||||
std::sort(indices.begin(), indices.end(), [&](size_t a, size_t b) {
|
||||
// Only consider commands that were evaluated at previous depth
|
||||
if (highestDepthCompleted[a] >= prevDepth && highestDepthCompleted[b] >= prevDepth) {
|
||||
return scoresByDepth[a][prevDepth] > scoresByDepth[b][prevDepth];
|
||||
}
|
||||
// Commands not evaluated at prev depth go to the end
|
||||
return highestDepthCompleted[a] >= prevDepth;
|
||||
});
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
auto IterativeDeepeningAI::SelectBestResult(
|
||||
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
|
||||
const std::vector<int>& highestDepthCompleted) const -> SearchResult {
|
||||
SearchResult result;
|
||||
result.bestScore = -std::numeric_limits<ScoreValue>::infinity();
|
||||
result.searchCompleted = false;
|
||||
|
||||
// Find the command with best score at its highest evaluated depth
|
||||
for (size_t i = 0; i < scoresByDepth.size(); ++i) {
|
||||
if (highestDepthCompleted[i] > 0) {
|
||||
int depth = highestDepthCompleted[i];
|
||||
ScoreValue score = scoresByDepth[i][depth];
|
||||
if (score > result.bestScore) {
|
||||
result.bestScore = score;
|
||||
result.bestCommandIndex = i;
|
||||
result.depthAchieved = depth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,109 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 07/04/25.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_ITERATIVEDEEPENINGAI_HPP
|
||||
#define EAGLE0_ITERATIVEDEEPENINGAI_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
#include "AIStrategy.hpp"
|
||||
#include "AITimeBudget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class ShardokEngine;
|
||||
using ScoreValue = double;
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
|
||||
/// Reason why AI evaluation completed at the achieved depth.
|
||||
enum class EvaluationCompletionReason {
|
||||
RAN_OUT_OF_COMMANDS, ///< All remaining commands were trivial (e.g., END_TURN)
|
||||
RAN_OUT_OF_TIME, ///< Time budget was exhausted with meaningful commands remaining
|
||||
NOT_ENOUGH_TIME_TO_CONTINUE ///< Insufficient time budget to start next depth iteration
|
||||
};
|
||||
|
||||
class IterativeDeepeningAI {
|
||||
public:
|
||||
struct SearchResult {
|
||||
size_t bestCommandIndex;
|
||||
ScoreValue bestScore;
|
||||
int depthAchieved;
|
||||
std::chrono::milliseconds timeUsed;
|
||||
bool minimumDepthCompleted;
|
||||
bool searchCompleted;
|
||||
size_t availableCommandCount;
|
||||
size_t commandCountEvaluated;
|
||||
EvaluationCompletionReason completionReason;
|
||||
|
||||
SearchResult()
|
||||
: bestCommandIndex(0),
|
||||
bestScore(0),
|
||||
depthAchieved(0),
|
||||
timeUsed(0),
|
||||
minimumDepthCompleted(false),
|
||||
searchCompleted(false),
|
||||
availableCommandCount(0),
|
||||
commandCountEvaluated(0),
|
||||
completionReason(EvaluationCompletionReason::RAN_OUT_OF_TIME) {}
|
||||
};
|
||||
|
||||
IterativeDeepeningAI(
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache);
|
||||
|
||||
[[nodiscard]] SearchResult IterativeSearch(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
const std::vector<CommandProto>& commands,
|
||||
const AITimeBudget& timeBudget) const;
|
||||
|
||||
private:
|
||||
PlayerId playerId;
|
||||
bool isDefender;
|
||||
AIStrategy strategy;
|
||||
CoordsSet castleCoords;
|
||||
const APDCache& apdCache;
|
||||
const ALCache& alCache;
|
||||
|
||||
// Reusable vectors to reduce memory allocations
|
||||
mutable std::vector<std::vector<ScoreValue>> scoresByDepth;
|
||||
mutable std::vector<int> highestDepthCompleted;
|
||||
mutable std::vector<size_t> reusableSortedIndices;
|
||||
|
||||
[[nodiscard]] static bool IsTimeExpired(const AITimeBudget& budget);
|
||||
|
||||
[[nodiscard]] SearchResult SearchCommandAtDepthWithEngine(
|
||||
const ShardokEngine& guessedEngine,
|
||||
const GameSettings::Getter& settingsGetter,
|
||||
int maxRepeatCount,
|
||||
const std::vector<CommandProto>& commands,
|
||||
size_t commandIndex,
|
||||
int depth,
|
||||
ScoreValue currentUtility,
|
||||
AITimeBudget& timeBudget) const;
|
||||
|
||||
[[nodiscard]] std::vector<size_t> GetCommandsSortedByPreviousDepth(
|
||||
int currentDepth,
|
||||
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
|
||||
const std::vector<int>& highestDepthCompleted) const;
|
||||
|
||||
[[nodiscard]] SearchResult SelectBestResult(
|
||||
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
|
||||
const std::vector<int>& highestDepthCompleted) const;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_ITERATIVEDEEPENINGAI_HPP
|
||||
@@ -12,20 +12,17 @@
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
#include "AITimeBudget.hpp"
|
||||
#include "IterativeDeepeningAI.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/TimeUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateGuesser.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/action_result_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
const static bool kDebugTimings = false;
|
||||
|
||||
using net::eagle0::shardok::api::ActionResultView;
|
||||
using net::eagle0::shardok::api::GameStateView;
|
||||
|
||||
static constexpr bool kPerformanceLogging = true;
|
||||
|
||||
void ApplyUpdate(GameStateView ¤tView, const ActionResultView &update) {}
|
||||
|
||||
auto RoundsRemaining(const GameSettingsSPtr &settings, const GameStateView &gsv) -> int {
|
||||
@@ -42,29 +39,7 @@ ShardokAIClient::ShardokAIClient(
|
||||
: playerId(playerId),
|
||||
isDefender(isDefender),
|
||||
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
|
||||
waterCrossingCommandChooser(playerId, apdCache) {
|
||||
// Pre-generate the most common cache entries for better performance
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
|
||||
|
||||
// Pre-fetch for all battalion types, both with and without brave water
|
||||
using BattalionTypeId = net::eagle0::shardok::storage::fb::BattalionTypeId;
|
||||
|
||||
for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
|
||||
typeId <= BattalionTypeId::BattalionTypeId_MAX;
|
||||
typeId++) {
|
||||
const auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
|
||||
const auto battalionType = settings.GetBattalionType(battalionTypeId);
|
||||
|
||||
// Pre-fetch without brave water (braveWaterActionPointCost = -1)
|
||||
apdCache->GetRaw(hexMap, mapId, battalionType, false, -1);
|
||||
|
||||
// Pre-fetch with brave water (includeBravingWater = true, braveWaterActionPointCost = 0)
|
||||
apdCache->GetRaw(hexMap, mapId, battalionType, true, 0);
|
||||
}
|
||||
|
||||
// Consolidate all the pre-fetched entries into the persistent cache
|
||||
apdCache->ConsolidateThreadLocalCache_Racy();
|
||||
}
|
||||
waterCrossingCommandChooser(playerId, apdCache) {}
|
||||
|
||||
void CheckCommand(const CommandProto &realDescriptor, const CommandProto &guessedDescriptor) {
|
||||
string diff;
|
||||
@@ -85,23 +60,16 @@ void CheckCommand(const CommandProto &realDescriptor, const CommandProto &guesse
|
||||
auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &guessedState,
|
||||
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
|
||||
const vector<CommandProto> &realAvailableCommands) const -> size_t {
|
||||
const auto settingsGetter = settings->GetGetter();
|
||||
const auto guessedEngine = ShardokEngine(settings, guessedState);
|
||||
|
||||
// Calculate time budget based on game situation using new settings
|
||||
const auto timeBudget = CalculateTimeBudget(playerId, settings, guessedState);
|
||||
const auto castleCoords = AllCastleCoords(guessedState->hex_map());
|
||||
const auto maxLookahead = settingsGetter.Backing().max_lookahead_turns();
|
||||
const auto maxRepeatCount = settingsGetter.Backing().ai_utility_repeat_count();
|
||||
|
||||
const auto guessedCommands = guessedEngine.GetAvailableCommandProtos(playerId, false);
|
||||
const auto commandCount = guessedCommands.size();
|
||||
|
||||
assert(commandCount == realAvailableCommands.size());
|
||||
for (int i = 0; i < commandCount; i++) {
|
||||
CheckCommand(realAvailableCommands[i], guessedCommands[i]);
|
||||
}
|
||||
|
||||
// Determine strategy once for consistent scoring throughout iterative deepening
|
||||
const auto castleCoords = AllCastleCoords(guessedState->hex_map());
|
||||
const AIStrategy strategy = isDefender ? AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
guessedState,
|
||||
castleCoords,
|
||||
@@ -117,107 +85,92 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
waterCrossingCommandChooser,
|
||||
realAvailableCommands);
|
||||
|
||||
// Use iterative deepening AI for Phase 2 implementation
|
||||
IterativeDeepeningAI
|
||||
iterativeAI(playerId, isDefender, strategy, castleCoords, apdCache, alCache);
|
||||
auto search_result =
|
||||
iterativeAI.IterativeSearch(settings, guessedState, realAvailableCommands, timeBudget);
|
||||
|
||||
CommandChoiceResults result{};
|
||||
result.chosenIndex = search_result.bestCommandIndex;
|
||||
result.availableCommandCount = search_result.availableCommandCount;
|
||||
result.depthAchieved = search_result.depthAchieved;
|
||||
result.commandCountEvaluated = search_result.commandCountEvaluated;
|
||||
result.completionReason = search_result.completionReason;
|
||||
|
||||
if constexpr (kPerformanceLogging) {
|
||||
if (result.commandCountEvaluated < result.availableCommandCount) {
|
||||
printf("ID AI: Depth %d - evaluated %lu/%zu commands\n",
|
||||
result.depthAchieved,
|
||||
result.commandCountEvaluated,
|
||||
result.availableCommandCount);
|
||||
}
|
||||
printf("ID AI: Search complete - achieved depth %d for best command %zu\n",
|
||||
result.depthAchieved,
|
||||
result.chosenIndex);
|
||||
|
||||
fflush(stdout);
|
||||
assert(commandCount == realAvailableCommands.size());
|
||||
for (int i = 0; i < commandCount; i++) {
|
||||
CheckCommand(realAvailableCommands[i], guessedCommands[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
const ScoreValue currentUtility = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
guessedState,
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
return AIScoreCalculator::BestCommandIndex(
|
||||
playerId,
|
||||
isDefender,
|
||||
maxLookahead,
|
||||
maxRepeatCount,
|
||||
guessedEngine,
|
||||
strategy,
|
||||
currentUtility,
|
||||
settingsGetter,
|
||||
castleCoords,
|
||||
apdCache,
|
||||
alCache)
|
||||
.index;
|
||||
}
|
||||
|
||||
auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &guessedState,
|
||||
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
|
||||
if (const auto dismissCommand = std::ranges::find_if(
|
||||
realAvailableCommands,
|
||||
const vector<CommandProto> &realAvailableCommands) const -> size_t {
|
||||
if (const auto dismissCommand = std::find_if(
|
||||
realAvailableCommands.begin(),
|
||||
realAvailableCommands.end(),
|
||||
[](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
|
||||
return cmd.type() == net::eagle0::shardok::common::DISMISS_UNIT_COMMAND;
|
||||
});
|
||||
dismissCommand == realAvailableCommands.end()) {
|
||||
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
} else {
|
||||
CommandChoiceResults results{};
|
||||
results.chosenIndex =
|
||||
static_cast<size_t>(std::distance(realAvailableCommands.begin(), dismissCommand));
|
||||
results.availableCommandCount = realAvailableCommands.size();
|
||||
results.depthAchieved = 1; // Simple heuristic choice
|
||||
results.commandCountEvaluated = 1; // Only evaluated one command type
|
||||
results.completionReason =
|
||||
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS; // Heuristic choice
|
||||
return results;
|
||||
return static_cast<size_t>(std::distance(realAvailableCommands.begin(), dismissCommand));
|
||||
}
|
||||
}
|
||||
|
||||
auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &guessedState,
|
||||
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
|
||||
if (const auto fleeCommand = std::ranges::find_if(
|
||||
realAvailableCommands,
|
||||
const vector<CommandProto> &realAvailableCommands) const -> size_t {
|
||||
if (const auto fleeCommand = std::find_if(
|
||||
realAvailableCommands.begin(),
|
||||
realAvailableCommands.end(),
|
||||
[](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
|
||||
return cmd.type() == net::eagle0::shardok::common::FLEE_COMMAND;
|
||||
});
|
||||
fleeCommand == realAvailableCommands.end()) {
|
||||
return LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
} else {
|
||||
CommandChoiceResults results{};
|
||||
results.chosenIndex =
|
||||
static_cast<size_t>(std::distance(realAvailableCommands.begin(), fleeCommand));
|
||||
results.availableCommandCount = realAvailableCommands.size();
|
||||
results.depthAchieved = 1; // Simple heuristic choice
|
||||
results.commandCountEvaluated = 1; // Only evaluated one command type
|
||||
results.completionReason =
|
||||
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS; // Heuristic choice
|
||||
return results;
|
||||
return static_cast<size_t>(std::distance(realAvailableCommands.begin(), fleeCommand));
|
||||
}
|
||||
}
|
||||
|
||||
auto ShardokAIClient::ChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateView &gsv,
|
||||
const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
|
||||
const vector<CommandProto> &realAvailableCommands) const -> size_t {
|
||||
static int typeChosenCount[net::eagle0::shardok::common::CommandType_MAX + 1];
|
||||
static int totalChoices = 0;
|
||||
|
||||
CommandChoiceResults results{};
|
||||
size_t chosenIndex;
|
||||
|
||||
const auto guessedState = GameStateGuesser::GuessedState(playerId, settings->GetGetter(), gsv);
|
||||
|
||||
if (const int roundsRemaining = RoundsRemaining(settings, gsv);
|
||||
!isDefender && roundsRemaining <= 1) {
|
||||
results =
|
||||
chosenIndex =
|
||||
FinalRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
} else if (!isDefender && roundsRemaining <= 3) {
|
||||
results =
|
||||
chosenIndex =
|
||||
LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
} else {
|
||||
results = StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
chosenIndex = StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
}
|
||||
|
||||
const auto chosenType = realAvailableCommands[results.chosenIndex].type();
|
||||
const auto chosenType = realAvailableCommands[chosenIndex].type();
|
||||
typeChosenCount[static_cast<int>(chosenType)]++;
|
||||
totalChoices++;
|
||||
|
||||
@@ -230,19 +183,20 @@ auto ShardokAIClient::ChooseCommandIndex(
|
||||
}
|
||||
}
|
||||
|
||||
std::ranges::sort(choices);
|
||||
std::ranges::reverse(choices);
|
||||
std::sort(choices.begin(), choices.end());
|
||||
std::reverse(choices.begin(), choices.end());
|
||||
for (const auto &[index, choice] : choices) {
|
||||
printf("%5d %s\n", index, CommandType_Name(choice).c_str());
|
||||
}
|
||||
printf("\n\n");
|
||||
}
|
||||
|
||||
return results;
|
||||
return chosenIndex;
|
||||
}
|
||||
|
||||
auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const
|
||||
-> CommandChoiceResults {
|
||||
auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const -> size_t {
|
||||
const auto startTimeMicros = CurrentTimeMicros();
|
||||
|
||||
if (const auto &availableCommands = engine.GetAvailableCommandProtos(playerId, false);
|
||||
availableCommands.empty()) {
|
||||
printf("no commands for player %d\n", playerId);
|
||||
@@ -252,9 +206,15 @@ auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const
|
||||
const auto &settings = engine.GetGameSettings();
|
||||
const auto &gsv = engine.GetGameStateView(GetPlayerId());
|
||||
|
||||
const auto results = ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
apdCache->ConsolidateThreadLocalCache_Racy();
|
||||
return results;
|
||||
const size_t chosenIndex = ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
const auto elapsedMicros = CurrentTimeMicros() - startTimeMicros;
|
||||
|
||||
if (kDebugTimings) {
|
||||
std::cerr << "Milliseconds to choose command index: " << elapsedMicros / 1000
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
return chosenIndex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,24 +13,13 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AITimeBudget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCommandChooser.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/IterativeDeepeningAI.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using VictoryCondition = net::eagle0::shardok::storage::fb::VictoryCondition;
|
||||
|
||||
/// Results from AI command selection, including performance metrics.
|
||||
struct CommandChoiceResults {
|
||||
size_t chosenIndex; ///< Index of the chosen command in the available commands list
|
||||
size_t availableCommandCount; ///< Total number of commands that were available to choose from
|
||||
int depthAchieved; ///< Maximum search depth reached for the best command
|
||||
size_t commandCountEvaluated; ///< Number of commands evaluated at the highest achieved depth
|
||||
EvaluationCompletionReason completionReason; ///< Why evaluation stopped at this depth
|
||||
};
|
||||
|
||||
//
|
||||
// A ShardokGameClient representing an AI player.
|
||||
//
|
||||
@@ -47,19 +36,19 @@ private:
|
||||
[[nodiscard]] auto StandardChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& guessedState,
|
||||
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
|
||||
const vector<CommandProto>& realAvailableCommands) const -> size_t;
|
||||
[[nodiscard]] auto LateRoundAttackerChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& guessedState,
|
||||
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
|
||||
const vector<CommandProto>& realAvailableCommands) const -> size_t;
|
||||
[[nodiscard]] auto FinalRoundAttackerChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& guessedState,
|
||||
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
|
||||
const vector<CommandProto>& realAvailableCommands) const -> size_t;
|
||||
[[nodiscard]] auto ChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const net::eagle0::shardok::api::GameStateView& gsv,
|
||||
const vector<CommandProto>& realAvailableCommands) const -> CommandChoiceResults;
|
||||
const vector<CommandProto>& realAvailableCommands) const -> size_t;
|
||||
|
||||
public:
|
||||
explicit ShardokAIClient(
|
||||
@@ -71,8 +60,7 @@ public:
|
||||
|
||||
[[nodiscard]] auto GetPlayerId() const -> PlayerId { return playerId; }
|
||||
|
||||
[[nodiscard]] auto ChooseCommandIndex(const ShardokEngine& engine) const
|
||||
-> CommandChoiceResults;
|
||||
[[nodiscard]] auto ChooseCommandIndex(const ShardokEngine& engine) const -> size_t;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
# True Iterative Deepening Implementation
|
||||
|
||||
## Current Status
|
||||
|
||||
### Phase 1: Core Implementation ✅ COMPLETED
|
||||
- ✅ Updated `IterativeDeepeningAI.hpp` with new data structures
|
||||
- ✅ Implemented new `IterativeSearch` function with generalized depth loop
|
||||
- ✅ Added `GetCommandsSortedByPreviousDepth` helper function
|
||||
- ✅ Added `SelectBestResult` helper function
|
||||
- ✅ Implemented 50% budget check to prevent incomplete deep searches
|
||||
- ✅ Added SET_UP phase detection and depth limiting
|
||||
- ✅ Ensured depth 1 always completes regardless of time budget
|
||||
- ✅ Added END_TURN detection to prevent excessive depth exploration
|
||||
- ✅ Implemented command change logging for debugging
|
||||
- ✅ All tests passing
|
||||
|
||||
### Phase 2: Code Cleanup 🚧 PLANNED
|
||||
|
||||
#### Proposed Cleanup Tasks
|
||||
|
||||
1. **Replace Heuristic END_TURN Detection**
|
||||
- Current: Uses score comparison heuristic to detect when lookahead hits END_TURN
|
||||
- Proposed: Modify `AIScoreCalculator` to return explicit `performedLookahead` flag
|
||||
- Benefits: More reliable, cleaner architecture, explicit intent
|
||||
|
||||
2. **Refactor Return Structures**
|
||||
- Add `bool performedLookahead` to `CommandEvaluationResult`
|
||||
- Update `BasicLookaheadCalculator` to track and return lookahead status
|
||||
- Thread this information through the scoring pipeline
|
||||
|
||||
3. **Architecture Improvements**
|
||||
- Consider extracting iterative deepening statistics into a separate class
|
||||
- Improve separation of concerns between search algorithm and scoring
|
||||
|
||||
4. **Performance Optimizations**
|
||||
- Profile memory allocations in deep searches
|
||||
- Consider pre-allocating vectors for very deep searches
|
||||
- Investigate parallel evaluation opportunities at each depth
|
||||
|
||||
### Key Implementation Details
|
||||
|
||||
1. **Data Structure Changes**:
|
||||
- Replaced `reusableDepth1Results` with `scoresByDepth` (2D vector)
|
||||
- Added `highestDepthCompleted` to track the maximum depth achieved per command
|
||||
|
||||
2. **Algorithm Flow**:
|
||||
- Starts at depth 1, evaluates ALL commands regardless of time budget
|
||||
- For each subsequent depth, evaluates commands ordered by previous depth scores
|
||||
- Continues until time expires, all commands at max depth are evaluated, or 50% budget is used
|
||||
- SET_UP phase limits max depth to 2
|
||||
- **Important**: Depth 1 always completes even if time budget is exhausted
|
||||
|
||||
3. **Memory Efficiency**:
|
||||
- Reuses data structures across searches to minimize allocations
|
||||
- Dynamically resizes score vectors as needed
|
||||
|
||||
4. **Command Change Logging**:
|
||||
- Tracks the best command at each depth
|
||||
- Logs when a new depth results in a different best command selection
|
||||
- Provides detailed debug output showing old and new commands with scores
|
||||
|
||||
## Overview
|
||||
|
||||
This document tracks the implementation of true iterative deepening for the Shardok AI, upgrading from a hard-coded 2-depth limit to dynamic depth exploration based on available time budget. The implementation is complete and functional, with planned cleanup tasks for future improvement.
|
||||
|
||||
## Current Implementation
|
||||
|
||||
The current implementation:
|
||||
- Evaluates ALL commands at depth 1
|
||||
- Sorts commands by depth-1 scores
|
||||
- Evaluates commands at depth 2 in sorted order until time expires
|
||||
- Never proceeds beyond depth 2
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
### Core Algorithm
|
||||
|
||||
The new algorithm will:
|
||||
1. **Depth 1**: Evaluate ALL commands (unchanged)
|
||||
2. **Depth 2+**: For each depth, attempt to evaluate all commands ordered by their scores from the previous depth
|
||||
3. **Completion check**: Only proceed to depth N+1 if all commands at depth N were evaluated
|
||||
4. **50% budget check**: Only proceed to depth N+1 if less than 50% of total time budget has been used
|
||||
5. **SET_UP phase limit**: Limit maximum depth to 2 during the SET_UP game phase
|
||||
|
||||
### Main Loop Pseudocode
|
||||
|
||||
```cpp
|
||||
int currentDepth = 1;
|
||||
bool isSetupPhase = (guessedState->status()->state() == GameStatus_::State_SET_UP);
|
||||
int maxDepth = isSetupPhase ? 2 : std::numeric_limits<int>::max();
|
||||
|
||||
// Track initial budget for percentage calculations
|
||||
const auto initialBudget = timeBudget.remainingBudget;
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
// Track scores at each depth for each command
|
||||
std::vector<std::vector<ScoreValue>> scoresByDepth(commands.size());
|
||||
std::vector<int> highestDepthCompleted(commands.size(), 0);
|
||||
|
||||
while (!IsTimeExpired(timeBudget) && currentDepth <= maxDepth) {
|
||||
auto depthStartTime = std::chrono::steady_clock::now();
|
||||
|
||||
// Get command indices sorted by best score from previous depth
|
||||
std::vector<size_t> sortedIndices = GetCommandsSortedByPreviousDepth(
|
||||
currentDepth, scoresByDepth, highestDepthCompleted);
|
||||
|
||||
int evaluatedCount = 0;
|
||||
bool allEvaluated = true;
|
||||
|
||||
// Try to evaluate all commands at this depth
|
||||
for (size_t cmdIndex : sortedIndices) {
|
||||
if (IsTimeExpired(timeBudget)) {
|
||||
allEvaluated = false;
|
||||
break;
|
||||
}
|
||||
|
||||
auto result = SearchCommandAtDepthWithEngine(
|
||||
guessedEngine, settingsGetter, maxRepeatCount,
|
||||
commands, cmdIndex, currentDepth, currentUtility, timeBudget);
|
||||
|
||||
scoresByDepth[cmdIndex][currentDepth] = result.bestScore;
|
||||
highestDepthCompleted[cmdIndex] = currentDepth;
|
||||
evaluatedCount++;
|
||||
}
|
||||
|
||||
printf("ID AI: Depth %d - evaluated %d/%zu commands\n",
|
||||
currentDepth, evaluatedCount, commands.size());
|
||||
|
||||
// Only proceed to next depth if we completed all commands at current depth
|
||||
if (!allEvaluated) {
|
||||
printf("ID AI: Stopping - time expired during depth %d\n", currentDepth);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we've used more than 50% of total budget
|
||||
auto totalElapsed = std::chrono::steady_clock::now() - startTime;
|
||||
auto totalElapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(totalElapsed);
|
||||
double budgetUsedPercent = (double)totalElapsedMs.count() / initialBudget.count();
|
||||
|
||||
if (budgetUsedPercent > 0.5) {
|
||||
printf("ID AI: Stopping after depth %d - used %.1f%% of time budget\n",
|
||||
currentDepth, budgetUsedPercent * 100);
|
||||
break;
|
||||
}
|
||||
|
||||
currentDepth++;
|
||||
}
|
||||
|
||||
// Select best result from highest depth achieved for each command
|
||||
SearchResult finalResult = SelectBestResult(scoresByDepth, highestDepthCompleted);
|
||||
```
|
||||
|
||||
### Key Helper Functions
|
||||
|
||||
#### GetCommandsSortedByPreviousDepth
|
||||
|
||||
Sort commands by their scores at the previous depth:
|
||||
|
||||
```cpp
|
||||
std::vector<size_t> GetCommandsSortedByPreviousDepth(
|
||||
int currentDepth,
|
||||
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
|
||||
const std::vector<int>& highestDepthCompleted) {
|
||||
|
||||
std::vector<size_t> indices(scoresByDepth.size());
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
|
||||
if (currentDepth == 1) {
|
||||
// For depth 1, return natural order
|
||||
return indices;
|
||||
}
|
||||
|
||||
// Sort by score at previous depth
|
||||
int prevDepth = currentDepth - 1;
|
||||
std::sort(indices.begin(), indices.end(),
|
||||
[&](size_t a, size_t b) {
|
||||
// Only consider commands that were evaluated at previous depth
|
||||
if (highestDepthCompleted[a] >= prevDepth &&
|
||||
highestDepthCompleted[b] >= prevDepth) {
|
||||
return scoresByDepth[a][prevDepth] > scoresByDepth[b][prevDepth];
|
||||
}
|
||||
// Commands not evaluated at prev depth go to the end
|
||||
return highestDepthCompleted[a] >= prevDepth;
|
||||
});
|
||||
|
||||
return indices;
|
||||
}
|
||||
```
|
||||
|
||||
#### SelectBestResult
|
||||
|
||||
Choose the best command considering the depth achieved:
|
||||
|
||||
```cpp
|
||||
SearchResult SelectBestResult(
|
||||
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
|
||||
const std::vector<int>& highestDepthCompleted) {
|
||||
|
||||
SearchResult result;
|
||||
result.bestScore = -std::numeric_limits<ScoreValue>::infinity();
|
||||
|
||||
// Find the command with best score at its highest evaluated depth
|
||||
for (size_t i = 0; i < scoresByDepth.size(); ++i) {
|
||||
if (highestDepthCompleted[i] > 0) {
|
||||
ScoreValue score = scoresByDepth[i][highestDepthCompleted[i]];
|
||||
if (score > result.bestScore) {
|
||||
result.bestScore = score;
|
||||
result.bestCommandIndex = i;
|
||||
result.depthAchieved = highestDepthCompleted[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### Data Structure Updates
|
||||
|
||||
Replace the current separate tracking with unified structures:
|
||||
|
||||
```cpp
|
||||
class IterativeDeepeningAI {
|
||||
// ... existing members ...
|
||||
|
||||
// New reusable storage to reduce allocations
|
||||
mutable std::vector<std::vector<ScoreValue>> scoresByDepth;
|
||||
mutable std::vector<int> highestDepthCompleted;
|
||||
mutable std::vector<size_t> reusableSortedIndices;
|
||||
};
|
||||
```
|
||||
|
||||
## Rationale for 50% Budget Check
|
||||
|
||||
The 50% time budget check is crucial because of the exponential nature of game tree search:
|
||||
- If depth N takes time T, depth N+1 typically takes B×T (where B is the branching factor)
|
||||
- If we've used >50% of budget at depth N, we likely can't complete even one command at depth N+1
|
||||
- Better to have complete results at depth N than incomplete results at depth N+1
|
||||
|
||||
Example with branching factor ~40:
|
||||
- Depth 1: 100ms (10% of 1000ms budget)
|
||||
- Depth 2: 400ms (total 50%)
|
||||
- Depth 3: Would take ~1600ms (total 210%) - don't attempt
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Adaptability**: Automatically adjusts search depth based on available time
|
||||
2. **Completeness**: Ensures all commands are evaluated at each attempted depth
|
||||
3. **Optimality**: Commands are always evaluated in order of promise from previous depth
|
||||
4. **Scalability**: Can search arbitrarily deep when time permits
|
||||
5. **Robustness**: 50% check prevents wasting time on incomplete deep searches
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Maintain backward compatibility with existing time budget calculations
|
||||
- Add comprehensive logging to track depth progression
|
||||
- Consider memory allocation optimizations for deep searches
|
||||
- Test thoroughly with various time budgets and game states
|
||||
|
||||
## Implementation Results
|
||||
|
||||
The true iterative deepening implementation has been successfully completed. The key changes include:
|
||||
|
||||
1. **Generalized Depth Loop**: The algorithm now supports arbitrary depths instead of being limited to depth 2
|
||||
2. **50% Budget Check**: Prevents starting a new depth if more than half the time budget is consumed
|
||||
3. **SET_UP Phase Handling**: Limits depth to 2 during game setup to avoid overthinking unit placement
|
||||
4. **Efficient Sorting**: Commands are evaluated at each depth in order of their scores from the previous depth
|
||||
5. **Memory Optimization**: Reuses data structures to minimize allocations during search
|
||||
|
||||
The implementation maintains backward compatibility while enabling deeper searches when time permits, leading to potentially better AI decisions in complex game situations.
|
||||
|
||||
### Critical Fixes Applied
|
||||
|
||||
#### 1. Depth 1 Always Completes
|
||||
We ensured that depth 1 ALWAYS completes regardless of time budget by:
|
||||
- Modifying the outer loop condition: `(currentDepth == 1 || !IsTimeExpired(timeBudget))`
|
||||
- Modifying the inner loop condition: `if (currentDepth > 1 && IsTimeExpired(timeBudget))`
|
||||
|
||||
This guarantees the AI always has at least a depth-1 evaluation for every command, preventing the AI from making no decision due to time constraints.
|
||||
|
||||
#### 2. END_TURN Detection
|
||||
Added logic to prevent excessive depth exploration when the game tree terminates:
|
||||
- **Root-level check**: If all commands at the current game state are END_TURN_COMMAND, stop after depth 1
|
||||
- **Lookahead termination check**: If scores don't change between depth N-1 and depth N for all commands, it indicates the lookahead hit END_TURN_COMMAND and stopped recursing
|
||||
|
||||
This prevents the AI from exploring to extreme depths (1000+) when there are no meaningful decisions to make, which can happen when there are very few commands available and the game tree quickly reaches states where only END_TURN_COMMAND is available.
|
||||
|
||||
#### 3. Command Change Logging
|
||||
Added comprehensive logging to track when deeper search changes the AI's decision:
|
||||
- After each depth, identifies the best command based on current evaluations
|
||||
- Compares with the best command from the previous depth
|
||||
- Logs detailed information when the best command changes, including:
|
||||
- Both commands' indices and scores
|
||||
- Full command debug strings for analysis
|
||||
|
||||
This helps understand when and why deeper search is beneficial, providing insights into the AI's decision-making process.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Shardok Performance Optimization Plan
|
||||
|
||||
## Current Status
|
||||
PostActionUnchecked reduced from 45.4% to 39.3% of total runtime after shared_ptr optimizations.
|
||||
|
||||
## ✅ Completed Optimizations
|
||||
|
||||
### 1. APDCache Thread-Local Caching
|
||||
- **Problem**: PreCachedAPDs constructor taking 18.5% of processing time
|
||||
- **Solution**: Moved thread-local caching into APDCache API using existing FullCacheKey infrastructure
|
||||
- **Implementation**: Hybrid API with both shared_ptr and raw pointer access, migrated 21+ call sites
|
||||
- **Result**: Successfully eliminated shared_ptr overhead in AI calculations
|
||||
|
||||
### 2. SharedPtr Reference Counting Fix
|
||||
- **Problem**: Atomic reference counting overhead in ShardokAction::Execute (28.4% of total runtime)
|
||||
- **Solution**: Changed RandomGenerator parameter from `std::shared_ptr<RandomGenerator>` to `const std::shared_ptr<RandomGenerator>&`
|
||||
- **Implementation**: Updated 49+ override sites across all command and action classes
|
||||
- **Result**: Reduced PostActionUnchecked from 45.4% to 39.3% of runtime
|
||||
|
||||
## ❌ Failed Attempts
|
||||
|
||||
### 1. ToByteString() Caching
|
||||
- **Problem**: Suspected expensive game state serialization calls
|
||||
- **Solution**: Added hash-based caching to avoid repeated ToByteString() calls
|
||||
- **Result**: No measurable performance improvement (discarded)
|
||||
|
||||
## 📋 Next Steps (Priority Order)
|
||||
|
||||
### 1. Optimize Occupant() with Array-based Indexing (HIGH PRIORITY)
|
||||
- **Problem**: `Occupant()` function iterates through ALL units (O(n)) to find unit at specific coordinates
|
||||
- **Solution**: Replace with O(1) array lookup indexed by `row * columnCount + column`
|
||||
- **Implementation**:
|
||||
- Simple array storing UnitId (or INVALID_UNIT_ID) at each map position
|
||||
- Update index when units move/spawn/die
|
||||
- Use in GameStateW wrapper with lazy initialization
|
||||
- **Rationale**: Clear algorithmic improvement, frequently called function
|
||||
- **Expected Impact**: Unknown but potentially significant
|
||||
|
||||
### 2. Profile Next Bottleneck (HIGH PRIORITY)
|
||||
- **Goal**: After Occupant() optimization, re-profile to identify next hotspot
|
||||
- **Focus**: PostActionUnchecked still 39.3% - drill deeper into remaining time consumption
|
||||
- **Approach**: Look for unexpected bottlenecks like the shared_ptr reference counting we discovered
|
||||
- **Rationale**: Profiling has revealed surprising performance issues
|
||||
|
||||
### 3. Defer UpdateGameStatusAction (MEDIUM PRIORITY)
|
||||
- **Problem**: Victory conditions checked after every action
|
||||
- **Solution**: Batch victory condition checks to end of turn or specific triggers
|
||||
- **Expected Impact**: Reduce redundant computation overhead
|
||||
|
||||
### 4. Object Pooling (MEDIUM PRIORITY)
|
||||
- **Problem**: Frequent allocation/deallocation of ActionResult and other objects
|
||||
- **Solution**: Implement object pools for frequently created objects
|
||||
- **Focus**: ActionResult objects, other high-frequency allocations
|
||||
- **Expected Impact**: Reduce memory allocation overhead
|
||||
|
||||
### 5. Lazy Modifier Hash Calculation (LOW PRIORITY)
|
||||
- **Problem**: Hash calculations performed unnecessarily
|
||||
- **Solution**: Compute hashes only when needed, cache between modifications
|
||||
- **Expected Impact**: Minor optimization for specific scenarios
|
||||
|
||||
## Key Insights
|
||||
|
||||
1. **Profiling Reveals Surprises**: Both major optimizations (APDCache and shared_ptr) were discovered through profiling rather than intuition
|
||||
2. **Atomic Operations Are Expensive**: Shared_ptr reference counting showed up as significant assembly-level overhead
|
||||
3. **Algorithmic Improvements Matter**: O(n) → O(1) optimizations like the proposed Occupant() fix are worth pursuing
|
||||
4. **Measurement is Critical**: ToByteString() caching seemed logical but provided no benefit
|
||||
5. **Incremental Progress**: Each optimization reveals the next bottleneck, requiring continuous profiling
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Always profile before and after changes to measure actual impact
|
||||
- Be prepared to discard optimizations that don't provide measurable benefit
|
||||
- Focus on algorithmic improvements and unexpected bottlenecks revealed by profiling
|
||||
- Continue systematic analysis of PostActionUnchecked hotspots
|
||||
@@ -1,304 +0,0 @@
|
||||
# AIScoreCalculator ThreadPool Migration Plan
|
||||
|
||||
## Overview
|
||||
This document outlines the plan to migrate AIScoreCalculator from using std::async to using the new ThreadPool class with priority-based scheduling and deadline support.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### std::async Usage
|
||||
1. **CalcOne method (line 893)**: Uses `std::async(std::launch::async, lookaheadLambda)` to asynchronously calculate lookahead scores
|
||||
2. **BestCommandIndex method (line 1053)**: Uses `std::async(std::launch::deferred, ...)` for deferred calculation of weighted scores
|
||||
3. **Future resolution (lines 1086-1091)**: Waits on all futures using `.get()` in a loop
|
||||
|
||||
### Time Management
|
||||
- IterativeDeepeningAI manages time budgets via AITimeBudget structure
|
||||
- Time budget contains `remainingBudget` (std::chrono::milliseconds) that gets decremented
|
||||
- No explicit deadline passing to AIScoreCalculator currently
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### 1. ThreadPool Integration
|
||||
- Create a static thread pool instance with 32 threads in AIScoreCalculator
|
||||
- Thread pool will be shared across all AIScoreCalculator operations
|
||||
|
||||
### 2. Priority Assignment
|
||||
- Priority = current lookahead depth (passed via `remainingLookahead` parameter)
|
||||
- Higher depth = higher priority (deeper searches are more valuable)
|
||||
- This ensures shallow searches complete first, allowing iterative deepening to work effectively
|
||||
|
||||
### 3. Deadline Support
|
||||
- Calculate deadline based on remaining time budget from IterativeDeepeningAI
|
||||
- Pass deadline to thread pool using `enqueue_with_deadline` for time-critical tasks
|
||||
- Tasks that exceed deadline will be automatically skipped by the thread pool
|
||||
|
||||
### 4. Implementation Details
|
||||
|
||||
#### Changes to CalcOne:
|
||||
```cpp
|
||||
// OLD:
|
||||
returnValue.lookaheadScore = std::async(std::launch::async, lookaheadLambda);
|
||||
|
||||
// NEW:
|
||||
// Priority = remainingLookahead (higher depth = higher priority)
|
||||
// Deadline = current_time + remaining_budget_fraction
|
||||
returnValue.lookaheadScore = threadPool.enqueue_with_deadline(
|
||||
lookaheadLambda,
|
||||
remainingLookahead, // priority
|
||||
deadline // calculated from time budget
|
||||
);
|
||||
```
|
||||
|
||||
#### Changes to BestCommandIndex:
|
||||
```cpp
|
||||
// Deferred calculations stay as-is (no change needed)
|
||||
// They're already using std::launch::deferred which is appropriate
|
||||
|
||||
// The .get() loop (lines 1086-1091) can be moved to a lower priority task:
|
||||
auto futureResolutionTask = [&]() {
|
||||
for (uint32_t i = 0; i < commandCount; i++) {
|
||||
const auto count = static_cast<ScoreValue>(scoreFutures[i].size());
|
||||
ScoreValue total = 0.0;
|
||||
for (auto &oneFuture : scoreFutures[i]) {
|
||||
total += oneFuture.get();
|
||||
}
|
||||
allIndices[i].lookaheadScore = total / count;
|
||||
}
|
||||
};
|
||||
|
||||
// Enqueue with lower priority (0 or negative) to ensure all calculations complete first
|
||||
threadPool.enqueue(futureResolutionTask, 0);
|
||||
```
|
||||
|
||||
### 5. Thread Pool Lifecycle
|
||||
- Initialize as static member: `static inline ThreadPool threadPool{32};`
|
||||
- Destruction handled automatically by ThreadPool destructor
|
||||
- No explicit cleanup needed
|
||||
|
||||
### 6. Deadline Calculation
|
||||
- Need to pass time budget information down from IterativeDeepeningAI
|
||||
- Add optional `AITimeBudget*` parameter to CalcOne and BestCommandIndex
|
||||
- Calculate deadline as: `now() + (remainingBudget * depth_fraction)`
|
||||
- Deeper searches get proportionally less time
|
||||
|
||||
### 7. Benefits
|
||||
1. **Better CPU utilization**: 32 threads vs unbounded std::async
|
||||
2. **Priority scheduling**: Deeper searches get higher priority
|
||||
3. **Deadline enforcement**: Automatic timeout handling
|
||||
4. **Resource control**: Fixed thread pool prevents thread explosion
|
||||
5. **Performance**: Thread reuse avoids creation/destruction overhead
|
||||
|
||||
### 8. Testing Plan
|
||||
1. Verify thread pool initialization
|
||||
2. Test priority ordering (shallow searches complete first)
|
||||
3. Test deadline enforcement (expired tasks are skipped)
|
||||
4. Compare performance with existing implementation
|
||||
5. Stress test with multiple concurrent AI calculations
|
||||
|
||||
### 9. Rollback Plan
|
||||
- Keep MULTITHREAD macro to allow switching between implementations
|
||||
- Add THREAD_POOL macro to conditionally compile new implementation
|
||||
- Allows A/B testing and gradual migration
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### ✅ Completed
|
||||
1. **ThreadPool Integration** - Added static 32-thread pool to AIScoreCalculator
|
||||
2. **Priority Assignment** - Priority = current lookahead depth (higher depth = higher priority)
|
||||
3. **Deadline Support** - Calculate deadline based on remaining time budget from IterativeDeepeningAI
|
||||
4. **Method Signatures Updated** - Added `AITimeBudget* timeBudget` parameter to CalcOne and BestCommandIndex
|
||||
5. **std::async Replacement** - Replaced `std::async(std::launch::async)` with `threadPool.enqueue_with_deadline()`
|
||||
6. **Time Budget Integration** - IterativeDeepeningAI now passes timeBudget to AIScoreCalculator methods
|
||||
7. **Build System Updates** - Added thread_pool dependency to BUILD.bazel files
|
||||
8. **Build Verification** - Successfully builds with `bazel build //src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator`
|
||||
|
||||
### Implementation Details
|
||||
- **Priority Calculation**: `priority = remainingLookahead` (deeper searches get higher priority)
|
||||
- **Deadline Calculation**: `deadline = now + (remainingBudget / (remainingLookahead + 1))`
|
||||
- **Thread Pool**: Static 32-thread pool shared across all AIScoreCalculator operations
|
||||
- **Backward Compatibility**: MULTITHREAD macro preserved for rollback capability
|
||||
- **Deferred Tasks**: std::launch::deferred calls remain unchanged as planned
|
||||
|
||||
### 🔧 Fixed Issues
|
||||
|
||||
#### Build Issues
|
||||
- **SearchAtDepth method**: Fixed missing timeBudget parameter - now passes nullptr since this method doesn't have access to time budget
|
||||
- **AI Performance Runner**: Successfully builds and runs with `bazel build //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner`
|
||||
|
||||
#### Runtime Issues
|
||||
- **Hanging AI Performance Test**: ✅ FIXED WITH PROPER DEADLINE SUPPORT
|
||||
- **Root Cause**: Deadline expiration in ThreadPool was skipping task execution, leaving futures unresolved
|
||||
- **Symptom**: `./scripts/ai_perf_test.sh` would hang indefinitely when calling `oneFuture.get()` in BestCommandIndex
|
||||
- **Solution**: Implemented proper status code system with `TaskResult<T>` wrapper
|
||||
- Created `TaskStatus` enum (SUCCESS, DEADLINE_EXCEEDED, CANCELLED)
|
||||
- Updated ThreadPool to return `TaskResult<T>` instead of raw `T`
|
||||
- Tasks that exceed deadline return `TaskResult` with `DEADLINE_EXCEEDED` status
|
||||
- AIScoreCalculator checks status codes and handles expired tasks gracefully
|
||||
- **Additional Fix**: Lambda capture issue in `enqueue_with_deadline`
|
||||
- **Problem**: Complex lambda capture syntax `[f = std::forward<F>(f), args..., deadline]` was causing compilation/runtime issues
|
||||
- **Solution**: Used `std::bind` to create callable object: `auto actualTask = std::bind(std::forward<F>(f), std::forward<Args>(args)...);`
|
||||
- **Result**: Cleaner, more reliable task capture and execution
|
||||
- **Final Result**: Full deadline functionality restored, AI performance test runs correctly without hanging
|
||||
|
||||
### Implementation Details - TaskResult System
|
||||
```cpp
|
||||
// TaskResult wrapper with status code
|
||||
template<typename T>
|
||||
struct TaskResult {
|
||||
T value;
|
||||
TaskStatus status;
|
||||
|
||||
bool succeeded() const { return status == TaskStatus::SUCCESS; }
|
||||
bool deadlineExceeded() const { return status == TaskStatus::DEADLINE_EXCEEDED; }
|
||||
operator T() const { return value; } // Implicit conversion for compatibility
|
||||
};
|
||||
|
||||
// Usage in AIScoreCalculator
|
||||
for (auto &oneFuture : scoreFutures[i]) {
|
||||
auto result = oneFuture.get();
|
||||
if (result.succeeded()) {
|
||||
total += result.value;
|
||||
validResults++;
|
||||
} else if (result.deadlineExceeded()) {
|
||||
// Skip deadline-exceeded results, fall back to immediate score
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Status: ✅ COMPLETE AND FULLY FUNCTIONAL
|
||||
All ThreadPool migration work is complete with proper deadline support. The implementation:
|
||||
- ✅ Builds successfully
|
||||
- ✅ Runs correctly with full deadline functionality
|
||||
- ✅ Handles deadline expiration gracefully without hanging
|
||||
- ✅ AI performance test works perfectly
|
||||
- ✅ Fixed infinite loop issue caused by TaskResult implicit conversion
|
||||
|
||||
### 🔧 Final Issue Resolution - Infinite Loop Bug
|
||||
|
||||
#### Problem: TaskResult Implicit Conversion Causing Infinite Recursion
|
||||
- **Root Cause**: TaskResult<T> had an implicit conversion operator that was interfering with AI search control flow
|
||||
- **Symptom**: AI performance test hanging in infinite loop, processing same set of futures repeatedly
|
||||
- **Evidence**: Debug output showed endless cycle processing futures 0-45
|
||||
|
||||
#### Solution: Remove Implicit Conversion Operator
|
||||
- **Change**: Removed `operator T() const { return value; }` from TaskResult<T>
|
||||
- **Replacement**: Added explicit `.get()` method: `T get() const { return value; }`
|
||||
- **Code Updates**: Updated all AIScoreCalculator usage to explicitly call `.value` or handle TaskResult properly
|
||||
|
||||
#### Updated TaskResult Implementation
|
||||
```cpp
|
||||
template<typename T>
|
||||
struct TaskResult {
|
||||
T value;
|
||||
TaskStatus status;
|
||||
|
||||
// NO implicit conversion - this was causing infinite recursion
|
||||
T get() const { return value; }
|
||||
|
||||
bool succeeded() const { return status == TaskStatus::SUCCESS; }
|
||||
bool deadlineExceeded() const { return status == TaskStatus::DEADLINE_EXCEEDED; }
|
||||
};
|
||||
```
|
||||
|
||||
#### Final Result
|
||||
- **AI Performance Test**: ✅ Now runs successfully, no more hanging
|
||||
- **ThreadPool Usage**: ✅ Correctly using 32-thread pool with priority-based scheduling
|
||||
- **Deadline Support**: ✅ Full deadline functionality with proper status codes
|
||||
- **Performance**: ✅ AI achieves depth 2 searches consistently across turns
|
||||
|
||||
### 🔧 Critical Bug Fix - Promise Lifetime Issue
|
||||
|
||||
#### Problem: Stack-Allocated Promise Destruction
|
||||
The final and most critical bug was in the immediate path (no lookahead) promise handling:
|
||||
|
||||
**Broken Code:**
|
||||
```cpp
|
||||
if (remainingLookahead <= 0) {
|
||||
std::promise<TaskResult<ScoreValue>> p; // Stack allocated!
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
p.set_value(TaskResult<ScoreValue>(innerUtility));
|
||||
// BUG: 'p' destructor runs here, invalidating the future!
|
||||
}
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
- Most AI calls use `remainingLookahead=0` (immediate path)
|
||||
- Stack-allocated promise `p` was destroyed when leaving scope
|
||||
- This left associated futures in invalid/undefined state
|
||||
- `future.get()` calls on invalid futures hang indefinitely
|
||||
- ThreadPool tasks (priority 1,2) would queue up waiting for invalid futures
|
||||
|
||||
**Fixed Code:**
|
||||
```cpp
|
||||
if (remainingLookahead <= 0) {
|
||||
auto p = std::make_shared<std::promise<TaskResult<ScoreValue>>>(); // Heap allocated!
|
||||
returnValue.lookaheadScore = p->get_future();
|
||||
p->set_value(TaskResult<ScoreValue>(innerUtility));
|
||||
// Promise stays alive through shared_ptr reference counting
|
||||
}
|
||||
```
|
||||
|
||||
#### Impact of Fix
|
||||
- **Before**: AI hanging indefinitely on invalid futures, ThreadPool backing up with 271+ queued tasks
|
||||
- **After**: AI runs smoothly with proper ThreadPool execution, normal performance restored
|
||||
- **Key Insight**: The ThreadPool itself was working correctly - the hang was caused by invalid futures from destroyed promises
|
||||
|
||||
### 🔧 Additional Defensive Improvement - Future Timeout Protection
|
||||
|
||||
Added `wait_until()` timeout protection before all `future.get()` calls with budget-aware timeouts:
|
||||
|
||||
```cpp
|
||||
// Before: Direct .get() call could hang indefinitely
|
||||
auto result = future.get();
|
||||
|
||||
// After: Budget-aware timeout protection with fallback
|
||||
auto timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); // Default
|
||||
if (timeBudget && timeBudget->remainingBudget.count() > 0) {
|
||||
timeout = std::chrono::steady_clock::now() + timeBudget->remainingBudget + std::chrono::milliseconds(100);
|
||||
}
|
||||
if (future.wait_until(timeout) == std::future_status::timeout) {
|
||||
// Fall back to immediate score or skip result
|
||||
return immediateScore;
|
||||
}
|
||||
auto result = future.get();
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Prevents hanging**: Even if ThreadPool has issues, system won't freeze
|
||||
- **Budget-aware**: Uses actual time budget + 100ms buffer instead of arbitrary timeouts
|
||||
- **Graceful degradation**: Falls back to immediate scores when tasks timeout
|
||||
- **User experience driven**: Respects the original deadline constraints for responsiveness
|
||||
- **Multi-layered protection**: ThreadPool deadline + budget-aware future timeout
|
||||
- **Conservative fallback**: Uses 2-second timeout in contexts without time budget
|
||||
|
||||
### Status: ✅ FULLY WORKING THREADPOOL MIGRATION - COMPLETE
|
||||
ThreadPool migration is complete and production-ready:
|
||||
- ✅ 32-thread pool with priority-based scheduling (higher depth = higher priority)
|
||||
- ✅ Deadline support with graceful timeout handling
|
||||
- ✅ TaskResult wrapper with explicit status checking (no implicit conversion)
|
||||
- ✅ Proper promise/future lifetime management
|
||||
- ✅ Defensive timeout protection on all future.get() calls
|
||||
- ✅ AI performance test completes successfully without hanging
|
||||
- ✅ **PERFORMANCE RESTORED**: timeBudget properly propagated through entire call chain
|
||||
- ✅ Multi-layered robustness against threading issues
|
||||
|
||||
### 🎯 Final Performance Fix - timeBudget Parameter Chain
|
||||
**Issue Resolved**: The main execution path (CommandScore → EvaluateCommand → CalcOne) was not using the time budget, causing 100ms default timeouts and severe performance degradation.
|
||||
|
||||
**Root Cause**: Missing timeBudget parameter in key call sites:
|
||||
- IterativeDeepeningAI → CommandScore (missing timeBudget parameter)
|
||||
- EvaluateCommand → CalcOne (passing nullptr instead of timeBudget)
|
||||
- BasicLookaheadCalculator → BestCommandIndex (missing timeBudget parameter)
|
||||
|
||||
**Solution Implemented**:
|
||||
1. **Updated method signatures**: Added `const AITimeBudget *timeBudget = nullptr` to BasicLookaheadCalculator
|
||||
2. **Fixed all CalcOne calls in EvaluateCommand**: Changed from `nullptr` to `timeBudget` (3 call sites)
|
||||
3. **Updated lambda capture in CalcOne**: Added timeBudget to capture list and pass to BasicLookaheadCalculator
|
||||
4. **Fixed IterativeDeepeningAI**: Added `&timeBudget` parameter to CommandScore call
|
||||
5. **Verified build and performance**: AI now properly uses time budget, reaches depth 2, shows correct budget values
|
||||
|
||||
**Performance Test Results**:
|
||||
- **Before**: Severe performance degradation, limited depth achievement
|
||||
- **After**: Normal performance restored, proper depth 2 searches, budget-aware timeouts working
|
||||
- **Evidence**: Logs show correct budget usage: `budget: 1500ms`, `budget: 1068ms`, `achieved depth 2`
|
||||
|
||||
The ThreadPool migration is now **FULLY COMPLETE** with all performance issues resolved.
|
||||
@@ -1,306 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 2025-01-15.
|
||||
//
|
||||
|
||||
#include "AIPerformanceRunner.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "PerformanceTestGameStateBuilder.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
|
||||
using namespace shardok;
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Convert completion reason to human-readable string.
|
||||
*/
|
||||
auto CompletionReasonToString(EvaluationCompletionReason reason) -> std::string {
|
||||
switch (reason) {
|
||||
case EvaluationCompletionReason::RAN_OUT_OF_COMMANDS:
|
||||
return "completed all meaningful commands";
|
||||
case EvaluationCompletionReason::RAN_OUT_OF_TIME: return "time budget exhausted";
|
||||
case EvaluationCompletionReason::NOT_ENOUGH_TIME_TO_CONTINUE:
|
||||
return "insufficient time for next depth";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments into a configuration struct.
|
||||
*/
|
||||
auto ParseCommandLineArgs(int argc, char* argv[]) -> PerformanceTestConfig {
|
||||
PerformanceTestConfig config;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg(argv[i]);
|
||||
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
std::cout << "Shardok AI Performance Runner\n"
|
||||
<< "Usage: " << argv[0] << " [options]\n"
|
||||
<< "\n"
|
||||
<< "Options:\n"
|
||||
<< " --map=NAME Map name (default: Alah)\n"
|
||||
<< " --turns=N Number of turns to test (default: 5)\n"
|
||||
<< " --defender=BOOL AI is defender (default: false)\n"
|
||||
<< " --verbose Enable verbose output\n"
|
||||
<< " --help, -h Show this help message\n";
|
||||
std::exit(0);
|
||||
} else if (arg.starts_with("--map=")) {
|
||||
config.mapName = arg.substr(6);
|
||||
} else if (arg.starts_with("--turns=")) {
|
||||
config.numTurns = std::stoi(arg.substr(8));
|
||||
} else if (arg.starts_with("--defender=")) {
|
||||
std::string value = arg.substr(11);
|
||||
config.defenderToggle = (value == "true" || value == "1");
|
||||
} else if (arg == "--verbose") {
|
||||
config.verbose = true;
|
||||
} else {
|
||||
std::cerr << "Unknown argument: " << arg << "\n";
|
||||
std::cerr << "Use --help for usage information.\n";
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::cout << "Starting AI Performance Runner..." << std::endl;
|
||||
|
||||
// Set exec path so FilesystemUtils can find resource files
|
||||
FilesystemUtils::SetExecPath(argv[0]);
|
||||
|
||||
// Set cache directory for ActionPointDistances
|
||||
FixedActionPointDistances::SetCacheDirectory(
|
||||
FilesystemUtils::CacheFilesDirectory() + "apdCache/");
|
||||
|
||||
try {
|
||||
std::cout << "Shardok AI Performance Runner\n";
|
||||
std::cout << "==============================\n";
|
||||
|
||||
// Parse command line arguments
|
||||
auto config = ParseCommandLineArgs(argc, argv);
|
||||
|
||||
if (config.verbose) {
|
||||
std::cout << "Configuration:\n";
|
||||
std::cout << " Map: " << config.mapName << "\n";
|
||||
std::cout << " Turns: " << config.numTurns << "\n";
|
||||
std::cout << " AI is defender: " << (config.defenderToggle ? "Yes" : "No") << "\n";
|
||||
}
|
||||
|
||||
// Initialize game settings
|
||||
auto settings = PerformanceTestGameStateBuilder::InitializeGameSettings();
|
||||
|
||||
// Create test game state
|
||||
auto gameState = PerformanceTestGameStateBuilder::CreatePerfTestGameState(
|
||||
settings,
|
||||
config.defenderToggle);
|
||||
|
||||
// Create engine
|
||||
ShardokEngine engine(settings, gameState);
|
||||
|
||||
// Test basic functionality
|
||||
auto currentState = engine.GetCurrentGameState();
|
||||
|
||||
// Create AI client for testing
|
||||
const PlayerId aiPlayerId = 0;
|
||||
const bool isDefender = config.defenderToggle;
|
||||
const auto* hexMap = currentState->hex_map();
|
||||
const auto settingsGetter = settings->GetGetter();
|
||||
|
||||
ShardokAIClient aiClient(aiPlayerId, isDefender, hexMap, settingsGetter);
|
||||
|
||||
// Create a second AI client for the human player during setup
|
||||
// This ensures consistent state handling during setup phase
|
||||
const PlayerId humanPlayerId = 1;
|
||||
ShardokAIClient humanSetupAI(humanPlayerId, !isDefender, hexMap, settingsGetter);
|
||||
|
||||
// Complete setup phase - AI makes intelligent placement decisions
|
||||
if (currentState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
|
||||
while (currentState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
|
||||
PlayerId currentPlayer = currentState->current_player();
|
||||
auto availableCommands = engine.GetAvailableCommandProtos(currentPlayer, false);
|
||||
|
||||
if (availableCommands.empty()) {
|
||||
std::cout << "No commands available for player "
|
||||
<< static_cast<int>(currentPlayer) << "\n";
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentPlayer == aiPlayerId) {
|
||||
// Let AI make intelligent placement decisions
|
||||
auto choiceResults = aiClient.ChooseCommandIndex(engine);
|
||||
engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
|
||||
} else {
|
||||
// Human player: use AI for setup to ensure consistent state handling
|
||||
auto choiceResults = humanSetupAI.ChooseCommandIndex(engine);
|
||||
engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
|
||||
}
|
||||
|
||||
currentState = engine.GetCurrentGameState();
|
||||
}
|
||||
}
|
||||
|
||||
// Test AI performance for configured number of turns
|
||||
std::cout << "Running AI performance test for " << config.numTurns << " turns...\n";
|
||||
|
||||
std::vector<AIPerformanceMetrics> metrics;
|
||||
|
||||
for (int turn = 0; turn < config.numTurns; ++turn) {
|
||||
// Check if AI can make a move
|
||||
const auto availableCommands = engine.GetAvailableCommandProtos(aiPlayerId, false);
|
||||
if (availableCommands.empty()) {
|
||||
std::cout << " No commands available for AI player. Ending test.\n";
|
||||
break;
|
||||
}
|
||||
|
||||
// Get AI decision with performance metrics
|
||||
auto choiceResults = aiClient.ChooseCommandIndex(engine);
|
||||
|
||||
std::cout << " AI chose command index: " << choiceResults.chosenIndex << "\n";
|
||||
std::cout << " Depth achieved: " << choiceResults.depthAchieved << "\n";
|
||||
std::cout << " Commands evaluated: " << choiceResults.commandCountEvaluated << "/"
|
||||
<< choiceResults.availableCommandCount << "\n";
|
||||
|
||||
// Create metrics for this turn
|
||||
AIPerformanceMetrics turnMetrics;
|
||||
turnMetrics.commandNumber = turn + 1;
|
||||
turnMetrics.totalCommands = static_cast<int>(choiceResults.availableCommandCount);
|
||||
turnMetrics.depthAchieved = choiceResults.depthAchieved;
|
||||
turnMetrics.commandsEvaluated = static_cast<int>(choiceResults.commandCountEvaluated);
|
||||
turnMetrics.selectedCommandType = net::eagle0::shardok::common::CommandType_Name(
|
||||
availableCommands[choiceResults.chosenIndex].type());
|
||||
turnMetrics.completionReason = choiceResults.completionReason;
|
||||
|
||||
metrics.push_back(turnMetrics);
|
||||
|
||||
if (config.verbose) {
|
||||
std::cout << " Command: " << turnMetrics.selectedCommandType << "\n";
|
||||
std::cout << " Search depth: " << turnMetrics.depthAchieved << "\n";
|
||||
std::cout << " Commands evaluated: " << turnMetrics.commandsEvaluated << "\n";
|
||||
std::cout << " Applying command...\n";
|
||||
}
|
||||
|
||||
// Apply the chosen command
|
||||
engine.PostCommand(aiPlayerId, choiceResults.chosenIndex);
|
||||
|
||||
// Check if game is over
|
||||
if (engine.GameIsOver()) {
|
||||
std::cout << " Game over after " << (turn + 1) << " turns.\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary
|
||||
std::cout << "\nAI Search Performance Summary:\n";
|
||||
std::cout << "==============================\n";
|
||||
std::cout << "Total turns: " << metrics.size() << "\n";
|
||||
|
||||
if (!metrics.empty()) {
|
||||
// Calculate summary statistics
|
||||
double avgDepth = 0.0;
|
||||
int totalEvaluated = 0;
|
||||
int totalAvailable = 0;
|
||||
|
||||
for (const auto& metric : metrics) {
|
||||
avgDepth += metric.depthAchieved;
|
||||
totalEvaluated += metric.commandsEvaluated;
|
||||
totalAvailable += metric.totalCommands;
|
||||
}
|
||||
|
||||
avgDepth /= metrics.size();
|
||||
|
||||
std::cout << "Average search depth: " << std::fixed << std::setprecision(1) << avgDepth
|
||||
<< "\n";
|
||||
std::cout << "Total commands evaluated: " << totalEvaluated << "/" << totalAvailable
|
||||
<< "\n";
|
||||
|
||||
// Calculate evaluation rate by depth
|
||||
// Find max depth achieved across all turns
|
||||
int maxDepth = 0;
|
||||
for (const auto& metric : metrics) {
|
||||
maxDepth = std::max(maxDepth, metric.depthAchieved);
|
||||
}
|
||||
|
||||
if (maxDepth >= 2) {
|
||||
std::cout << "\nCommands evaluated by depth:\n";
|
||||
for (int depth = 2; depth <= maxDepth; ++depth) {
|
||||
int turnsAtThisDepth = 0;
|
||||
int totalCommandsAtDepth = 0;
|
||||
int totalCommandsAvailableAtDepth = 0;
|
||||
|
||||
for (const auto& metric : metrics) {
|
||||
bool reachedThisDepth = metric.depthAchieved >= depth;
|
||||
bool completedAtLowerDepth =
|
||||
(metric.depthAchieved < depth &&
|
||||
metric.completionReason ==
|
||||
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS);
|
||||
|
||||
if (reachedThisDepth || completedAtLowerDepth) {
|
||||
turnsAtThisDepth++;
|
||||
totalCommandsAvailableAtDepth += metric.totalCommands;
|
||||
|
||||
if (metric.depthAchieved > depth || completedAtLowerDepth) {
|
||||
// If achieved higher depth OR completed all commands at lower
|
||||
// depth, we evaluated ALL commands at this depth
|
||||
totalCommandsAtDepth += metric.totalCommands;
|
||||
} else if (metric.depthAchieved == depth) {
|
||||
// If stopped at this depth, we evaluated commandsEvaluated commands
|
||||
if (metric.completionReason ==
|
||||
EvaluationCompletionReason::RAN_OUT_OF_COMMANDS) {
|
||||
// If ran out of commands, we evaluated all of them
|
||||
totalCommandsAtDepth += metric.totalCommands;
|
||||
} else {
|
||||
// Otherwise we evaluated the reported number
|
||||
totalCommandsAtDepth += metric.commandsEvaluated;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If didn't reach this depth, contributes 0 commands (implicit)
|
||||
}
|
||||
|
||||
double evalRate =
|
||||
totalCommandsAvailableAtDepth > 0
|
||||
? (100.0 * totalCommandsAtDepth / totalCommandsAvailableAtDepth)
|
||||
: 0.0;
|
||||
|
||||
std::cout << " Depth " << depth << ": " << totalCommandsAtDepth << "/"
|
||||
<< totalCommandsAvailableAtDepth << " commands (" << std::fixed
|
||||
<< std::setprecision(1) << evalRate << "%, " << turnsAtThisDepth
|
||||
<< "/" << metrics.size() << " turns reached)\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "\nTurn-by-turn details:\n";
|
||||
for (const auto& metric : metrics) {
|
||||
std::string depthStr = std::to_string(metric.depthAchieved);
|
||||
if (metric.completionReason == EvaluationCompletionReason::RAN_OUT_OF_COMMANDS) {
|
||||
depthStr += "*";
|
||||
}
|
||||
std::cout << "Turn " << metric.commandNumber << ": depth " << depthStr
|
||||
<< ", evaluated " << metric.commandsEvaluated << "/"
|
||||
<< metric.totalCommands << ", chose " << metric.selectedCommandType
|
||||
<< " (" << CompletionReasonToString(metric.completionReason) << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 2025-01-15.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AIPERFORMANCERUNNER_HPP
|
||||
#define EAGLE0_AIPERFORMANCERUNNER_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/IterativeDeepeningAI.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
/**
|
||||
* Metrics captured for each AI command evaluation during performance testing.
|
||||
*/
|
||||
struct AIPerformanceMetrics {
|
||||
int commandNumber;
|
||||
int depthAchieved;
|
||||
int commandsEvaluated;
|
||||
int totalCommands;
|
||||
std::string selectedCommandType;
|
||||
EvaluationCompletionReason completionReason;
|
||||
};
|
||||
|
||||
/**
|
||||
* Overall results from a performance test run.
|
||||
*/
|
||||
struct PerformanceTestResults {
|
||||
std::string mapName;
|
||||
int totalTurns;
|
||||
std::vector<AIPerformanceMetrics> commandMetrics;
|
||||
double averageDepth;
|
||||
double completionRate;
|
||||
std::chrono::milliseconds totalTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration options for performance testing.
|
||||
*/
|
||||
struct PerformanceTestConfig {
|
||||
std::string mapName = "Alah";
|
||||
int numTurns = 5;
|
||||
bool defenderToggle = false;
|
||||
bool verbose = false;
|
||||
int aiUnitCount = 6;
|
||||
int humanUnitCount = 6;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AIPERFORMANCERUNNER_HPP
|
||||
@@ -1,51 +0,0 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_binary(
|
||||
name = "ai_performance_runner",
|
||||
srcs = [
|
||||
"AIPerformanceRunner.cpp",
|
||||
"AIPerformanceRunner.hpp",
|
||||
],
|
||||
copts = COPTS,
|
||||
data = [
|
||||
"//src/main/resources/net/eagle0/shardok:battalion_types",
|
||||
"//src/main/resources/net/eagle0/shardok:settings",
|
||||
"//src/main/resources/net/eagle0/shardok/maps",
|
||||
],
|
||||
deps = [
|
||||
":performance_test_game_state_builder",
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/common:time_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_attacker_strategy_selector",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_defender_strategy_selector",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_iterative_deepening",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_time_budget",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_water_crossing_command_chooser",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:battalion_type_registrar",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "performance_test_game_state_builder",
|
||||
srcs = ["PerformanceTestGameStateBuilder.cpp"],
|
||||
hdrs = [
|
||||
"PerformanceTestGameStateBuilder.hpp",
|
||||
],
|
||||
copts = COPTS,
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:game_state_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:battalion_type_registrar",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:player_info_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -1,205 +0,0 @@
|
||||
# AI Performance Runner Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the implementation plan for an automated AI performance testing tool for Shardok. The tool will replicate the manual performance testing currently done through the Unity client's "Custom Battle" interface, providing reproducible and automated performance measurements.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Automate Performance Testing**: Eliminate the need for manual Unity client interaction
|
||||
2. **Reproducible Results**: Ensure consistent test conditions across runs
|
||||
3. **Detailed Metrics**: Capture the same metrics currently observed manually (commands evaluated at each depth)
|
||||
4. **Clean Architecture**: Maintain proper dependency boundaries (no src/test dependencies in src/main)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/main/cpp/net/eagle0/shardok/ai_performance_runner/
|
||||
├── AIPerformanceRunner.cpp # Main binary entry point
|
||||
├── AIPerformanceRunner.hpp # Performance metrics structs and helpers
|
||||
├── PerformanceTestGameStateBuilder.cpp # Game state setup utilities
|
||||
├── PerformanceTestGameStateBuilder.hpp # Game state builder interface
|
||||
├── BUILD.bazel # Build configuration
|
||||
└── README.md # Usage documentation
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Performance Metrics Structure
|
||||
|
||||
```cpp
|
||||
struct AIPerformanceMetrics {
|
||||
int commandNumber;
|
||||
int depthAchieved;
|
||||
std::map<int, int> commandsEvaluatedAtDepth; // depth -> count
|
||||
std::chrono::milliseconds timeUsed;
|
||||
bool minimumDepthCompleted;
|
||||
bool searchCompleted;
|
||||
std::string selectedCommandType;
|
||||
};
|
||||
|
||||
struct PerformanceTestResults {
|
||||
std::string mapName;
|
||||
int totalTurns;
|
||||
std::vector<AIPerformanceMetrics> commandMetrics;
|
||||
double averageDepth;
|
||||
double completionRate;
|
||||
std::chrono::milliseconds totalTime;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Test Configuration
|
||||
|
||||
The default configuration replicates the Unity client's "Perf" button:
|
||||
|
||||
- **Map**: "Alah"
|
||||
- **AI Player**: 6 units with professions 1-6, all battalion type 4 (Heavy Infantry)
|
||||
- **Human Player**: 6 units (no specific configuration needed since AI will control)
|
||||
- **Defender Toggle**: Configurable (affects starting positions)
|
||||
|
||||
### 3. Key Components
|
||||
|
||||
#### AIPerformanceRunner.cpp
|
||||
- Main entry point with command-line argument parsing
|
||||
- Test execution loop
|
||||
- Results formatting and output
|
||||
- Integration with ShardokEngine and IterativeDeepeningAI
|
||||
|
||||
#### PerformanceTestGameStateBuilder.cpp
|
||||
- Game state creation utilities (migrated from test code)
|
||||
- Map loading helpers
|
||||
- Unit placement logic
|
||||
- Player setup functions
|
||||
|
||||
### 4. Build Configuration
|
||||
|
||||
```python
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_binary(
|
||||
name = "ai_performance_runner",
|
||||
srcs = ["AIPerformanceRunner.cpp"],
|
||||
copts = COPTS,
|
||||
data = [
|
||||
"//src/main/resources/net/eagle0/shardok:battalion_types",
|
||||
"//src/main/resources/net/eagle0/shardok:settings",
|
||||
"//src/main/resources/net/eagle0/shardok/maps",
|
||||
],
|
||||
deps = [
|
||||
":performance_test_game_state_builder",
|
||||
"//src/main/cpp/net/eagle0/common:time_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_iterative_deepening",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_attacker_strategy_selector",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_defender_strategy_selector",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_time_budget",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_water_crossing_command_chooser",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:battalion_type_registrar",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "performance_test_game_state_builder",
|
||||
srcs = ["PerformanceTestGameStateBuilder.cpp"],
|
||||
hdrs = [
|
||||
"AIPerformanceRunner.hpp",
|
||||
"PerformanceTestGameStateBuilder.hpp",
|
||||
],
|
||||
copts = COPTS,
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/common:tsv_parser",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:player_info_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Command-Line Interface
|
||||
|
||||
```bash
|
||||
# Run default performance test (Alah map, 6v6 units)
|
||||
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner
|
||||
|
||||
# Run with specific number of turns
|
||||
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --turns=10
|
||||
|
||||
# Run with defender configuration
|
||||
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --defender=true
|
||||
|
||||
# Run with verbose output
|
||||
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --verbose
|
||||
|
||||
# Run with specific map
|
||||
bazel run //src/main/cpp/net/eagle0/shardok/ai_performance_runner:ai_performance_runner -- --map=Chipingia
|
||||
```
|
||||
|
||||
### 6. Expected Output Format
|
||||
|
||||
```
|
||||
Shardok AI Performance Test
|
||||
===========================
|
||||
Map: Alah
|
||||
Configuration: 6v6 units (AI as attacker)
|
||||
Time Budget: Dynamic (proximity-based)
|
||||
|
||||
Turn 1:
|
||||
Command 1: Depth 2, evaluated 140/280 commands, time: 1250ms [MoveCommand]
|
||||
Command 2: Depth 2, evaluated ALL commands, time: 1180ms [MeleeCommand]
|
||||
Command 3: Depth 3, evaluated 21/156 commands, time: 1300ms [ArcheryCommand]
|
||||
Command 4: Depth 3, evaluated 78/312 commands, time: 1290ms [MoveCommand]
|
||||
Turn Summary: Avg depth 2.5, Total time: 5020ms
|
||||
|
||||
Overall Results:
|
||||
Total Turns: 5
|
||||
Average Depth Achieved: 2.4
|
||||
Commands Completed at Target Depth: 85%
|
||||
Total Time: 25.1s
|
||||
Average Time per Command: 1255ms
|
||||
```
|
||||
|
||||
### 7. Implementation Phases
|
||||
|
||||
#### Phase 1: Basic Infrastructure
|
||||
1. Create directory structure and BUILD.bazel
|
||||
2. Implement PerformanceTestGameStateBuilder with minimal game state creation
|
||||
3. Create basic AIPerformanceRunner that can load a map and create players
|
||||
|
||||
#### Phase 2: AI Integration
|
||||
1. Integrate IterativeDeepeningAI
|
||||
2. Implement performance metric collection
|
||||
3. Add basic output formatting
|
||||
|
||||
#### Phase 3: Full Feature Set
|
||||
1. Add command-line argument parsing
|
||||
2. Implement multiple test configurations (Perf, Rivers, Custom)
|
||||
3. Add detailed performance metrics and analysis
|
||||
|
||||
#### Phase 4: Polish and Documentation
|
||||
1. Create comprehensive README.md
|
||||
2. Add error handling and validation
|
||||
3. Implement baseline comparison features
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **Functional**: Tool successfully runs AI turns and captures performance metrics
|
||||
2. **Accurate**: Results match manually observed performance within reasonable variance
|
||||
3. **Reproducible**: Multiple runs produce consistent results
|
||||
4. **Maintainable**: Clean code structure with no dependencies on src/test
|
||||
5. **Usable**: Clear command-line interface and helpful output
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- JSON output format for automated analysis
|
||||
- Performance regression detection
|
||||
- Integration with CI/CD pipeline
|
||||
- Configurable test scenarios beyond "Perf" and "Rivers"
|
||||
- Multi-threaded performance testing
|
||||
-253
@@ -1,253 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 2025-01-15.
|
||||
//
|
||||
|
||||
#include "PerformanceTestGameStateBuilder.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/TsvParser.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/BattalionTypeRegistrar.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
namespace {
|
||||
|
||||
// Profession enum values
|
||||
constexpr int NO_PROFESSION = 0;
|
||||
|
||||
// Player IDs
|
||||
constexpr PlayerId AI_PLAYER_ID = 0;
|
||||
constexpr PlayerId HUMAN_PLAYER_ID = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
auto PerformanceTestGameStateBuilder::InitializeGameSettings() -> GameSettingsSPtr {
|
||||
auto settings = std::make_shared<GameSettings>();
|
||||
auto setter = settings->GetSetter();
|
||||
|
||||
// Load battalion types
|
||||
BattalionTypeRegistrar::RegisterBattalionTypes(setter);
|
||||
|
||||
// Load complete settings from settings.tsv file
|
||||
TsvParser parser;
|
||||
const string settingsPath = FilesystemUtils::StaticShardokFilesDirectory() + "settings.tsv";
|
||||
const string settingsTsv = string(byte_vector::FromPath(settingsPath));
|
||||
const auto valuesAndTypes = parser.ParseColumnEntryTsv(settingsTsv);
|
||||
setter.SetFromTypesAndValues(valuesAndTypes[1], valuesAndTypes[0]);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
auto PerformanceTestGameStateBuilder::CreatePerfTestGameState(
|
||||
const GameSettingsSPtr& settings,
|
||||
bool defenderToggle) -> GameStateW {
|
||||
return CreateCustomTestGameState(
|
||||
settings,
|
||||
"Alah",
|
||||
6, // 6 AI units (full test configuration)
|
||||
6, // 6 human units (full test configuration)
|
||||
defenderToggle);
|
||||
}
|
||||
|
||||
auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
|
||||
const GameSettingsSPtr& settings,
|
||||
const std::string& mapName,
|
||||
int aiUnitCount,
|
||||
int humanUnitCount,
|
||||
bool defenderToggle) -> GameStateW {
|
||||
// Load the map using existing utilities
|
||||
auto hexMapProto = LoadMap(mapName);
|
||||
|
||||
// Create player info protos
|
||||
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos;
|
||||
|
||||
// AI player
|
||||
net::eagle0::shardok::common::PlayerInfo aiPlayerInfo;
|
||||
aiPlayerInfo.set_player_id(AI_PLAYER_ID);
|
||||
aiPlayerInfo.set_is_defender(defenderToggle);
|
||||
aiPlayerInfo.set_starting_food(1000);
|
||||
aiPlayerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
|
||||
if (defenderToggle) {
|
||||
aiPlayerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
|
||||
} else {
|
||||
aiPlayerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
|
||||
}
|
||||
playerInfoProtos.push_back(aiPlayerInfo);
|
||||
|
||||
// Human player
|
||||
net::eagle0::shardok::common::PlayerInfo humanPlayerInfo;
|
||||
humanPlayerInfo.set_player_id(HUMAN_PLAYER_ID);
|
||||
humanPlayerInfo.set_is_defender(!defenderToggle);
|
||||
humanPlayerInfo.set_starting_food(1000);
|
||||
humanPlayerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
|
||||
if (!defenderToggle) {
|
||||
humanPlayerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
|
||||
} else {
|
||||
humanPlayerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
|
||||
}
|
||||
playerInfoProtos.push_back(humanPlayerInfo);
|
||||
|
||||
// Create units
|
||||
std::vector<net::eagle0::shardok::storage::fb::Unit> units;
|
||||
|
||||
// Create AI units in reserve (location -1, -1)
|
||||
for (int i = 0; i < aiUnitCount && i < 6; ++i) {
|
||||
units.push_back(AddGenericUnit(
|
||||
AI_PLAYER_ID,
|
||||
i, // Unit ID
|
||||
net::eagle0::shardok::storage::fb::Coords(-1, -1), // Reserve location
|
||||
i + 1, // Profession: 1-6 (Mage through Strategist)
|
||||
HEAVY_INFANTRY_BATTALION_TYPE,
|
||||
defenderToggle ? -1 : 0)); // Defender: -1, Attacker: 0
|
||||
}
|
||||
|
||||
// Create human units in reserve (location -1, -1)
|
||||
for (int i = 0; i < humanUnitCount && i < 6; ++i) {
|
||||
units.push_back(AddGenericUnit(
|
||||
HUMAN_PLAYER_ID,
|
||||
aiUnitCount + i, // Unit ID starting aiUnitCount
|
||||
net::eagle0::shardok::storage::fb::Coords(-1, -1), // Reserve location
|
||||
NO_PROFESSION,
|
||||
HEAVY_INFANTRY_BATTALION_TYPE,
|
||||
defenderToggle ? 0 : -1)); // Defender: -1, Attacker: 0
|
||||
}
|
||||
|
||||
// Use the proper SetupInitialGameState helper (setup phase will be handled by AI)
|
||||
return shardok::fb::SetupInitialGameState(
|
||||
"performance_test_game", // gameId
|
||||
hexMapProto,
|
||||
playerInfoProtos,
|
||||
units,
|
||||
4, // month
|
||||
false, // isWinter
|
||||
settings->GetGetter());
|
||||
}
|
||||
|
||||
auto PerformanceTestGameStateBuilder::AddPlayerInfo(
|
||||
flatbuffers::FlatBufferBuilder& fbb,
|
||||
int playerId,
|
||||
bool isDefender,
|
||||
int food) -> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo> {
|
||||
std::vector<int8_t> victoryConditions{
|
||||
net::eagle0::shardok::storage::fb::
|
||||
VictoryCondition_VICTORY_CONDITION_LAST_PLAYER_STANDING};
|
||||
|
||||
if (isDefender) {
|
||||
victoryConditions.push_back(
|
||||
net::eagle0::shardok::storage::fb::
|
||||
VictoryCondition_VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
|
||||
} else {
|
||||
victoryConditions.push_back(
|
||||
net::eagle0::shardok::storage::fb::
|
||||
VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
|
||||
}
|
||||
|
||||
auto victoryConditionsOffset = fbb.CreateVector(victoryConditions);
|
||||
|
||||
net::eagle0::shardok::storage::fb::PlayerInfoBuilder pib(fbb);
|
||||
pib.add_player_id(playerId);
|
||||
pib.add_starting_food(food);
|
||||
pib.add_is_defender(isDefender);
|
||||
pib.add_victory_conditions(victoryConditionsOffset);
|
||||
|
||||
return pib.Finish();
|
||||
}
|
||||
|
||||
auto PerformanceTestGameStateBuilder::AddGenericUnit(
|
||||
PlayerId playerId,
|
||||
UnitId unitId,
|
||||
const net::eagle0::shardok::storage::fb::Coords& location,
|
||||
int profession,
|
||||
int battalionType,
|
||||
int startingPositionIndex) -> net::eagle0::shardok::storage::fb::Unit {
|
||||
net::eagle0::shardok::storage::fb::Unit unit{}; // Initialize to zero
|
||||
|
||||
// Basic unit properties (following UnitConversions.cpp pattern)
|
||||
unit.mutate_player_id(playerId);
|
||||
unit.mutate_unit_id(unitId);
|
||||
unit.mutate_eagle_player_id(playerId); // Set eagle player ID
|
||||
unit.mutable_location() = location;
|
||||
unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
unit.mutate_remaining_action_points(12);
|
||||
unit.mutate_hidden(false);
|
||||
unit.mutate_fortified(false);
|
||||
unit.mutate_can_flee(true);
|
||||
unit.mutate_can_start_fire(false);
|
||||
unit.mutate_can_archery(false);
|
||||
unit.mutate_stun_rounds_remaining(0);
|
||||
unit.mutate_commanding_unit_id(-1);
|
||||
unit.mutate_targeted_unit(-1);
|
||||
unit.mutate_starting_position_index(startingPositionIndex);
|
||||
unit.mutate_has_moved_in_zoc(false);
|
||||
unit.mutate_volleys_remaining(0);
|
||||
unit.mutate_food_remaining(1000.0f); // Set food remaining
|
||||
|
||||
// Battalion
|
||||
net::eagle0::shardok::storage::fb::Battalion battalion;
|
||||
battalion.mutate_type(
|
||||
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(battalionType));
|
||||
battalion.mutate_size(1000.0);
|
||||
battalion.mutate_armament(100.0f);
|
||||
battalion.mutate_training(100.0f);
|
||||
battalion.mutate_morale(50.0f);
|
||||
unit.mutable_battalion() = battalion;
|
||||
|
||||
// Hero (if profession is specified)
|
||||
if (profession != NO_PROFESSION) {
|
||||
unit.mutate_has_attached_hero(true);
|
||||
|
||||
net::eagle0::shardok::storage::fb::Hero hero;
|
||||
hero.mutate_strength(50);
|
||||
hero.mutate_strength_xp(0);
|
||||
hero.mutate_agility(50);
|
||||
hero.mutate_agility_xp(0);
|
||||
hero.mutate_wisdom(50);
|
||||
hero.mutate_wisdom_xp(0);
|
||||
hero.mutate_charisma(50);
|
||||
hero.mutate_charisma_xp(0);
|
||||
hero.mutate_constitution(80);
|
||||
hero.mutate_constitution_xp(0);
|
||||
hero.mutate_vigor(50);
|
||||
hero.mutate_starting_vigor(50);
|
||||
hero.mutate_spent_vigor(0);
|
||||
hero.mutate_bravery(50);
|
||||
hero.mutate_integrity(50);
|
||||
hero.mutate_ambition(50);
|
||||
hero.mutate_eagle_hero_id(unitId + 1);
|
||||
hero.mutate_is_vip(false);
|
||||
|
||||
hero.mutable_profession_info().mutate_profession(
|
||||
static_cast<net::eagle0::shardok::storage::fb::Profession>(profession));
|
||||
hero.mutable_profession_info().mutate_meteor_cast_state(
|
||||
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
|
||||
|
||||
hero.mutable_control_info().mutate_controlled_unit_id(-1);
|
||||
hero.mutable_control_info().mutate_controlled_this_round(false);
|
||||
|
||||
unit.mutable_attached_hero() = hero;
|
||||
} else {
|
||||
unit.mutate_has_attached_hero(false);
|
||||
}
|
||||
|
||||
// Initialize opponent knowledge for both players (player IDs 0 and 1)
|
||||
unit.mutable_opponent_knowledge()->Mutate(0, 0); // Player 0 knowledge
|
||||
unit.mutable_opponent_knowledge()->Mutate(1, 0); // Player 1 knowledge
|
||||
|
||||
return unit;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 2025-01-15.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_PERFORMANCETESTGAMESTATEBUILDER_HPP
|
||||
#define EAGLE0_PERFORMANCETESTGAMESTATEBUILDER_HPP
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/player_info.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class GameSettings;
|
||||
using GameSettingsSPtr = std::shared_ptr<GameSettings>;
|
||||
|
||||
/**
|
||||
* Builder class for creating game states used in performance testing.
|
||||
* Provides utilities to set up specific test scenarios matching the Unity client's
|
||||
* "Perf" button configuration.
|
||||
*/
|
||||
class PerformanceTestGameStateBuilder {
|
||||
public:
|
||||
/**
|
||||
* Initialize game settings from the default configuration files.
|
||||
* Must be called before creating game states.
|
||||
*/
|
||||
static auto InitializeGameSettings() -> GameSettingsSPtr;
|
||||
|
||||
/**
|
||||
* Create the standard "Perf" test configuration:
|
||||
* - Map: Alah
|
||||
* - 6 AI units with professions 1-6, all Heavy Infantry
|
||||
* - 6 Human units (minimal configuration)
|
||||
*
|
||||
* @param settings The game settings to use
|
||||
* @param defenderToggle If true, AI is defender; if false, AI is attacker
|
||||
* @return A GameStateW with the configured battle
|
||||
*/
|
||||
static auto CreatePerfTestGameState(
|
||||
const GameSettingsSPtr& settings,
|
||||
bool defenderToggle = false) -> GameStateW;
|
||||
|
||||
/**
|
||||
* Create a custom test configuration with specified parameters.
|
||||
*
|
||||
* @param settings The game settings to use
|
||||
* @param mapName Name of the map to load
|
||||
* @param aiUnitCount Number of AI units to create
|
||||
* @param humanUnitCount Number of human units to create
|
||||
* @param defenderToggle If true, AI is defender; if false, AI is attacker
|
||||
* @return A GameStateW with the configured battle
|
||||
*/
|
||||
static auto CreateCustomTestGameState(
|
||||
const GameSettingsSPtr& settings,
|
||||
const std::string& mapName,
|
||||
int aiUnitCount,
|
||||
int humanUnitCount,
|
||||
bool defenderToggle) -> GameStateW;
|
||||
|
||||
private:
|
||||
// Helper functions for building game state components
|
||||
static auto
|
||||
AddPlayerInfo(flatbuffers::FlatBufferBuilder& fbb, int playerId, bool isDefender, int food)
|
||||
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
|
||||
|
||||
static auto AddGenericUnit(
|
||||
PlayerId playerId,
|
||||
UnitId unitId,
|
||||
const net::eagle0::shardok::storage::fb::Coords& location,
|
||||
int profession,
|
||||
int battalionType,
|
||||
int startingPositionIndex = -1) -> net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
// Battalion type constants (matching Unity client)
|
||||
static constexpr int HEAVY_INFANTRY_BATTALION_TYPE = 4;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_PERFORMANCETESTGAMESTATEBUILDER_HPP
|
||||
@@ -129,7 +129,7 @@ auto ShardokGameController::LockedCheckOneAICommand() -> bool {
|
||||
|
||||
const PlayerId currentPid = engine->GetCurrentPlayerId();
|
||||
if (const shared_ptr<ShardokAIClient> currentPlayerClient = LockedAIClientForPid(currentPid)) {
|
||||
const int index = currentPlayerClient->ChooseCommandIndex(*engine).chosenIndex;
|
||||
const int index = currentPlayerClient->ChooseCommandIndex(*engine);
|
||||
|
||||
engine->PostCommand(currentPid, index);
|
||||
LockedNotifyClients();
|
||||
|
||||
@@ -9,15 +9,17 @@
|
||||
#ifndef AvailableCommandsFactory_hpp
|
||||
#define AvailableCommandsFactory_hpp
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using std::optional;
|
||||
using std::unique_ptr;
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
using UnitIdOptional = optional<UnitId>;
|
||||
|
||||
class AvailableCommandsFactory {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "game_state_w",
|
||||
srcs = ["GameStateW.cpp"],
|
||||
hdrs = ["GameStateW.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/common:container_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "engine",
|
||||
srcs = ["ShardokEngine.cpp"],
|
||||
@@ -22,7 +7,6 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":game_state_w",
|
||||
":unit_placement_info",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:perform_undead_commands_action",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:update_game_status_action",
|
||||
@@ -31,6 +15,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:game_state_validator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:action_result_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_filter",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_with_resulting_state_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -132,9 +117,10 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = ["//src/main/cpp/net/eagle0/shardok/library:__subpackages__"],
|
||||
deps = [
|
||||
":game_state_w",
|
||||
":shardok_exception",
|
||||
"//src/main/cpp/net/eagle0/common:random_generator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 2025-01-21.
|
||||
//
|
||||
|
||||
#include "GameStateW.hpp"
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
auto GameStateW::GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
|
||||
-> const Unit* {
|
||||
const auto* state = Get();
|
||||
if (!state || !state->hex_map()) { return nullptr; }
|
||||
|
||||
const int16_t rowCount = state->hex_map()->row_count();
|
||||
const int16_t columnCount = state->hex_map()->column_count();
|
||||
|
||||
// Check bounds
|
||||
if (coords.row() < 0 || coords.row() >= rowCount || coords.column() < 0 ||
|
||||
coords.column() >= columnCount) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Fast path: use bitfield cache if available
|
||||
if (state->occupied_tiles() && !state->occupied_tiles()->empty()) {
|
||||
const size_t tileIndex = coords.row() * columnCount + coords.column();
|
||||
const size_t expectedBitfieldSize = (rowCount * columnCount + 7) / 8; // Ceiling division
|
||||
|
||||
if (state->occupied_tiles()->size() == expectedBitfieldSize) {
|
||||
const size_t byteIndex = tileIndex / 8;
|
||||
const size_t bitOffset = tileIndex % 8;
|
||||
const uint8_t byte = state->occupied_tiles()->Get(byteIndex);
|
||||
const bool isOccupied = (byte & (1 << bitOffset)) != 0;
|
||||
|
||||
if (!isOccupied) {
|
||||
return nullptr; // Fast path: definitely no unit here (90% of cases)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: O(n) search through units
|
||||
// Used when bitfield not available OR when bitfield indicates occupation
|
||||
if (!state->units()) { return nullptr; }
|
||||
|
||||
for (int i = 0; i < state->units()->size(); ++i) {
|
||||
const auto* unit = state->units()->Get(i);
|
||||
if (unit && unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location().row() == coords.row() &&
|
||||
unit->location().column() == coords.column()) {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto GameStateW::GetKnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const net::eagle0::shardok::storage::fb::Coords& coords) const -> const Unit* {
|
||||
const auto* occupant = GetOccupant(coords);
|
||||
if (occupant) {
|
||||
if (!occupant->hidden() && occupant->player_id() != playerId &&
|
||||
!common::Contains(allyPids, occupant->player_id())) {
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void GameStateW::UpdateOccupiedTile(
|
||||
const net::eagle0::shardok::storage::fb::Coords& oldCoords,
|
||||
const net::eagle0::shardok::storage::fb::Coords& newCoords) {
|
||||
const auto* state = Get();
|
||||
auto* mutableOccupiedTiles = (*this)->mutable_occupied_tiles();
|
||||
if (!state || !state->hex_map() || !mutableOccupiedTiles) { return; }
|
||||
|
||||
const int16_t rowCount = state->hex_map()->row_count();
|
||||
const int16_t columnCount = state->hex_map()->column_count();
|
||||
|
||||
// Clear old position in bitfield
|
||||
if (oldCoords.row() >= 0 && oldCoords.row() < rowCount && oldCoords.column() >= 0 &&
|
||||
oldCoords.column() < columnCount) {
|
||||
const size_t tileIndex = oldCoords.row() * columnCount + oldCoords.column();
|
||||
const size_t byteIndex = tileIndex / 8;
|
||||
const size_t bitOffset = tileIndex % 8;
|
||||
if (byteIndex < mutableOccupiedTiles->size()) {
|
||||
uint8_t byte = mutableOccupiedTiles->Get(byteIndex);
|
||||
byte &= ~(1 << bitOffset); // Clear the bit
|
||||
mutableOccupiedTiles->Mutate(byteIndex, byte);
|
||||
}
|
||||
}
|
||||
|
||||
// Set new position in bitfield
|
||||
if (newCoords.row() >= 0 && newCoords.row() < rowCount && newCoords.column() >= 0 &&
|
||||
newCoords.column() < columnCount) {
|
||||
const size_t tileIndex = newCoords.row() * columnCount + newCoords.column();
|
||||
const size_t byteIndex = tileIndex / 8;
|
||||
const size_t bitOffset = tileIndex % 8;
|
||||
if (byteIndex < mutableOccupiedTiles->size()) {
|
||||
uint8_t byte = mutableOccupiedTiles->Get(byteIndex);
|
||||
byte |= (1 << bitOffset); // Set the bit
|
||||
mutableOccupiedTiles->Mutate(byteIndex, byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto GameStateW::GetOccupiedTilesBitfield() const -> const flatbuffers::Vector<uint8_t>* {
|
||||
const auto* state = Get();
|
||||
if (!state || !state->hex_map()) { return nullptr; }
|
||||
|
||||
if (!state->occupied_tiles() || state->occupied_tiles()->empty()) { return nullptr; }
|
||||
|
||||
// Verify the bitfield size matches expected map size
|
||||
const int16_t rowCount = state->hex_map()->row_count();
|
||||
const int16_t columnCount = state->hex_map()->column_count();
|
||||
const size_t expectedBitfieldSize = (rowCount * columnCount + 7) / 8;
|
||||
|
||||
if (state->occupied_tiles()->size() != expectedBitfieldSize) { return nullptr; }
|
||||
|
||||
return state->occupied_tiles();
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,109 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 2025-01-15.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_GAMESTATEW_HPP
|
||||
#define EAGLE0_GAMESTATEW_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
/**
|
||||
* @class GameStateW
|
||||
* @brief A wrapper class for the FlatBuffer-generated GameState type.
|
||||
*
|
||||
* GameStateW extends the Wrapper class to provide additional functionality
|
||||
* for working with the net::eagle0::shardok::storage::fb::GameState type.
|
||||
* It inherits all constructors and assignment operators from the base Wrapper
|
||||
* class, enabling seamless integration with the underlying FlatBuffer type.
|
||||
*
|
||||
* This class is part of the shardok namespace and is designed to simplify
|
||||
* interactions with the GameState FlatBuffer type while maintaining the
|
||||
* flexibility and functionality of the Wrapper base class.
|
||||
*/
|
||||
class GameStateW : public Wrapper<net::eagle0::shardok::storage::fb::GameState> {
|
||||
public:
|
||||
using BaseType = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
// Inherit all constructors from Wrapper
|
||||
using BaseType::BaseType;
|
||||
|
||||
// Default constructor
|
||||
GameStateW() : BaseType() {}
|
||||
|
||||
// Copy constructor
|
||||
GameStateW(const GameStateW& other) : BaseType(other) {}
|
||||
|
||||
// Move constructor
|
||||
GameStateW(GameStateW&& other) noexcept : BaseType(std::move(other)) {}
|
||||
|
||||
// Copy assignment
|
||||
GameStateW& operator=(const GameStateW& other) {
|
||||
BaseType::operator=(other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Move assignment
|
||||
GameStateW& operator=(GameStateW&& other) noexcept {
|
||||
BaseType::operator=(std::move(other));
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Constructor from base type
|
||||
GameStateW(const BaseType& base) : BaseType(base) {}
|
||||
GameStateW(BaseType&& base) : BaseType(std::move(base)) {}
|
||||
|
||||
/**
|
||||
* @brief Get the unit occupying the specified coordinates using occupied tiles bitfield.
|
||||
* @param coords The coordinates to check.
|
||||
* @return Pointer to the unit at the coordinates, or nullptr if none.
|
||||
*
|
||||
* Fast path: O(1) bitfield check for empty tiles (~90% of cases).
|
||||
* Slow path: O(n) unit search only when bitfield indicates occupation (~10% of cases).
|
||||
*/
|
||||
[[nodiscard]] auto GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
|
||||
-> const Unit*;
|
||||
|
||||
/**
|
||||
* @brief Get the known enemy unit occupying the specified coordinates using occupied tiles
|
||||
* bitfield.
|
||||
* @param playerId The player ID to check enemies for.
|
||||
* @param allyPids Vector of allied player IDs.
|
||||
* @param coords The coordinates to check.
|
||||
* @return Pointer to the enemy unit at the coordinates, or nullptr if none.
|
||||
*
|
||||
* Uses the bitfield-optimized GetOccupant() internally.
|
||||
*/
|
||||
[[nodiscard]] auto GetKnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const std::vector<PlayerId>& allyPids,
|
||||
const net::eagle0::shardok::storage::fb::Coords& coords) const -> const Unit*;
|
||||
|
||||
/**
|
||||
* @brief Update the occupied tiles bitfield when a unit changes position.
|
||||
* @param oldCoords The previous coordinates (use {-1, -1} if unit was off-map).
|
||||
* @param newCoords The new coordinates (use {-1, -1} if unit is now off-map).
|
||||
*/
|
||||
void UpdateOccupiedTile(
|
||||
const net::eagle0::shardok::storage::fb::Coords& oldCoords,
|
||||
const net::eagle0::shardok::storage::fb::Coords& newCoords);
|
||||
|
||||
/**
|
||||
* @brief Get the occupied tiles bitfield for efficient tile occupancy checking.
|
||||
* @return Pointer to the bitfield data, or nullptr if not available.
|
||||
*
|
||||
* Returns the raw bitfield where bit at index (row*column_count + col) indicates
|
||||
* if that tile is occupied. Useful for caching the bitfield to avoid repeated
|
||||
* GameStateW lookups in performance-critical code like MoveCommand.
|
||||
*/
|
||||
[[nodiscard]] auto GetOccupiedTilesBitfield() const -> const flatbuffers::Vector<uint8_t>*;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_GAMESTATEW_HPP
|
||||
@@ -14,7 +14,7 @@ using std::vector;
|
||||
|
||||
auto ShardokAction::Execute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
|
||||
vector<ActionResult> results = InternalExecute(currentState, generator);
|
||||
|
||||
return results;
|
||||
@@ -22,7 +22,7 @@ auto ShardokAction::Execute(
|
||||
|
||||
auto ShardokAction::ExecuteWithRoll(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator,
|
||||
std::shared_ptr<RandomGenerator> generator,
|
||||
const std::optional<int32_t> roll) const -> vector<ActionResult> {
|
||||
vector<ActionResult> results = InternalExecuteWithRoll(currentState, generator, roll);
|
||||
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
|
||||
#include "ShardokException.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using net::eagle0::shardok::storage::ActionResult;
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
using std::shared_ptr;
|
||||
using std::vector;
|
||||
using PercentileRollOdds = net::eagle0::shardok::storage::Odds;
|
||||
@@ -37,13 +39,13 @@ private:
|
||||
// override that one and get the default behavior here.
|
||||
[[nodiscard]] virtual auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> std::vector<ActionResult> {
|
||||
const std::shared_ptr<RandomGenerator> generator) const -> std::vector<ActionResult> {
|
||||
return InternalExecuteWithRoll(currentState, generator, std::optional<int32_t>());
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual auto InternalExecuteWithRoll(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator,
|
||||
std::shared_ptr<RandomGenerator> generator,
|
||||
std::optional<int32_t> roll) const -> std::vector<ActionResult> {
|
||||
throw ShardokClientErrorException("Roll not supported");
|
||||
}
|
||||
@@ -56,11 +58,11 @@ public:
|
||||
|
||||
[[nodiscard]] auto Execute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> std::vector<ActionResult>;
|
||||
std::shared_ptr<RandomGenerator> generator) const -> std::vector<ActionResult>;
|
||||
|
||||
[[nodiscard]] auto ExecuteWithRoll(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator,
|
||||
std::shared_ptr<RandomGenerator> generator,
|
||||
std::optional<int32_t> roll) const -> std::vector<ActionResult>;
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ using net::eagle0::shardok::storage::ShardokActionWithResultingState;
|
||||
using GameStatusProto = net::eagle0::shardok::common::GameStatus;
|
||||
using TileModifierProto = net::eagle0::shardok::common::TileModifier;
|
||||
|
||||
[[nodiscard]] auto ShardokEngine::GetCurrentGameState() const
|
||||
-> net::eagle0::shardok::storage::fb::GameState const * {
|
||||
return gameState.Get();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto ShardokEngine::GetCurrentGameStateBytes() const -> byte_vector {
|
||||
return gameState.ToByteVector();
|
||||
}
|
||||
@@ -92,7 +97,7 @@ void ShardokEngine::ApplyAndAddActionResults(const vector<ActionResultProto> &re
|
||||
}
|
||||
|
||||
void ShardokEngine::ApplyAndAddActionResult(const ActionResultProto &result) {
|
||||
gameState = ApplyResult(std::move(gameState), result, settingsGetter);
|
||||
MutatingApplyResult(gameState, result, settingsGetter);
|
||||
|
||||
if (trackHistory) {
|
||||
actionHistory.emplace_back();
|
||||
@@ -109,7 +114,7 @@ ShardokEngine::ShardokEngine(
|
||||
settingsGetter(settings->GetGetter()),
|
||||
availableCommandsFactory(
|
||||
AvailableCommandsFactory::MakeAvailableCommandsFactory(settingsGetter)),
|
||||
gameState(GameStateW::FromByteString(history.back().state_after_fb())),
|
||||
gameState(fb::GameStateW::FromByteString(history.back().state_after_fb())),
|
||||
trackHistory(trackHistory),
|
||||
actionHistory(history),
|
||||
criticalTileCoords(gameState->hex_map()) {}
|
||||
@@ -179,7 +184,7 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
|
||||
const ShardokActionWithResultingState &awrs : newHistory) {
|
||||
GameStateView viewAfter = GameStateFilteredForPlayer(
|
||||
settingsGetter,
|
||||
GameStateW::FromByteString(awrs.state_after_fb()),
|
||||
fb::GameStateW::FromByteString(awrs.state_after_fb()),
|
||||
askingPlayer);
|
||||
|
||||
if (auto filteredResult = ActionResultFilteredForPlayer(
|
||||
@@ -192,7 +197,7 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
|
||||
filteredResult.has_value()) {
|
||||
filteredHistory.push_back(*filteredResult);
|
||||
}
|
||||
previousState = GameStateW::FromByteString(awrs.state_after_fb());
|
||||
previousState = fb::GameStateW::FromByteString(awrs.state_after_fb());
|
||||
previousStatePtr = previousState.Get();
|
||||
previousView = viewAfter;
|
||||
}
|
||||
@@ -214,7 +219,7 @@ auto ShardokEngine::GetUnitById(const PlayerId askingPlayer, const UnitId unitId
|
||||
}
|
||||
|
||||
void ShardokEngine::PostWhileCurrentPlayerHasOnlyOneOption(
|
||||
std::shared_ptr<RandomGenerator> randomGenerator) {
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator) {
|
||||
while (GetGameStatus()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING &&
|
||||
GetCurrentPlayerId() != UNCONTROLLED_PLAYER_ID) {
|
||||
@@ -455,7 +460,7 @@ void ShardokEngine::HandleActionResult(
|
||||
const Coords modifiedCoords = FromCoordsProto(modifierWithCoords.coords());
|
||||
const TileModifierProto &modifier = modifierWithCoords.modifiers();
|
||||
|
||||
const Unit *occupant = gameState.GetOccupant(modifiedCoords);
|
||||
const Unit *occupant = Occupant(GetCurrentGameState()->units(), modifiedCoords);
|
||||
// Check for swept away hero
|
||||
if (const Terrain *terrain = GetTerrain(GetCurrentGameState()->hex_map(), modifiedCoords);
|
||||
occupant && IsWater(terrain->type()) && !IsTraversible(modifier) &&
|
||||
@@ -582,7 +587,6 @@ void AddUnits(vector<net::eagle0::shardok::storage::ResolvedUnit> &to, const Uni
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
ru.set_status(
|
||||
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT);
|
||||
break;
|
||||
@@ -600,7 +604,7 @@ auto ShardokEngine::EndGameUnits() const -> vector<net::eagle0::shardok::storage
|
||||
"Trying to get the end game units before the game is over");
|
||||
}
|
||||
|
||||
const auto &gs = GetCurrentGameState();
|
||||
const auto *gs = GetCurrentGameState();
|
||||
|
||||
vector<net::eagle0::shardok::storage::ResolvedUnit> endgameUnits;
|
||||
AddUnits(endgameUnits, *gs->units());
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/action_result_view.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"
|
||||
@@ -34,6 +35,7 @@ using std::vector;
|
||||
|
||||
using net::eagle0::shardok::api::UnitView;
|
||||
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
using net::eagle0::shardok::storage::ShardokActionWithResultingState;
|
||||
using HexMapProto = net::eagle0::shardok::common::HexMap;
|
||||
|
||||
@@ -60,14 +62,15 @@ private:
|
||||
|
||||
[[nodiscard]] auto HandleUnitFallingIntoWater(
|
||||
const Terrain *terrain,
|
||||
const fb::Unit *unit,
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
std::shared_ptr<RandomGenerator> randomGenerator) const -> vector<ActionResult>;
|
||||
|
||||
void HandleActionResult(
|
||||
const ActionResult &actionResult,
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator);
|
||||
|
||||
[[nodiscard]] auto GetUnit(const UnitId uid) const -> const fb::Unit * {
|
||||
[[nodiscard]] auto GetUnit(const UnitId uid) const
|
||||
-> const net::eagle0::shardok::storage::fb::Unit * {
|
||||
return GetCurrentGameState()->units()->Get(uid);
|
||||
}
|
||||
|
||||
@@ -111,7 +114,8 @@ public:
|
||||
|
||||
[[nodiscard]] auto GetGameStateAtStartOfAction(ActionId startingActionId) const -> GameStateW;
|
||||
|
||||
[[nodiscard]] auto GetCurrentGameState() const -> const GameStateW & { return gameState; }
|
||||
[[nodiscard]] auto GetCurrentGameState() const
|
||||
-> net::eagle0::shardok::storage::fb::GameState const *;
|
||||
|
||||
[[nodiscard]] auto GetCurrentGameStateBytes() const -> byte_vector;
|
||||
|
||||
@@ -125,7 +129,7 @@ public:
|
||||
|
||||
// Controller API
|
||||
[[nodiscard]] auto GetGameHistory(ActionId lastUpdatedActionId) const
|
||||
-> vector<ShardokActionWithResultingState>;
|
||||
-> vector<net::eagle0::shardok::storage::ShardokActionWithResultingState>;
|
||||
|
||||
[[nodiscard]] auto GetUnfilteredHistoryCount() const -> size_t {
|
||||
return actionHistory.size() + startingHistoryCount;
|
||||
@@ -141,7 +145,8 @@ public:
|
||||
[[nodiscard]] auto GetFilteredGameHistory(PlayerId askingPlayer) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
|
||||
[[nodiscard]] auto GetUnitById(PlayerId askingPlayer, UnitId unitId) const -> UnitView;
|
||||
[[nodiscard]] auto GetUnitById(PlayerId askingPlayer, UnitId unitId) const
|
||||
-> net::eagle0::shardok::api::UnitView;
|
||||
|
||||
void PostPlacementCommands(
|
||||
PlayerId player,
|
||||
@@ -156,7 +161,8 @@ public:
|
||||
std::shared_ptr<RandomGenerator> randomGenerator = nullptr,
|
||||
std::optional<int32_t> roll = std::nullopt);
|
||||
|
||||
void PostWhileCurrentPlayerHasOnlyOneOption(std::shared_ptr<RandomGenerator> randomGenerator);
|
||||
void PostWhileCurrentPlayerHasOnlyOneOption(
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator);
|
||||
auto PostWhilePlayerHasOnlyOneOption(
|
||||
PlayerId playerId,
|
||||
std::shared_ptr<RandomGenerator> randomGenerator) -> bool;
|
||||
@@ -174,7 +180,7 @@ public:
|
||||
[[nodiscard]] auto GetMonth() const -> int { return GetCurrentGameState()->month(); }
|
||||
|
||||
[[nodiscard]] auto GetPlayerInfos() const -> vector<PlayerInfoProto> {
|
||||
const auto ¤tGameState = GetCurrentGameState();
|
||||
const auto *currentGameState = GetCurrentGameState();
|
||||
vector<PlayerInfoProto> protos{};
|
||||
for (const auto *const piFB : *currentGameState->player_infos()) {
|
||||
protos.push_back(fb::ToPlayerInfoProto(piFB));
|
||||
@@ -182,18 +188,18 @@ public:
|
||||
return protos;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetGameStatus() const
|
||||
-> const net::eagle0::shardok::storage::fb::GameStatus * {
|
||||
auto GetGameStatus() const -> const net::eagle0::shardok::storage::fb::GameStatus * {
|
||||
return GetCurrentGameState()->status();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetGameSettings() const -> GameSettingsSPtr { return gameSettings; }
|
||||
auto GetGameSettings() const -> GameSettingsSPtr { return gameSettings; }
|
||||
|
||||
static inline auto GameIsOver(const fb::GameStatus *status) -> bool {
|
||||
static inline auto GameIsOver(const net::eagle0::shardok::storage::fb::GameStatus *status)
|
||||
-> bool {
|
||||
return (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto GameIsOver() const -> bool { return GameIsOver(GetGameStatus()); }
|
||||
inline auto GameIsOver() const -> bool { return GameIsOver(GetGameStatus()); }
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -10,10 +10,14 @@
|
||||
#define MeteorCastActionFactory_hpp
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokAction.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
|
||||
class MeteorCastActionFactory {
|
||||
private:
|
||||
const SettingsGetter settings;
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ auto PlayerSetupCommandFactory::AddAvailablePlaceAndHideUnitCommandsForOneUnit(
|
||||
|
||||
CoordsSet unusedStartingPositions(gameState->hex_map());
|
||||
for (const Coords *possiblePosition : *thisUnitStartingPositions) {
|
||||
if (!gameState.GetOccupant(*possiblePosition)) {
|
||||
if (!Occupant(gameState->units(), *possiblePosition)) {
|
||||
unusedStartingPositions.Add(*possiblePosition);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ auto PlayerSetupCommandFactory::AddAvailablePlaceAndHideUnitCommandsForOneUnit(
|
||||
CoordsSet unusedHidingPositions(gameState->hex_map());
|
||||
|
||||
for (const Coords &possibleHidingPosition : GetAllCoords(gameState->hex_map())) {
|
||||
if (!gameState.GetOccupant(possibleHidingPosition)) {
|
||||
if (!Occupant(gameState->units(), possibleHidingPosition)) {
|
||||
const Terrain *terrain = GetTerrain(gameState->hex_map(), possibleHidingPosition);
|
||||
if (AllowsHiding(terrain)) { unusedHidingPositions.Add(possibleHidingPosition); }
|
||||
}
|
||||
|
||||
+3
-1
@@ -5,12 +5,14 @@
|
||||
#ifndef EAGLE0_PLAYERSETUPCOMMANDFACTORY_HPP
|
||||
#define EAGLE0_PLAYERSETUPCOMMANDFACTORY_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
class PlayerSetupCommandFactory {
|
||||
|
||||
+3
-1
@@ -5,11 +5,13 @@
|
||||
#ifndef EAGLE0_UNDEADCHANGEACTIONFACTORY_HPP
|
||||
#define EAGLE0_UNDEADCHANGEACTIONFACTORY_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokAction.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
|
||||
class UndeadChangeActionFactory {
|
||||
private:
|
||||
|
||||
+54
-106
@@ -4,7 +4,6 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistances.hpp"
|
||||
|
||||
#include <queue>
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
|
||||
@@ -32,22 +31,23 @@ auto ActionPointDistances::BraveWaterPossibleCoords(const HexMap* hexMap) const
|
||||
std::shared_ptr<BraveableTileInfo> info = std::make_shared<BraveableTileInfo>(hexMap);
|
||||
CoordsSet& braveWaterPossibleCoords = info->cs;
|
||||
const int indexCount = hexMap->row_count() * hexMap->column_count();
|
||||
const auto indexToCoords = CreateIndexToCoords(hexMap);
|
||||
for (int i = 0; i < indexCount; i++) {
|
||||
if (const Terrain* terrain = hexMap->terrain()->Get(i);
|
||||
IsWater(terrain->type()) && !terrain->modifier().bridge().present() &&
|
||||
const Terrain* terrain = hexMap->terrain()->Get(i);
|
||||
if (IsWater(terrain->type()) && !terrain->modifier().bridge().present() &&
|
||||
!terrain->modifier().ice().present()) {
|
||||
const Coords& currentCoords = indexToCoords[i];
|
||||
const Coords currentCoords =
|
||||
Coords(int8_t(i / hexMap->column_count()), int8_t(i % hexMap->column_count()));
|
||||
|
||||
const CoordsSet adjacentToWaterCoords =
|
||||
HexMapUtils::GetAdjacentCoords(hexMap, currentCoords);
|
||||
CoordsSet braveableTo(hexMap);
|
||||
for (const Coords& adjacentToWater : adjacentToWaterCoords) {
|
||||
for (auto acrossWaterCoords = GetTilesAcrossWater(hexMap, adjacentToWater);
|
||||
const auto& braveCoords : acrossWaterCoords) {
|
||||
auto acrossWaterCoords = GetTilesAcrossWater(hexMap, adjacentToWater);
|
||||
|
||||
for (const auto& braveCoords : acrossWaterCoords) {
|
||||
const auto braveIndex = ToIndex(braveCoords);
|
||||
if (const auto* swimTerrain = hexMap->terrain()->Get(braveIndex);
|
||||
IsWater(swimTerrain->type()) ||
|
||||
const auto* swimTerrain = hexMap->terrain()->Get(braveIndex);
|
||||
if (IsWater(swimTerrain->type()) ||
|
||||
swimTerrain->type() ==
|
||||
net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN) {
|
||||
continue;
|
||||
@@ -68,86 +68,46 @@ auto ActionPointDistances::BraveWaterPossibleCoords(const HexMap* hexMap) const
|
||||
return info;
|
||||
}
|
||||
|
||||
auto ActionPointDistances::CreateIndexToCoords(const HexMap* hexMap) -> vector<Coords> {
|
||||
const int8_t columnCount = hexMap->column_count();
|
||||
const int indexCount = hexMap->row_count() * columnCount;
|
||||
vector<Coords> indexToCoords;
|
||||
indexToCoords.reserve(indexCount);
|
||||
for (int i = 0; i < indexCount; i++) {
|
||||
indexToCoords.emplace_back(
|
||||
static_cast<int8_t>(i / columnCount),
|
||||
static_cast<int8_t>(i % columnCount));
|
||||
}
|
||||
return indexToCoords;
|
||||
}
|
||||
|
||||
auto ActionPointDistances::CreateAdjacencyTable(const HexMap* hexMap)
|
||||
-> vector<std::array<int, 6>> {
|
||||
const int8_t columnCount = hexMap->column_count();
|
||||
const int indexCount = hexMap->row_count() * columnCount;
|
||||
vector<std::array<int, 6>> adjacencyTable;
|
||||
adjacencyTable.reserve(indexCount);
|
||||
for (int i = 0; i < indexCount; i++) {
|
||||
const Coords coords(
|
||||
static_cast<int8_t>(i / columnCount),
|
||||
static_cast<int8_t>(i % columnCount));
|
||||
const CoordsSet adjacentCoords = HexMapUtils::GetAdjacentCoords(hexMap, coords);
|
||||
std::array<int, 6> neighbors{};
|
||||
neighbors.fill(-1); // -1 indicates invalid/no neighbor
|
||||
int neighborIdx = 0;
|
||||
for (const auto adjacentIndex : adjacentCoords.indexIterator()) {
|
||||
if (neighborIdx < 6) { neighbors[neighborIdx++] = static_cast<int>(adjacentIndex); }
|
||||
}
|
||||
adjacencyTable.push_back(neighbors);
|
||||
}
|
||||
|
||||
return adjacencyTable;
|
||||
}
|
||||
|
||||
void ActionPointDistances::PopulateOne(
|
||||
vector<DIST_T>& ds,
|
||||
const HexMap* hexMap,
|
||||
const bool includeBravingWater,
|
||||
const int braveWaterCost,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
const std::shared_ptr<BraveableTileInfo>& braveWaterPossibleCoords,
|
||||
const vector<Coords>& indexToCoords,
|
||||
const vector<std::array<int, 6>>& adjacencyTable) {
|
||||
const std::shared_ptr<BraveableTileInfo>& braveableTileInfo) {
|
||||
vector<uint8_t> visited(ds.size());
|
||||
|
||||
// Priority queue for efficient minimum selection: {distance, index}
|
||||
std::priority_queue<std::pair<DIST_T, int>, std::vector<std::pair<DIST_T, int>>, std::greater<>>
|
||||
pq;
|
||||
const int indexCount = hexMap->row_count() * hexMap->column_count();
|
||||
|
||||
// Find starting index (the one with distance 0)
|
||||
for (int i = 0; i < static_cast<int>(ds.size()); i++) {
|
||||
if (ds[i] == 0) {
|
||||
pq.emplace(0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
int firstUnvisitedIndex = 0;
|
||||
while (true) {
|
||||
// Choose the current index
|
||||
int currentIndex = -1;
|
||||
int currentIndexDistance = IMPOSSIBLE;
|
||||
|
||||
while (!pq.empty()) {
|
||||
auto [currentIndexDistance, currentIndex] = pq.top();
|
||||
pq.pop();
|
||||
|
||||
// Skip if already visited (can happen due to multiple insertions)
|
||||
if (visited[currentIndex]) continue;
|
||||
|
||||
// Skip if we found a better path since insertion
|
||||
if (currentIndexDistance > ds[currentIndex]) continue;
|
||||
|
||||
const Coords& currentCoords = indexToCoords[currentIndex];
|
||||
visited[currentIndex] = true;
|
||||
|
||||
// Prefetch terrain data for all neighbors to reduce memory stalls
|
||||
const std::array<int, 6>& neighbors = adjacencyTable[currentIndex];
|
||||
for (int i = 0; i < 6 && neighbors[i] != -1; i++) {
|
||||
__builtin_prefetch(hexMap->terrain()->Get(neighbors[i]), 0, 3);
|
||||
bool foundUnvisited = false;
|
||||
for (int toIndex = firstUnvisitedIndex; toIndex < indexCount; toIndex++) {
|
||||
if (!visited[toIndex]) {
|
||||
if (!foundUnvisited) {
|
||||
firstUnvisitedIndex = toIndex;
|
||||
foundUnvisited = true;
|
||||
}
|
||||
if (ds[toIndex] != IMPOSSIBLE &&
|
||||
(currentIndex == -1 || ds[toIndex] < currentIndexDistance)) {
|
||||
currentIndex = toIndex;
|
||||
currentIndexDistance = ds[toIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int adjacentIndex : neighbors) {
|
||||
if (adjacentIndex == -1) break; // End of valid neighbors
|
||||
if (currentIndex == -1 || currentIndexDistance == IMPOSSIBLE) return;
|
||||
|
||||
const Coords currentCoords =
|
||||
Coords(int8_t(currentIndex / hexMap->column_count()),
|
||||
int8_t(currentIndex % hexMap->column_count()));
|
||||
|
||||
const CoordsSet adjacentCoords = HexMapUtils::GetAdjacentCoords(hexMap, currentCoords);
|
||||
for (const auto adjacentIndex : adjacentCoords.indexIterator()) {
|
||||
if (visited[adjacentIndex]) continue;
|
||||
|
||||
const auto adjacentTerrain = hexMap->terrain()->Get(adjacentIndex);
|
||||
@@ -156,47 +116,40 @@ void ActionPointDistances::PopulateOne(
|
||||
if (adjacentCost.type == ActionCost::impossible) continue;
|
||||
|
||||
const auto costThroughCurrentTile = currentIndexDistance + adjacentCost.points;
|
||||
if (const int currentBestDistance = ds[adjacentIndex];
|
||||
currentBestDistance > costThroughCurrentTile) {
|
||||
const int currentBestDistance = ds[adjacentIndex];
|
||||
if (currentBestDistance > costThroughCurrentTile) {
|
||||
ds[adjacentIndex] = static_cast<DIST_T>(costThroughCurrentTile);
|
||||
// Add to priority queue for future processing
|
||||
pq.emplace(ds[adjacentIndex], adjacentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// check for swimmable tiles
|
||||
if (includeBravingWater && battalionType->allowsBraveWater &&
|
||||
braveWaterPossibleCoords->cs.Contains(currentCoords)) {
|
||||
auto entry = std::ranges::find_if(
|
||||
braveWaterPossibleCoords->details,
|
||||
braveableTileInfo->cs.Contains(currentCoords)) {
|
||||
auto entry = std::find_if(
|
||||
braveableTileInfo->details.begin(),
|
||||
braveableTileInfo->details.end(),
|
||||
[currentCoords](const BraveableTileInfo::BraveableFromInfo& from) {
|
||||
return from.from == currentCoords;
|
||||
});
|
||||
if (entry != braveWaterPossibleCoords->details.end()) {
|
||||
// Prefetch terrain data for water braving targets
|
||||
for (const auto braveIndex : entry->to.indexIterator()) {
|
||||
__builtin_prefetch(hexMap->terrain()->Get(braveIndex), 0, 3);
|
||||
}
|
||||
|
||||
if (entry != braveableTileInfo->details.end()) {
|
||||
for (const auto braveIndex : entry->to.indexIterator()) {
|
||||
if (visited[braveIndex]) continue;
|
||||
|
||||
const auto* swimTerrain = hexMap->terrain()->Get(braveIndex);
|
||||
const auto adjacentCost = battalionType->GetCostToEnterTerrain(swimTerrain);
|
||||
|
||||
if (const auto adjacentCost = battalionType->GetCostToEnterTerrain(swimTerrain);
|
||||
adjacentCost.type == ActionCost::impossible)
|
||||
continue;
|
||||
if (adjacentCost.type == ActionCost::impossible) continue;
|
||||
|
||||
const auto costThroughCurrentTile = currentIndexDistance + braveWaterCost;
|
||||
if (const int currentBestDistance = ds[braveIndex];
|
||||
currentBestDistance > costThroughCurrentTile) {
|
||||
ds[braveIndex] = static_cast<DIST_T>(costThroughCurrentTile);
|
||||
// Add to priority queue for future processing
|
||||
pq.push({ds[braveIndex], braveIndex});
|
||||
const int currentBestDistance = ds[braveIndex];
|
||||
if (currentBestDistance > costThroughCurrentTile) {
|
||||
ds[braveIndex] = DIST_T(costThroughCurrentTile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visited[currentIndex] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,19 +166,13 @@ auto ActionPointDistances::GenerateDistances(
|
||||
ds[fromIndex] = 0;
|
||||
|
||||
if (!includeBravingWater || battalionType->allowsBraveWater) {
|
||||
// Create lookup tables once per distance calculation
|
||||
const auto indexToCoords = CreateIndexToCoords(hexMap);
|
||||
const auto adjacencyTable = CreateAdjacencyTable(hexMap);
|
||||
|
||||
PopulateOne(
|
||||
ds,
|
||||
hexMap,
|
||||
includeBravingWater,
|
||||
braveWaterCost,
|
||||
battalionType,
|
||||
braveWaterPossibleCoords,
|
||||
indexToCoords,
|
||||
adjacencyTable);
|
||||
braveWaterPossibleCoords);
|
||||
}
|
||||
|
||||
return ds;
|
||||
@@ -239,7 +186,7 @@ OnDemandActionPointDistances::OnDemandActionPointDistances(
|
||||
: ActionPointDistances(map->column_count()),
|
||||
hexMap(fb::CopyHexMap(map)),
|
||||
battalionType(std::move(battTp)),
|
||||
distances(static_cast<size_t>(map->row_count() * map->column_count())) {
|
||||
distances(map->row_count() * map->column_count()) {
|
||||
const int indexCount = map->row_count() * map->column_count();
|
||||
distances.resize(indexCount);
|
||||
|
||||
@@ -249,6 +196,7 @@ OnDemandActionPointDistances::OnDemandActionPointDistances(
|
||||
distances[fromIndex] = std::async(
|
||||
std::launch::deferred,
|
||||
&OnDemandActionPointDistances::GenerateDistances,
|
||||
this,
|
||||
fromIndex,
|
||||
hexMap,
|
||||
includeBravingWater,
|
||||
|
||||
+18
-19
@@ -6,7 +6,9 @@
|
||||
#define EAGLE0_ACTIONPOINTDISTANCES_HPP
|
||||
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/BattalionType.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
@@ -34,30 +36,22 @@ private:
|
||||
protected:
|
||||
struct BraveableTileInfo;
|
||||
|
||||
static void PopulateOne(
|
||||
void PopulateOne(
|
||||
vector<DIST_T> &ds,
|
||||
const net::eagle0::shardok::storage::fb::HexMap *hexMap,
|
||||
bool includeBravingWater,
|
||||
int braveWaterCost,
|
||||
const BattalionTypeSPtr &battalionType,
|
||||
const std::shared_ptr<BraveableTileInfo> &braveWaterPossibleCoords,
|
||||
const vector<Coords> &indexToCoords,
|
||||
const vector<std::array<int, 6>> &adjacencyTable);
|
||||
static auto GenerateDistances(
|
||||
const std::shared_ptr<BraveableTileInfo> &braveWaterPossibleCoords);
|
||||
auto GenerateDistances(
|
||||
int fromIndex,
|
||||
const HexMap *hexMap,
|
||||
bool includeBravingWater,
|
||||
int braveWaterCost,
|
||||
const BattalionTypeSPtr &battalionType,
|
||||
const std::shared_ptr<BraveableTileInfo> &braveWaterPossibleCoords) -> vector<DIST_T>;
|
||||
const std::shared_ptr<BraveableTileInfo> &braveableTileInfo) -> vector<DIST_T>;
|
||||
auto BraveWaterPossibleCoords(const HexMap *hexMap) const -> std::shared_ptr<BraveableTileInfo>;
|
||||
|
||||
// Create coordinate lookup table for efficient index->coords conversion
|
||||
static auto CreateIndexToCoords(const HexMap *hexMap) -> vector<Coords>;
|
||||
|
||||
// Create adjacency lookup table for efficient neighbor access
|
||||
static auto CreateAdjacencyTable(const HexMap *hexMap) -> vector<std::array<int, 6>>;
|
||||
|
||||
[[nodiscard]] auto ToIndex(const Coords &coords) const -> int {
|
||||
return coords.row() * column_count + coords.column();
|
||||
}
|
||||
@@ -69,32 +63,37 @@ public:
|
||||
|
||||
virtual ~ActionPointDistances() = default;
|
||||
|
||||
virtual auto Distance(int fromIndex, int toIndex) const -> DIST_T = 0;
|
||||
virtual auto Distance(int fromIndex, int toIndex) -> DIST_T = 0;
|
||||
|
||||
virtual auto Distance(const Coords &from, const Coords &to) const -> DIST_T = 0;
|
||||
virtual auto Distance(const Coords &from, const Coords &to) -> DIST_T = 0;
|
||||
};
|
||||
|
||||
class OnDemandActionPointDistances final : public ActionPointDistances {
|
||||
class OnDemandActionPointDistances : public ActionPointDistances {
|
||||
private:
|
||||
const HexMapW hexMap;
|
||||
const BattalionTypeSPtr battalionType;
|
||||
|
||||
vector<shared_future<vector<int16_t>>> distances;
|
||||
|
||||
static void fill(
|
||||
vector<std::unordered_map<size_t, std::shared_ptr<ActionPointDistances>>> &vec) {
|
||||
for (int i = 0; i < 6; i++) { vec.emplace_back(); }
|
||||
}
|
||||
|
||||
public:
|
||||
explicit OnDemandActionPointDistances(
|
||||
const HexMap *map,
|
||||
BattalionTypeSPtr battTp,
|
||||
BattalionTypeSPtr battalionType,
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost = -1);
|
||||
|
||||
~OnDemandActionPointDistances() override = default;
|
||||
~OnDemandActionPointDistances() override{};
|
||||
|
||||
auto Distance(const int fromIndex, const int toIndex) const -> int16_t override {
|
||||
auto Distance(const int fromIndex, const int toIndex) -> int16_t override {
|
||||
return distances[fromIndex].get()[toIndex];
|
||||
}
|
||||
|
||||
auto Distance(const Coords &from, const Coords &to) const -> int16_t override {
|
||||
auto Distance(const Coords &from, const Coords &to) -> int16_t override {
|
||||
return Distance(ToIndex(from), ToIndex(to));
|
||||
}
|
||||
};
|
||||
|
||||
+29
-206
@@ -4,110 +4,23 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
|
||||
|
||||
#define CACHE_STATS_LOGGING_ false
|
||||
#define CACHE_STATS_FREQUENCY_SECONDS_ 1
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Thread-local cache definition - stores raw pointers for zero overhead access
|
||||
thread_local ActionPointDistancesCache::TLSCache ActionPointDistancesCache::tlsCache;
|
||||
|
||||
#if CACHE_STATS_LOGGING_
|
||||
// Thread-local statistics for performance monitoring
|
||||
thread_local struct {
|
||||
int persistentHits = 0;
|
||||
int persistentMisses = 0;
|
||||
int localHits = 0;
|
||||
int localMisses = 0;
|
||||
int sharedAccesses = 0;
|
||||
int evictionEvents = 0;
|
||||
int apdLoadedFromFile = 0;
|
||||
int apdGeneratedFresh = 0;
|
||||
std::chrono::steady_clock::time_point lastReportTime = std::chrono::steady_clock::now();
|
||||
} cacheStats;
|
||||
|
||||
// Helper function to print stats periodically
|
||||
static void MaybePrintCacheStats() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (std::chrono::duration_cast<std::chrono::seconds>(now - cacheStats.lastReportTime).count() >=
|
||||
CACHE_STATS_FREQUENCY_SECONDS_) {
|
||||
printf("Thread cache stats: %d persistent hits, %d persistent misses, %d local hits, "
|
||||
"%d local misses, %d shared accesses, %d eviction events, "
|
||||
"%d APD loaded from file, %d APD generated fresh\n",
|
||||
cacheStats.persistentHits,
|
||||
cacheStats.persistentMisses,
|
||||
cacheStats.localHits,
|
||||
cacheStats.localMisses,
|
||||
cacheStats.sharedAccesses,
|
||||
cacheStats.evictionEvents,
|
||||
cacheStats.apdLoadedFromFile,
|
||||
cacheStats.apdGeneratedFresh);
|
||||
cacheStats.lastReportTime = now;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
class BadHashException final : public std::exception {
|
||||
class BadHashException : public std::exception {
|
||||
public:
|
||||
BadHashException() = default;
|
||||
BadHashException() : std::exception() {}
|
||||
|
||||
[[nodiscard]] auto what() const noexcept -> const char* override { return "Bad map hash!"; };
|
||||
};
|
||||
|
||||
// Helper function to check if any ice is present on the map
|
||||
static auto HasIceOnMap(const HexMap* map) -> bool {
|
||||
return std::ranges::any_of(*map->terrain(), [](const auto* terrain) {
|
||||
return terrain->modifier().ice().present();
|
||||
});
|
||||
}
|
||||
constexpr int kBattalionTypeCount = 6;
|
||||
|
||||
// Helper function to create a copy of the map with all ice removed
|
||||
// This ensures AI pathfinding treats ice as impassable water
|
||||
// This should only be called if ice is present on the map
|
||||
static auto CreateIceClearedMap(const HexMap* map) -> fb::HexMapW {
|
||||
using namespace flatbuffers;
|
||||
using namespace net::eagle0::shardok::storage::fb;
|
||||
|
||||
// First, create a full copy using the efficient memcpy approach
|
||||
auto mapCopy = fb::CopyHexMap(map);
|
||||
|
||||
// Now modify the ice on the mutable copy
|
||||
auto* mutableMap = mapCopy.Get();
|
||||
const auto* terrainVec = mutableMap->mutable_terrain();
|
||||
|
||||
for (size_t i = 0; i < terrainVec->size(); i++) {
|
||||
// Only process tiles with ice
|
||||
if (auto* terrain = terrainVec->GetMutableObject(i); terrain->modifier().ice().present()) {
|
||||
terrain->mutable_modifier().mutable_ice().mutate_present(false);
|
||||
terrain->mutable_modifier().mutable_ice().mutate_integrity(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Recompute the modifier hash using the canonical function
|
||||
// This ensures consistency with the standard hash computation
|
||||
mutableMap->mutate_modifier_hash(GetModifierHash(mutableMap));
|
||||
|
||||
return mapCopy;
|
||||
}
|
||||
|
||||
auto ActionPointDistancesCache::MakeCacheKey(
|
||||
const MapId& mapId,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
const bool includeBravingWater,
|
||||
const int braveWaterActionPointCost) -> FullCacheKey {
|
||||
return FullCacheKey{
|
||||
mapId,
|
||||
static_cast<int>(battalionType->typeId),
|
||||
includeBravingWater,
|
||||
braveWaterActionPointCost >= 0 ? braveWaterActionPointCost : 0};
|
||||
ActionPointDistancesCache::ActionPointDistancesCache() {
|
||||
bravingDistances.resize(kBattalionTypeCount);
|
||||
noBravingDistances.resize(kBattalionTypeCount);
|
||||
}
|
||||
|
||||
auto ActionPointDistancesCache::GetMapId(const HexMap* map) -> MapId {
|
||||
@@ -116,128 +29,38 @@ auto ActionPointDistancesCache::GetMapId(const HexMap* map) -> MapId {
|
||||
|
||||
return MapId{.terrainTypesId = map->base_hash(), .modifierId = modifierId};
|
||||
}
|
||||
void ActionPointDistancesCache::ConsolidateThreadLocalCache_Racy() {
|
||||
persistentCache.insert(std::begin(sharedDistances), std::end(sharedDistances));
|
||||
sharedDistances.clear();
|
||||
|
||||
// Clear the current thread's cache since persistent cache now has everything
|
||||
tlsCache.clear();
|
||||
}
|
||||
|
||||
auto ActionPointDistancesCache::GetRaw(
|
||||
auto ActionPointDistancesCache::Get(
|
||||
const HexMap* map,
|
||||
const MapId& mapId,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
const bool includeBravingWater,
|
||||
const int braveWaterActionPointCost) -> const ActionPointDistances* {
|
||||
// Create cache key first - check cache before expensive ice-clearing operation
|
||||
auto cacheKey =
|
||||
MakeCacheKey(mapId, battalionType, includeBravingWater, braveWaterActionPointCost);
|
||||
const int braveWaterActionPointCost) -> std::shared_ptr<ActionPointDistances> {
|
||||
auto& vec = includeBravingWater ? bravingDistances : noBravingDistances;
|
||||
auto& distancesMap = vec[battalionType->typeId];
|
||||
|
||||
// Check the persistent map first
|
||||
if (auto persistentIt = persistentCache.find(cacheKey); persistentIt != persistentCache.end()) {
|
||||
#if CACHE_STATS_LOGGING_
|
||||
cacheStats.persistentHits++;
|
||||
MaybePrintCacheStats();
|
||||
#endif
|
||||
// Return directly from persistent cache without TLS insertion
|
||||
// This avoids the overhead of thread-local storage operations on hot path
|
||||
return persistentIt->second.rawPtr;
|
||||
shared_ptr<ActionPointDistances> toReturn;
|
||||
|
||||
if (distancesMap.if_contains(mapId, [&toReturn](const auto& kv) { toReturn = kv.second; })) {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
#if CACHE_STATS_LOGGING_
|
||||
cacheStats.persistentMisses++;
|
||||
#endif
|
||||
distancesMap.lazy_emplace_l(
|
||||
mapId,
|
||||
[&toReturn](const auto& kv) { toReturn = kv.second; },
|
||||
[=, &toReturn](const auto& ctor) {
|
||||
auto newDistances = std::make_shared<FixedActionPointDistances>(
|
||||
map,
|
||||
mapId.terrainTypesId,
|
||||
mapId.modifierId,
|
||||
battalionType,
|
||||
includeBravingWater,
|
||||
braveWaterActionPointCost);
|
||||
ctor(mapId, newDistances);
|
||||
toReturn = newDistances;
|
||||
});
|
||||
|
||||
// Check thread-local cache first (no locks needed!)
|
||||
if (auto localIt = tlsCache.find(cacheKey); localIt != tlsCache.end()) {
|
||||
#if CACHE_STATS_LOGGING_
|
||||
cacheStats.localHits++;
|
||||
MaybePrintCacheStats();
|
||||
#endif
|
||||
return localIt->second.rawPtr; // Raw pointer - zero overhead access!
|
||||
}
|
||||
|
||||
#if CACHE_STATS_LOGGING_
|
||||
cacheStats.localMisses++;
|
||||
#endif
|
||||
|
||||
// Check shared cache before expensive ice-clearing operation
|
||||
shared_ptr<ActionPointDistances> sharedResult;
|
||||
if (sharedDistances.if_contains(cacheKey, [&sharedResult](const auto& kv) {
|
||||
sharedResult = kv.second;
|
||||
})) {
|
||||
#if CACHE_STATS_LOGGING_
|
||||
cacheStats.sharedAccesses++;
|
||||
#endif
|
||||
// Cache hit in shared cache - store in thread-local cache and return
|
||||
tlsCache.emplace(cacheKey, CacheEntry(sharedResult));
|
||||
#if CACHE_STATS_LOGGING_
|
||||
MaybePrintCacheStats();
|
||||
#endif
|
||||
return sharedResult.get();
|
||||
}
|
||||
|
||||
// Cache miss in both caches - need to create ice-cleared map for pathfinding computation
|
||||
const bool hasIce = HasIceOnMap(map);
|
||||
|
||||
// Declaring here to keep the copied map in scope
|
||||
const HexMap* mapToUse = map;
|
||||
|
||||
if (hasIce) {
|
||||
// Create ice-cleared map for pathfinding
|
||||
// This prevents AI from considering ice as a valid path toward enemies
|
||||
fb::HexMapW iceClearedMap = CreateIceClearedMap(map);
|
||||
mapToUse = iceClearedMap.Get();
|
||||
}
|
||||
|
||||
// Create new pathfinding result using factory method
|
||||
auto creationResult = FixedActionPointDistances::Create(
|
||||
mapToUse,
|
||||
mapId.terrainTypesId,
|
||||
mapId.modifierId,
|
||||
battalionType,
|
||||
includeBravingWater,
|
||||
braveWaterActionPointCost);
|
||||
|
||||
#if CACHE_STATS_LOGGING_
|
||||
// Track whether this was loaded from file or generated fresh
|
||||
if (creationResult.loadedFromFile) {
|
||||
cacheStats.apdLoadedFromFile++;
|
||||
} else {
|
||||
cacheStats.apdGeneratedFresh++;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto result = creationResult.apd;
|
||||
|
||||
// Store in shared cache
|
||||
sharedDistances.lazy_emplace_l(
|
||||
cacheKey,
|
||||
[](const auto& kv) { /* already checked above */ },
|
||||
[=](const auto& ctor) { ctor(cacheKey, result); });
|
||||
|
||||
// Cache result locally for future lookups by this thread
|
||||
// Store both shared_ptr and raw pointer for hybrid access
|
||||
tlsCache.emplace(cacheKey, CacheEntry(result));
|
||||
|
||||
// Prevent unbounded cache growth - limit to reasonable size
|
||||
if (tlsCache.size() > 100) {
|
||||
// Simple eviction: clear half the cache when it gets too large
|
||||
|
||||
#if CACHE_STATS_LOGGING_
|
||||
cacheStats.evictionEvents++;
|
||||
#endif
|
||||
auto it = tlsCache.begin();
|
||||
std::advance(it, tlsCache.size() / 2);
|
||||
tlsCache.erase(tlsCache.begin(), it);
|
||||
}
|
||||
|
||||
return result.get();
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
void ActionPointDistancesCache::ClearThreadLocalCache() { tlsCache.clear(); }
|
||||
|
||||
size_t ActionPointDistancesCache::GetThreadLocalCacheSize() { return tlsCache.size(); }
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
+19
-83
@@ -5,14 +5,11 @@
|
||||
#ifndef EAGLE0_ACTIONPOINTDISTANCESCACHE_HPP
|
||||
#define EAGLE0_ACTIONPOINTDISTANCESCACHE_HPP
|
||||
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistances.hpp"
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wthread-safety-analysis"
|
||||
#pragma GCC diagnostic ignored "-Wunused-result"
|
||||
#include <gtl/phmap.hpp>
|
||||
#include "parallel_hashmap/phmap.h"
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
@@ -20,106 +17,45 @@ namespace shardok {
|
||||
using std::shared_ptr;
|
||||
|
||||
struct MapId {
|
||||
uint64_t terrainTypesId;
|
||||
uint64_t modifierId;
|
||||
int64_t terrainTypesId;
|
||||
int64_t modifierId;
|
||||
|
||||
friend size_t hash_value(const MapId& id) {
|
||||
return phmap::HashState::combine(0, id.terrainTypesId, id.modifierId);
|
||||
}
|
||||
|
||||
auto operator==(const MapId& other) const -> bool {
|
||||
return terrainTypesId == other.terrainTypesId && modifierId == other.modifierId;
|
||||
}
|
||||
};
|
||||
|
||||
// Unified cache key for both thread-safe and thread-local caches
|
||||
struct FullCacheKey {
|
||||
MapId mapId;
|
||||
int battalionTypeId;
|
||||
bool includeBravingWater;
|
||||
int braveWaterCost;
|
||||
|
||||
bool operator==(const FullCacheKey& other) const {
|
||||
return mapId == other.mapId && battalionTypeId == other.battalionTypeId &&
|
||||
includeBravingWater == other.includeBravingWater &&
|
||||
braveWaterCost == other.braveWaterCost;
|
||||
}
|
||||
};
|
||||
|
||||
// Hash function for FullCacheKey
|
||||
struct FullCacheKeyHash {
|
||||
size_t operator()(const FullCacheKey& key) const {
|
||||
// Pack small fields into a single 64-bit value
|
||||
uint64_t packed = (static_cast<uint64_t>(key.battalionTypeId) << 32) |
|
||||
(static_cast<uint64_t>(key.braveWaterCost) << 1) |
|
||||
(key.includeBravingWater ? 1 : 0);
|
||||
|
||||
// Hash MapId fields directly instead of going through hash_value(MapId)
|
||||
return gtl::HashState::combine(0, key.mapId.terrainTypesId, key.mapId.modifierId, packed);
|
||||
}
|
||||
};
|
||||
using APDKey = MapId;
|
||||
|
||||
class ActionPointDistancesCache {
|
||||
private:
|
||||
struct CacheEntry {
|
||||
shared_ptr<ActionPointDistances> sharedPtr;
|
||||
const ActionPointDistances* rawPtr;
|
||||
|
||||
explicit CacheEntry(shared_ptr<ActionPointDistances> ptr)
|
||||
: sharedPtr(std::move(ptr)),
|
||||
rawPtr(sharedPtr.get()) {}
|
||||
};
|
||||
|
||||
// Tier 1: persistent map. This is NOT safe to write to while reads may be happening.
|
||||
using PersistentMap = gtl::flat_hash_map<FullCacheKey, CacheEntry, FullCacheKeyHash>;
|
||||
|
||||
PersistentMap persistentCache;
|
||||
|
||||
using APDMap = gtl::parallel_flat_hash_map<
|
||||
FullCacheKey,
|
||||
using APDMap = phmap::parallel_flat_hash_map<
|
||||
APDKey,
|
||||
shared_ptr<ActionPointDistances>,
|
||||
FullCacheKeyHash,
|
||||
std::equal_to<FullCacheKey>,
|
||||
std::allocator<std::pair<const FullCacheKey, shared_ptr<ActionPointDistances>>>,
|
||||
6,
|
||||
phmap::priv::hash_default_hash<APDKey>,
|
||||
phmap::priv::hash_default_eq<APDKey>,
|
||||
std::allocator<std::pair<const APDKey, shared_ptr<ActionPointDistances>>>,
|
||||
4,
|
||||
std::mutex>;
|
||||
|
||||
APDMap sharedDistances;
|
||||
|
||||
using TLSCache = gtl::flat_hash_map<FullCacheKey, CacheEntry, FullCacheKeyHash>;
|
||||
static thread_local TLSCache tlsCache;
|
||||
|
||||
// Epoch system removed - TLS cache uses size-based eviction instead
|
||||
|
||||
// Helper to build cache key
|
||||
static auto MakeCacheKey(
|
||||
const MapId& mapId,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost) -> FullCacheKey;
|
||||
vector<APDMap> noBravingDistances;
|
||||
vector<APDMap> bravingDistances;
|
||||
|
||||
public:
|
||||
explicit ActionPointDistancesCache() {
|
||||
// Pre-size persistent cache to reduce hash collisions
|
||||
// Estimate: ~12 entries from pre-fetching + ~50-100 entries during gameplay
|
||||
persistentCache.reserve(128);
|
||||
}
|
||||
explicit ActionPointDistancesCache();
|
||||
|
||||
// Returns raw pointer for zero overhead access
|
||||
// Lifetime guaranteed by shared cache ownership
|
||||
auto GetRaw(
|
||||
auto Get(
|
||||
const HexMap* map,
|
||||
const MapId& mapId,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost = -1) -> const ActionPointDistances*;
|
||||
int braveWaterActionPointCost = -1) -> shared_ptr<ActionPointDistances>;
|
||||
|
||||
static auto GetMapId(const HexMap* map) -> MapId;
|
||||
|
||||
// Consolidate the thread-safe cache into the persistent cache and clear
|
||||
// the current thread's local cache. This is only safe if we know reads
|
||||
// are not happening from other threads.
|
||||
void ConsolidateThreadLocalCache_Racy();
|
||||
|
||||
// Cache management methods
|
||||
static void ClearThreadLocalCache();
|
||||
static size_t GetThreadLocalCacheSize();
|
||||
};
|
||||
|
||||
using APDCache = shared_ptr<ActionPointDistancesCache>;
|
||||
|
||||
@@ -30,7 +30,7 @@ cc_library(
|
||||
":fixed_action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:hex_map_hasher",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
|
||||
"@gtl",
|
||||
"@parallel_hashmap",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+28
-52
@@ -4,18 +4,9 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
|
||||
// Dynamic thread count based on hardware capabilities
|
||||
static const int ASYNC_COUNT = []() {
|
||||
const int cores = static_cast<int>(std::thread::hardware_concurrency());
|
||||
// Use cores-2 to leave room for OS and other processes, minimum 4 threads
|
||||
const int threadCount = std::max(4, cores - 4);
|
||||
printf("ActionPointDistances using %d threads (detected %d cores)\n", threadCount, cores);
|
||||
return threadCount;
|
||||
}();
|
||||
static constexpr int ASYNC_COUNT = 8;
|
||||
|
||||
namespace shardok {
|
||||
|
||||
@@ -26,24 +17,14 @@ void FixedActionPointDistances::SetCacheDirectory(const string& newDir) {
|
||||
|
||||
static thread_local byte_vector _scratch;
|
||||
|
||||
FixedActionPointDistances::FixedActionPointDistances(const HexMap* map, int columnCount)
|
||||
: ActionPointDistances(columnCount) {}
|
||||
|
||||
auto FixedActionPointDistances::Create(
|
||||
FixedActionPointDistances::FixedActionPointDistances(
|
||||
const HexMap* map,
|
||||
int64_t terrainTypesHash,
|
||||
int64_t modifierHash,
|
||||
const BattalionTypeSPtr& battalionType,
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost) -> CreationResult {
|
||||
// Create the object using private constructor
|
||||
auto apd = std::shared_ptr<FixedActionPointDistances>(
|
||||
new FixedActionPointDistances(map, map->column_count()));
|
||||
|
||||
CreationResult result;
|
||||
result.apd = apd;
|
||||
result.loadedFromFile = false;
|
||||
|
||||
int braveWaterActionPointCost)
|
||||
: ActionPointDistances(map->column_count()) {
|
||||
string path = "";
|
||||
|
||||
if (!cacheDirectory.empty()) {
|
||||
@@ -65,64 +46,59 @@ auto FixedActionPointDistances::Create(
|
||||
const int indexCount = map->row_count() * map->column_count();
|
||||
|
||||
if (!path.empty() && FilesystemUtils::FileExistsAtPath(path)) {
|
||||
apd->distances.resize(indexCount);
|
||||
distances.resize(indexCount);
|
||||
// load from file
|
||||
const auto& bytes = _scratch.ReplaceWithPath(path);
|
||||
const auto* ptr = reinterpret_cast<const DIST_T*>(bytes.data());
|
||||
|
||||
for (int fromIndex = 0; fromIndex < indexCount; fromIndex++) {
|
||||
apd->distances[fromIndex].insert(
|
||||
apd->distances[fromIndex].end(),
|
||||
&(ptr[0]),
|
||||
&(ptr[indexCount]));
|
||||
distances[fromIndex].insert(distances[fromIndex].end(), &(ptr[0]), &(ptr[indexCount]));
|
||||
ptr += indexCount;
|
||||
}
|
||||
result.loadedFromFile = true;
|
||||
} else {
|
||||
_scratch.reserve(indexCount * indexCount * sizeof(DIST_T));
|
||||
|
||||
vector<std::future<vector<vector<DIST_T>>>> futures(indexCount);
|
||||
|
||||
auto braveWaterPossibleCoords =
|
||||
includeBravingWater ? apd->BraveWaterPossibleCoords(map) : nullptr;
|
||||
includeBravingWater ? BraveWaterPossibleCoords(map) : nullptr;
|
||||
|
||||
int chunkSize = (indexCount + ASYNC_COUNT - 1) / ASYNC_COUNT;
|
||||
// Break into chunks for async
|
||||
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
|
||||
futures[chunkIdx] = std::async(std::launch::async, [=]() -> vector<vector<DIST_T>> {
|
||||
vector<vector<DIST_T>> chunkVec;
|
||||
chunkVec.reserve(chunkSize);
|
||||
const int chunkStartIndex = chunkIdx * chunkSize;
|
||||
futures[chunkIdx] =
|
||||
std::async(std::launch::async, [=, this]() -> vector<vector<DIST_T>> {
|
||||
vector<vector<DIST_T>> chunkVec;
|
||||
chunkVec.reserve(chunkSize);
|
||||
const int chunkStartIndex = chunkIdx * chunkSize;
|
||||
|
||||
for (int i = 0; i < chunkSize; i++) {
|
||||
const auto fromIndex = chunkStartIndex + i;
|
||||
if (fromIndex >= indexCount) { continue; }
|
||||
chunkVec.push_back(ActionPointDistances::GenerateDistances(
|
||||
fromIndex,
|
||||
map,
|
||||
includeBravingWater,
|
||||
braveWaterActionPointCost,
|
||||
battalionType,
|
||||
braveWaterPossibleCoords));
|
||||
}
|
||||
return chunkVec;
|
||||
});
|
||||
for (int i = 0; i < chunkSize; i++) {
|
||||
const auto fromIndex = chunkStartIndex + i;
|
||||
if (fromIndex >= indexCount) { continue; }
|
||||
chunkVec.push_back(GenerateDistances(
|
||||
fromIndex,
|
||||
map,
|
||||
includeBravingWater,
|
||||
braveWaterActionPointCost,
|
||||
battalionType,
|
||||
braveWaterPossibleCoords));
|
||||
}
|
||||
return chunkVec;
|
||||
});
|
||||
}
|
||||
|
||||
apd->distances.reserve(indexCount);
|
||||
distances.reserve(indexCount);
|
||||
_scratch.clear();
|
||||
_scratch.reserve(indexCount * indexCount * sizeof(DIST_T));
|
||||
|
||||
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
|
||||
auto resultsVec = futures[chunkIdx].get();
|
||||
apd->distances.insert(apd->distances.end(), resultsVec.begin(), resultsVec.end());
|
||||
distances.insert(distances.end(), resultsVec.begin(), resultsVec.end());
|
||||
for (const auto& r : resultsVec) { _scratch.append(r); }
|
||||
}
|
||||
|
||||
if (!path.empty()) { FilesystemUtils::AtomicallySaveToPath(path, _scratch); }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
+4
-16
@@ -17,43 +17,31 @@ using std::vector;
|
||||
using BattalionTypeSPtr = std::shared_ptr<const BattalionType>;
|
||||
|
||||
class FixedActionPointDistances final : public ActionPointDistances {
|
||||
public:
|
||||
struct CreationResult {
|
||||
std::shared_ptr<FixedActionPointDistances> apd;
|
||||
bool loadedFromFile;
|
||||
};
|
||||
|
||||
private:
|
||||
vector<vector<DIST_T>> distances;
|
||||
|
||||
inline static string cacheDirectory = "";
|
||||
|
||||
// Private constructor - use Create factory method instead
|
||||
explicit FixedActionPointDistances(const HexMap *map, int columnCount);
|
||||
|
||||
public:
|
||||
static void SetCacheDirectory(const string &newDir);
|
||||
|
||||
// Factory method to create FixedActionPointDistances with metadata
|
||||
static auto Create(
|
||||
explicit FixedActionPointDistances(
|
||||
const HexMap *map,
|
||||
int64_t terrainTypesHash,
|
||||
int64_t modifierHash,
|
||||
const BattalionTypeSPtr &battalionType,
|
||||
bool includeBravingWater,
|
||||
int braveWaterActionPointCost = -1) -> CreationResult;
|
||||
int braveWaterActionPointCost = -1);
|
||||
|
||||
~FixedActionPointDistances() override = default;
|
||||
|
||||
[[nodiscard]] auto Distance(const int fromIndex, const int toIndex) const -> DIST_T override {
|
||||
auto Distance(const int fromIndex, const int toIndex) -> DIST_T override {
|
||||
return distances[fromIndex][toIndex];
|
||||
}
|
||||
|
||||
[[nodiscard]] auto Distance(const Coords &from, const Coords &to) const -> DIST_T override {
|
||||
auto Distance(const Coords &from, const Coords &to) -> DIST_T override {
|
||||
return Distance(ToIndex(from), ToIndex(to));
|
||||
}
|
||||
|
||||
friend struct CreationResult;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
+2
-3
@@ -36,10 +36,9 @@ auto main(int argc, char** argv) -> int {
|
||||
|
||||
const auto start = system_clock::now();
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
for (int i = 0; i < 300000; i++) {
|
||||
for (const HexMapW& hexMap : hexMaps) {
|
||||
shardok::FixedActionPointDistances
|
||||
distances(hexMap, 0x1234, 0xABCD, battalionType, true, 5);
|
||||
shardok::FixedActionPointDistances distances(hexMap, 0x1234, battalionType, true, 5);
|
||||
}
|
||||
}
|
||||
const auto end = system_clock::now();
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Action Point Distances Performance Optimization Status
|
||||
|
||||
This document tracks the performance optimization work for the Shardok tactical combat pathfinding system.
|
||||
|
||||
## Current Implementation Status
|
||||
|
||||
The system uses **Dijkstra's algorithm** with significant optimizations implemented, achieving ~8x performance improvement over the original implementation.
|
||||
|
||||
## 🎯 **Next Steps - Remaining Optimization Opportunities**
|
||||
|
||||
### **1. Fibonacci Heap** ⚠️ **High Complexity**
|
||||
|
||||
**Expected:** 20-40% speedup on larger maps
|
||||
**Effort:** Very High (3-5 days)
|
||||
**Complexity:** Complex data structure with circular doubly-linked lists, cascading cuts, degree tracking
|
||||
|
||||
Replace `std::priority_queue` with Fibonacci heap for O(1) decrease-key operations vs O(log V).
|
||||
|
||||
### **2. d-ary Heap** ⚠️ **Simpler Alternative**
|
||||
|
||||
**Expected:** 10-20% potential speedup
|
||||
**Effort:** Low (1-2 hours)
|
||||
**Complexity:** Much simpler than Fibonacci heap
|
||||
|
||||
Use 4-ary or 8-ary heap for better cache performance compared to binary heap.
|
||||
|
||||
## Performance Projections
|
||||
|
||||
| Map Size | Original | Current | Remaining Potential | Final Target |
|
||||
|---------------------|---------------|--------------|---------------------|--------------|
|
||||
| 12×14 (168 tiles) | 14,196 ops | ~1,800 ops | ~1,400 ops | **10-12x** |
|
||||
| 24×28 (672 tiles) | 226,128 ops | ~28,000 ops | ~20,000 ops | **11-15x** |
|
||||
| 48×56 (2,688 tiles) | 3,612,516 ops | ~450,000 ops | ~250,000 ops | **14-20x** |
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### **Recommended Next Steps**
|
||||
|
||||
1. **d-ary heap** - Low effort, moderate potential gain
|
||||
2. **Fibonacci heap** - High effort, uncertain benefit for typical map sizes
|
||||
|
||||
### **Conclusion**
|
||||
|
||||
**Current optimizations have achieved the primary performance goals.** Further optimizations show diminishing returns due to the algorithm being memory-bound rather than compute-bound on typical map sizes.
|
||||
|
||||
## Memory Usage Impact
|
||||
|
||||
**Current:** ~168² × 2 bytes = 56KB per distance matrix
|
||||
|
||||
- 50-80% reduction in computation memory bandwidth
|
||||
- Better cache hit rates (80% → 95%+)
|
||||
- Reduced memory allocation churn from lookup tables
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
- All optimizations maintain the same public API
|
||||
- Cache file format unchanged
|
||||
- Thread safety preserved
|
||||
- No breaking changes to existing code
|
||||
+14
-54
@@ -153,60 +153,28 @@ auto ApplyResults(
|
||||
|
||||
void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result) {
|
||||
UnitId maxChangedUnitId = 0;
|
||||
bool needsVectorExpansion = false;
|
||||
bool needsReservedSlotConversion = false;
|
||||
|
||||
// First pass: check what kind of modifications we need
|
||||
for (const auto &unitBytes : result.changed_units_fb()) {
|
||||
const auto *unit = (Unit *)unitBytes.data();
|
||||
maxChangedUnitId = std::max(maxChangedUnitId, unit->unit_id());
|
||||
|
||||
if (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() ==
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
|
||||
// Unit wants to use a reserved slot
|
||||
needsReservedSlotConversion = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Early return if no modifications needed
|
||||
if (!needsVectorExpansion && !needsReservedSlotConversion) { return; }
|
||||
|
||||
// If we need to expand the vector, go straight to slow path
|
||||
if (needsVectorExpansion) {
|
||||
int unitsNeeded = 1 + maxChangedUnitId - mutatingState->units()->size();
|
||||
mutatingState = CopyWithExtraUnits(mutatingState, unitsNeeded);
|
||||
if (maxChangedUnitId < mutatingState->units()->size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, we just need to convert reserved slots (fast path)
|
||||
if (needsReservedSlotConversion) {
|
||||
// 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 = (Unit *)unitBytes.data();
|
||||
auto *mutableUnit = mutatingState->mutable_units()->GetMutableObject(unit->unit_id());
|
||||
if (mutableUnit->status() ==
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
|
||||
// Convert this reserved slot to a real unit
|
||||
mutableUnit->mutate_status(
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
// The calling code will set the specific values it needs
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mutatingState = CopyWithExtraUnits(
|
||||
mutatingState,
|
||||
1 + maxChangedUnitId - mutatingState->units()->size());
|
||||
}
|
||||
}
|
||||
|
||||
auto ApplyResult(
|
||||
GameStateW startingState,
|
||||
const GameStateW &startingState,
|
||||
const ActionResultProto &result,
|
||||
const SettingsGetter &settings) -> GameStateW {
|
||||
MutatingApplyResult(startingState, result, settings);
|
||||
return startingState;
|
||||
auto endGS = startingState;
|
||||
MutatingApplyResult(endGS, result, settings);
|
||||
|
||||
return endGS;
|
||||
}
|
||||
|
||||
void MutatingApplyResult(
|
||||
@@ -338,18 +306,10 @@ void MutatingApplyResult(
|
||||
settings);
|
||||
}
|
||||
|
||||
// Capture old position before applying changes
|
||||
auto *mutableUnit = mutatingGameState->units()->GetMutableObject(changedUnit->unit_id());
|
||||
const auto oldLocation = mutableUnit->location();
|
||||
|
||||
fb::ApplyUnit(mutableUnit, changedUnit, status);
|
||||
|
||||
// Update occupied tiles bitfield if position changed
|
||||
const auto &newLocation = changedUnit->location();
|
||||
if (oldLocation.row() != newLocation.row() ||
|
||||
oldLocation.column() != newLocation.column()) {
|
||||
mutatingGameState.UpdateOccupiedTile(oldLocation, newLocation);
|
||||
}
|
||||
fb::ApplyUnit(
|
||||
mutatingGameState->units()->GetMutableObject(changedUnit->unit_id()),
|
||||
changedUnit,
|
||||
status);
|
||||
|
||||
if (battalionSizeBefore != battalionSizeAfter) {
|
||||
if (changedUnit->battalion().type() ==
|
||||
|
||||
+4
-2
@@ -11,11 +11,13 @@
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
using ActionResultProto = net::eagle0::shardok::storage::ActionResult;
|
||||
|
||||
// flatbuffers
|
||||
@@ -24,7 +26,7 @@ void MutatingApplyResult(
|
||||
const ActionResultProto& actionResult,
|
||||
const SettingsGetter& settings);
|
||||
auto ApplyResult(
|
||||
GameStateW startingState,
|
||||
const GameStateW& startingState,
|
||||
const ActionResultProto& actionResult,
|
||||
const SettingsGetter& settings) -> GameStateW;
|
||||
auto ApplyResults(
|
||||
|
||||
@@ -12,10 +12,11 @@ cc_library(
|
||||
deps = [
|
||||
":game_state_copier",
|
||||
":unit_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
|
||||
"//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:hex_map_utils",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -26,7 +27,8 @@ cc_library(
|
||||
hdrs = ["GameStateCopier.hpp"],
|
||||
copts = COPTS,
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/GameStateCopier.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
@@ -16,57 +14,12 @@ auto CopyWithExtraUnits(const GameStateW& original, int additionalCount) -> Game
|
||||
net::eagle0::shardok::storage::fb::GameStateT endGST;
|
||||
startGS->UnPackTo(&endGST);
|
||||
|
||||
// Add the requested units plus some extra slack for future use
|
||||
int extraSlack = std::max(5, additionalCount * 2);
|
||||
for (int i = 0; i < additionalCount + extraSlack; i++) {
|
||||
for (int i = 0; i < additionalCount; i++) {
|
||||
Unit unit;
|
||||
unit.mutate_unit_id(static_cast<int16_t>(endGST.units.size()));
|
||||
if (i < additionalCount) {
|
||||
unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
} else {
|
||||
unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT);
|
||||
// Set safe defaults for reserved slots
|
||||
unit.mutate_player_id(-1);
|
||||
unit.mutate_eagle_player_id(-1);
|
||||
unit.mutable_location().mutate_row(-1);
|
||||
unit.mutable_location().mutate_column(-1);
|
||||
}
|
||||
unit.mutate_unit_id(endGST.units.size());
|
||||
endGST.units.push_back(unit);
|
||||
}
|
||||
|
||||
// Copy occupied tiles bitfield from original GameState (much faster than O(n) rebuild)
|
||||
if (startGS->occupied_tiles() && startGS->hex_map()) {
|
||||
const size_t originalBitfieldSize = startGS->occupied_tiles()->size();
|
||||
endGST.occupied_tiles.resize(originalBitfieldSize);
|
||||
|
||||
// Fast O(bitfield_bytes) copy instead of O(units) rebuild
|
||||
std::memcpy(
|
||||
endGST.occupied_tiles.data(),
|
||||
startGS->occupied_tiles()->data(),
|
||||
originalBitfieldSize);
|
||||
} else if (endGST.hex_map) {
|
||||
// Fallback: create new bitfield only if original doesn't have one
|
||||
const int16_t rowCount = endGST.hex_map->row_count;
|
||||
const int16_t columnCount = endGST.hex_map->column_count;
|
||||
const size_t mapSize = rowCount * columnCount;
|
||||
const size_t bitfieldSize = (mapSize + 7) / 8; // Ceiling division
|
||||
endGST.occupied_tiles.resize(bitfieldSize, 0); // Initialize all bits to 0 (empty)
|
||||
|
||||
// Populate bitfield based on unit positions (O(n) fallback)
|
||||
for (const auto& unit : endGST.units) {
|
||||
if (unit.status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
const auto& location = unit.location();
|
||||
if (location.row() >= 0 && location.row() < rowCount && location.column() >= 0 &&
|
||||
location.column() < columnCount) {
|
||||
const size_t tileIndex = location.row() * columnCount + location.column();
|
||||
const size_t byteIndex = tileIndex / 8;
|
||||
const size_t bitOffset = tileIndex % 8;
|
||||
endGST.occupied_tiles[byteIndex] |= (1 << bitOffset); // Set the bit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flatbuffers::FlatBufferBuilder newFbb;
|
||||
newFbb.ForceDefaults(true);
|
||||
newFbb.Finish(net::eagle0::shardok::storage::fb::GameState::Pack(newFbb, &endGST));
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
#ifndef EAGLE0_GAMESTATECOPIER_HPP
|
||||
#define EAGLE0_GAMESTATECOPIER_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
|
||||
auto CopyWithExtraUnits(const GameStateW& original, int additionalCount) -> GameStateW;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
namespace shardok {
|
||||
auto DefensiveAmbushAction::InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
|
||||
const auto results = CombatUtils::InternalPerformMelee(
|
||||
ActionCost(ActionCost::standard, 0),
|
||||
currentState->units()->Get(ambusherId),
|
||||
|
||||
@@ -17,10 +17,8 @@ private:
|
||||
const SettingsGetter settings;
|
||||
|
||||
protected:
|
||||
auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const
|
||||
-> vector<ActionResult> override;
|
||||
auto InternalExecute(const GameStateW& currentState, std::shared_ptr<RandomGenerator> generator)
|
||||
const -> vector<ActionResult> override;
|
||||
|
||||
public:
|
||||
DefensiveAmbushAction(
|
||||
|
||||
@@ -15,7 +15,7 @@ using net::eagle0::shardok::common::GameStatus;
|
||||
|
||||
auto EndPlayerSetupCommand::InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResultProto> {
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
|
||||
auto startingGameState = gameState;
|
||||
|
||||
ActionResultProto endResult{};
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
|
||||
class EndPlayerSetupCommand : public ShardokCommand {
|
||||
private:
|
||||
const PlayerId nextPid;
|
||||
@@ -23,8 +26,7 @@ private:
|
||||
protected:
|
||||
[[nodiscard]] auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const
|
||||
-> vector<ActionResult> override;
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
|
||||
|
||||
public:
|
||||
explicit EndPlayerSetupCommand(
|
||||
|
||||
@@ -101,7 +101,7 @@ FallIntoWaterAction::FallIntoWaterAction(
|
||||
|
||||
auto FallIntoWaterAction::InternalExecute(
|
||||
const GameStateW ¤tState,
|
||||
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
|
||||
vector<ActionResult> results{};
|
||||
|
||||
auto fallerTerrain =
|
||||
@@ -150,7 +150,8 @@ auto FallIntoWaterAction::InternalExecute(
|
||||
Terrain bestTerrain{};
|
||||
|
||||
for (const auto &adjWithTerrain : adjacentCoordsAndTerrain) {
|
||||
const auto *possibleOccupant = currentState.GetOccupant(adjWithTerrain.adjacentCoords);
|
||||
const auto *possibleOccupant =
|
||||
Occupant(currentState->units(), adjWithTerrain.adjacentCoords);
|
||||
if (possibleOccupant && (possibleOccupant->player_id() == fallerAfter.player_id() ||
|
||||
!possibleOccupant->hidden())) {
|
||||
continue;
|
||||
@@ -171,7 +172,7 @@ auto FallIntoWaterAction::InternalExecute(
|
||||
}
|
||||
}
|
||||
|
||||
if (found && !currentState.GetOccupant(bestCoords)) {
|
||||
if (found && !Occupant(currentState->units(), bestCoords)) {
|
||||
PercentileRollOdds odds = EscapeChance(
|
||||
baseEscapeOdds,
|
||||
bestTerrain,
|
||||
|
||||
@@ -40,8 +40,7 @@ private:
|
||||
|
||||
[[nodiscard]] auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const
|
||||
-> vector<ActionResult> override;
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
|
||||
|
||||
const UnitId fallerId;
|
||||
const SettingsGetter settings;
|
||||
|
||||
@@ -24,7 +24,7 @@ FireOutAction::FireOutAction(
|
||||
|
||||
auto FireOutAction::InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
|
||||
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
|
||||
const auto fireOutRoll = generator->Percentile();
|
||||
|
||||
ActionResult resultProto;
|
||||
|
||||
@@ -17,10 +17,8 @@ namespace shardok {
|
||||
|
||||
class FireOutAction : public ShardokAction {
|
||||
private:
|
||||
auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const
|
||||
-> vector<ActionResult> override;
|
||||
auto InternalExecute(const GameStateW& currentState, std::shared_ptr<RandomGenerator> generator)
|
||||
const -> vector<ActionResult> override;
|
||||
|
||||
const Coords coords;
|
||||
const net::eagle0::shardok::storage::fb::TileModifier existingModifier;
|
||||
|
||||
@@ -24,7 +24,7 @@ FireSpreadAction::FireSpreadAction(
|
||||
|
||||
auto FireSpreadAction::InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
|
||||
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
|
||||
const auto fireSpreadRoll = generator->Percentile();
|
||||
|
||||
if (PercentileRollSucceeds(fireSpreadOdds, fireSpreadRoll)) {
|
||||
|
||||
@@ -17,10 +17,8 @@ namespace shardok {
|
||||
|
||||
class FireSpreadAction : public ShardokAction {
|
||||
private:
|
||||
auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const
|
||||
-> vector<ActionResult> override;
|
||||
auto InternalExecute(const GameStateW& currentState, std::shared_ptr<RandomGenerator> generator)
|
||||
const -> vector<ActionResult> override;
|
||||
|
||||
const Coords coords;
|
||||
const net::eagle0::shardok::storage::fb::TileModifier existingModifier;
|
||||
|
||||
@@ -41,7 +41,7 @@ auto effectiveIce(const TerrainProto& terr) -> double {
|
||||
|
||||
auto IceAdjustmentAction::InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
|
||||
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> {
|
||||
vector<ActionResult> results{};
|
||||
|
||||
TerrainProto newTerrain{};
|
||||
|
||||
@@ -19,8 +19,7 @@ class IceAdjustmentAction : public ShardokAction {
|
||||
private:
|
||||
[[nodiscard]] auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const
|
||||
-> vector<ActionResult> override;
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
|
||||
|
||||
const Coords coords;
|
||||
const Terrain terrain;
|
||||
|
||||
@@ -33,7 +33,6 @@ auto IsResolved(const net::eagle0::shardok::storage::fb::UnitStatus status) -> b
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT: return false;
|
||||
}
|
||||
|
||||
@@ -44,8 +43,7 @@ class MeteorUnitDamageAction : public ShardokAction {
|
||||
private:
|
||||
[[nodiscard]] auto InternalExecute(
|
||||
const GameStateW ¤tState,
|
||||
const std::shared_ptr<RandomGenerator> &generator) const
|
||||
-> vector<ActionResultProto> override;
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> override;
|
||||
|
||||
const SettingsGetter settings;
|
||||
const double attackerIntelligence;
|
||||
@@ -72,7 +70,7 @@ public:
|
||||
|
||||
auto MeteorUnitDamageAction::InternalExecute(
|
||||
const GameStateW ¤tState,
|
||||
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
|
||||
CombatDamage attackerDamage =
|
||||
CombatDamage::Builder()
|
||||
.SetFire(attackerIntelligence * baseDamage * damageMultiplier)
|
||||
@@ -99,10 +97,8 @@ auto MeteorUnitDamageAction::InternalExecute(
|
||||
|
||||
class MeteorTileDamageAction : public ShardokAction {
|
||||
private:
|
||||
auto InternalExecute(
|
||||
const GameStateW ¤tState,
|
||||
const std::shared_ptr<RandomGenerator> &generator) const
|
||||
-> vector<ActionResultProto> override;
|
||||
auto InternalExecute(const GameStateW ¤tState, std::shared_ptr<RandomGenerator> generator)
|
||||
const -> vector<ActionResultProto> override;
|
||||
|
||||
const Terrain *terrain;
|
||||
const Coords coords;
|
||||
@@ -132,7 +128,7 @@ public:
|
||||
|
||||
auto MeteorTileDamageAction::InternalExecute(
|
||||
const GameStateW ¤tState,
|
||||
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
|
||||
auto tm = fb::ToTileModifierProto(terrain->modifier());
|
||||
|
||||
MutatingAdjustBridgeIntegrity(&tm, integrityAdjustment);
|
||||
@@ -155,7 +151,7 @@ auto MeteorTileDamageAction::InternalExecute(
|
||||
|
||||
auto MeteorCastAction::InternalExecute(
|
||||
const GameStateW ¤tState,
|
||||
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
|
||||
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
|
||||
vector<ActionResultProto> allResults{};
|
||||
auto runningGameState = startingGameState;
|
||||
for (const UnitId &actorId : actorIds) {
|
||||
@@ -171,7 +167,7 @@ auto MeteorCastAction::PerformOneActorCast(
|
||||
vector<ActionResultProto> &results,
|
||||
const Unit *actorBefore,
|
||||
GameStateW &runningGameState,
|
||||
const std::shared_ptr<RandomGenerator> &generator) const -> GameStateW {
|
||||
std::shared_ptr<RandomGenerator> generator) const -> GameStateW {
|
||||
const double actorIntelligence = actorBefore->attached_hero().wisdom();
|
||||
|
||||
ActionResultProto mainResult{};
|
||||
@@ -183,7 +179,7 @@ auto MeteorCastAction::PerformOneActorCast(
|
||||
results.push_back(mainResult);
|
||||
|
||||
const Coords target = actorBefore->attached_hero().profession_info().cast_target();
|
||||
const Unit *possibleOccupant = runningGameState.GetOccupant(target);
|
||||
const Unit *possibleOccupant = Occupant(runningGameState->units(), target);
|
||||
const Terrain *targetTerrain = GetTerrain(startingGameState->hex_map(), target);
|
||||
if (possibleOccupant) {
|
||||
// Direct damage action
|
||||
@@ -248,7 +244,7 @@ auto MeteorCastAction::PerformOneActorCast(
|
||||
for (const Coords &splashCoords : adjacentCoords) {
|
||||
const auto &splashTerrain = GetTerrain(runningGameState->hex_map(), splashCoords);
|
||||
|
||||
const Unit *splashOccupant = runningGameState.GetOccupant(splashCoords);
|
||||
const Unit *splashOccupant = Occupant(runningGameState->units(), splashCoords);
|
||||
if (splashOccupant) {
|
||||
MeteorUnitDamageAction splashUnitDamageAction(
|
||||
settings,
|
||||
@@ -301,7 +297,7 @@ auto MeteorCastAction::PerformOneActorCast(
|
||||
|
||||
// Check for fallen heroes
|
||||
for (const Coords &coords : destroyedBridgeOrIceTiles) {
|
||||
const auto *maybeOccupant = runningGameState.GetOccupant(coords);
|
||||
const auto *maybeOccupant = Occupant(runningGameState->units(), coords);
|
||||
if (maybeOccupant) {
|
||||
const BattalionTypeSPtr &battalionType =
|
||||
settings.GetBattalionType(maybeOccupant->battalion().type());
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokAction.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
|
||||
class MeteorCastAction : public ShardokAction {
|
||||
private:
|
||||
@@ -26,12 +29,11 @@ private:
|
||||
vector<ActionResult>& results,
|
||||
const Unit* actorBefore,
|
||||
GameStateW& runningGameState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const -> GameStateW;
|
||||
std::shared_ptr<RandomGenerator> generator) const -> GameStateW;
|
||||
|
||||
[[nodiscard]] auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const
|
||||
-> vector<ActionResult> override;
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
|
||||
|
||||
const vector<UnitId> actorIds;
|
||||
const GameStateW startingGameState;
|
||||
|
||||
@@ -135,7 +135,7 @@ auto NewWeather(
|
||||
|
||||
auto NewRoundAction::InternalExecute(
|
||||
const GameStateW ¤tState,
|
||||
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
|
||||
vector<ActionResultProto> results{};
|
||||
|
||||
GameStateW runningGameState = startingGameState;
|
||||
|
||||
@@ -11,24 +11,25 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokAction.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/FireOutActionFactory.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/FireSpreadActionFactory.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/IceAndSnowAdjustmentActionFactory.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/UndeadChangeActionFactory.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
|
||||
|
||||
class NewRoundAction : public ShardokAction {
|
||||
private:
|
||||
[[nodiscard]] auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const
|
||||
-> vector<ActionResult> override;
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
|
||||
|
||||
const GameStateW startingGameState;
|
||||
const SettingsGetter settings;
|
||||
|
||||
@@ -22,7 +22,7 @@ auto ChooseUndeadCommand(
|
||||
|
||||
auto PerformUndeadCommandsAction::InternalExecute(
|
||||
const GameStateW ¤tState,
|
||||
const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
|
||||
const std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResultProto> {
|
||||
GameStateW runningGameState = startingGameState;
|
||||
vector<ActionResultProto> allResults{};
|
||||
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using GameStateW = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
|
||||
|
||||
class PerformUndeadCommandsAction : public ShardokAction {
|
||||
private:
|
||||
[[nodiscard]] auto InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
const std::shared_ptr<RandomGenerator>& generator) const
|
||||
-> vector<ActionResult> override;
|
||||
std::shared_ptr<RandomGenerator> generator) const -> vector<ActionResult> override;
|
||||
|
||||
const GameStateW& startingGameState;
|
||||
const SettingsGetter settings;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user