mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 08:35:42 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c691dc481a | ||
|
|
77cb1d8168 | ||
|
|
8ac81891d3 | ||
|
|
af3209e2b3 | ||
|
|
aa62555735 | ||
|
|
f3d09770ee | ||
|
|
83d81dc033 | ||
|
|
3b05bc3f0b | ||
|
|
c6c22778a7 | ||
|
|
11e81a639a |
+2
-1
@@ -20,7 +20,7 @@ project/boot/
|
||||
project/plugins/project/
|
||||
project/target/
|
||||
bazel-bin
|
||||
bazel-eagle0*
|
||||
bazel-eagle0
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
.ijwb
|
||||
@@ -32,6 +32,7 @@ buildWin.sh
|
||||
__pycache__/
|
||||
scripts/refresh_name_layers/vendor/
|
||||
scripts/refresh_name_layers/refresh_name_layers.zip
|
||||
.pre-commit-config.yaml
|
||||
.bazelbsp
|
||||
.bsp
|
||||
.metals
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: no-commit-to-branch
|
||||
args: [--branch, main]
|
||||
- repo: https://github.com/pocc/pre-commit-hooks
|
||||
rev: v1.3.5
|
||||
hooks:
|
||||
- id: clang-format
|
||||
args: [-i, --no-diff]
|
||||
types_or: ["c++", "c#"]
|
||||
exclude: ^src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins
|
||||
- repo: https://github.com/yoheimuta/protolint
|
||||
rev: v0.42.2
|
||||
hooks:
|
||||
- id: protolint
|
||||
args: [-fix]
|
||||
exclude: ^src/main/protobuf/scalapb/
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: scalafmt
|
||||
name: scalafmt
|
||||
language: system
|
||||
entry: scalafmt -i -f
|
||||
types_or: ["scala"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: gazelle
|
||||
name: gazelle
|
||||
language: system
|
||||
entry: bazel run //:gazelle
|
||||
files: '(\.go|\.proto|BUILD\.bazel|BUILD|WORKSPACE|WORKSPACE\.bazel|\.bzl)$'
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: update-action-result-types
|
||||
name: update-action-result-types
|
||||
language: system
|
||||
entry: ./scripts/updateActionResultTypes.sh
|
||||
files: 'src/main/protobuf/net/eagle0/eagle/common/action_result_type.proto'
|
||||
@@ -1,47 +1,6 @@
|
||||
version = "3.9.9"
|
||||
runner.dialect = scala3
|
||||
rewrite.scala3.convertToNewSyntax = true
|
||||
# Keep braces, don't use significant indentation
|
||||
# rewrite.scala3.removeOptionalBraces = yes
|
||||
rewrite.scala3.insertEndMarkerMinLines = 15
|
||||
rewrite.scala3.removeEndMarkerMaxLines = 14
|
||||
|
||||
# Strip margin settings
|
||||
assumeStandardLibraryStripMargin = false
|
||||
align.stripMargin = true
|
||||
|
||||
# Code Style & Formatting
|
||||
align.preset = more
|
||||
align.multiline = true
|
||||
align.arrowEnumeratorGenerator = true
|
||||
spaces.inImportCurlyBraces = false
|
||||
spaces.beforeContextBoundColon = Never
|
||||
maxColumn = 120
|
||||
docstrings.style = Asterisk
|
||||
docstrings.wrap = yes
|
||||
|
||||
# Method chaining
|
||||
newlines.beforeCurlyLambdaParams = multilineWithCaseOnly
|
||||
optIn.breakChainOnFirstMethodDot = true
|
||||
includeCurlyBraceInSelectChains = false
|
||||
|
||||
# Advanced Scala 3 Features
|
||||
rewrite.scala3.countEndMarkerLines = all
|
||||
rewrite.redundantBraces.stringInterpolation = true
|
||||
rewrite.redundantBraces.parensForOneLineApply = true
|
||||
|
||||
# Project-Specific Considerations
|
||||
optIn.annotationNewlines = true
|
||||
runner.optimizer.forceConfigStyleMinArgCount = 3
|
||||
|
||||
# Import sorting configuration
|
||||
rewrite.rules = [SortImports, RedundantBraces, RedundantParens]
|
||||
rewrite.imports.sort = scalastyle
|
||||
rewrite.imports.groups = [
|
||||
["java\\..*"],
|
||||
["javax\\..*"],
|
||||
["scala\\..*"],
|
||||
[".*"]
|
||||
]
|
||||
rewrite.imports.contiguousGroups = only
|
||||
rewrite.trailingCommas.style = never
|
||||
|
||||
@@ -35,12 +35,9 @@ Unity Client ↔ Eagle (gRPC streaming) ↔ Shardok (internal gRPC)
|
||||
# Build Eagle server (Scala strategic layer)
|
||||
bazel build //src/main/scala/net/eagle0/eagle:eagle_server_deploy.jar
|
||||
|
||||
# Build Shardok server (C++ tactical layer)
|
||||
# Build Shardok server (C++ tactical layer)
|
||||
bazel build -c opt //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
# Shardok server includes both AI algorithms
|
||||
bazel build //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
|
||||
@@ -97,54 +94,6 @@ bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,cla
|
||||
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' /Users/dancrosby/CodingProjects/github/eagle0/src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.cpp -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++20
|
||||
```
|
||||
|
||||
## AI Algorithm Selection
|
||||
|
||||
Eagle0 supports two AI algorithms for tactical combat decision-making:
|
||||
|
||||
### Iterative Deepening AI (Default)
|
||||
The original minimax-based AI with sophisticated randomness handling:
|
||||
- **Advantages**: Proven, sophisticated randomness evaluation, comprehensive lookahead
|
||||
- **Use cases**: Production builds, scenarios requiring precise evaluation
|
||||
- **Performance**: Single-threaded, thorough evaluation
|
||||
|
||||
### Monte Carlo Tree Search AI (MCTS)
|
||||
Modern MCTS-based AI with multithreading support:
|
||||
- **Advantages**: Multithreaded, better performance on modern CPUs, anytime algorithm
|
||||
- **Use cases**: Performance testing, scenarios requiring fast decisions
|
||||
- **Performance**: Multithreaded, adaptive depth based on time budget
|
||||
|
||||
### Switching Between Algorithms
|
||||
|
||||
The algorithm is selected at **runtime** via the ShardokAIClient constructor:
|
||||
|
||||
```cpp
|
||||
// Using Iterative Deepening AI (default)
|
||||
ShardokAIClient client(playerId, isDefender, hexMap, settings);
|
||||
// OR explicitly:
|
||||
ShardokAIClient client(playerId, isDefender, hexMap, settings, AIAlgorithmType::ITERATIVE_DEEPENING);
|
||||
|
||||
// Using MCTS AI
|
||||
ShardokAIClient client(playerId, isDefender, hexMap, settings, AIAlgorithmType::MCTS);
|
||||
```
|
||||
|
||||
```bash
|
||||
# Build the server (includes both AI algorithms)
|
||||
bazel build //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
# Test both algorithms
|
||||
bazel test //src/test/cpp/net/eagle0/shardok/ai:ai_iterative_deepening_test
|
||||
bazel test //src/test/cpp/net/eagle0/shardok/ai:ai_mcts_test # If available
|
||||
|
||||
# Performance tests
|
||||
./scripts/ai_perf_test.sh # Uses whatever algorithm the server is configured to use
|
||||
```
|
||||
|
||||
Both implementations are compatible with all existing interfaces and produce the same `SearchResult` structure.
|
||||
|
||||
**Note**: Both implementations are documented in `src/main/cpp/net/eagle0/shardok/ai/AI_SCORING_SYSTEM.md`, including recommendations for improving MCTS randomness handling.
|
||||
|
||||
The AI algorithm selection is made at runtime when creating ShardokAIClient instances, allowing different AI strategies to be used for different players or game situations within the same server process.
|
||||
|
||||
## Language-Specific Patterns
|
||||
|
||||
**Scala (Strategic Layer):**
|
||||
@@ -229,6 +178,4 @@ done
|
||||
|
||||
- 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`
|
||||
- Always run "bazel run //:gazelle" after editing any BUILD.bazel files
|
||||
- *ALWAYS ALWAYS* run "bazel run gazelle" after any change that modifies a BUILD.bazel file
|
||||
- Docker containerization available via `ci/eagle_run.Dockerfile`
|
||||
@@ -1,177 +0,0 @@
|
||||
# MCTS with Player Flips - Clean Design
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Single Perspective**: All scoring is from our AI's perspective (positive = good for us, negative = bad for us)
|
||||
2. **Player Flips**: Continue expansion/simulation until N player turn changes occur
|
||||
3. **Minimax Integration**: Our turns maximize our score, opponent turns minimize our score
|
||||
4. **Simplicity**: No special casing - just track when currentPlayer changes
|
||||
|
||||
## Node Structure
|
||||
|
||||
```cpp
|
||||
struct MCTSNode {
|
||||
// Command that led to this state
|
||||
size_t commandIndex;
|
||||
CommandType commandType;
|
||||
|
||||
// Game state after executing the command
|
||||
GameStateW resultingGameState;
|
||||
PlayerId currentPlayer; // Whose turn it is in this state
|
||||
|
||||
// Tree position
|
||||
int playerFlipsFromRoot; // Number of player changes from root
|
||||
bool isOurTurn; // currentPlayer == our AI's playerId
|
||||
|
||||
// MCTS statistics (always from our perspective)
|
||||
int visitCount = 0;
|
||||
double totalScore = 0.0;
|
||||
double averageScore = 0.0;
|
||||
|
||||
// Tree structure
|
||||
std::vector<std::unique_ptr<MCTSNode>> children;
|
||||
std::vector<size_t> untriedCommands;
|
||||
};
|
||||
```
|
||||
|
||||
## Expansion Algorithm
|
||||
|
||||
```cpp
|
||||
MCTSNode* Expand(MCTSNode* node) {
|
||||
// Check if we've reached max player flips
|
||||
if (node->playerFlipsFromRoot >= maxPlayerFlips) {
|
||||
return node; // Don't expand further
|
||||
}
|
||||
|
||||
// Pick untried command
|
||||
size_t cmdIndex = PickUntriedCommand(node);
|
||||
|
||||
// Execute command to create child state
|
||||
auto childEngine = std::make_shared<ShardokEngine>(parentEngine);
|
||||
PlayerId playerBefore = childEngine->GetCurrentPlayerId();
|
||||
childEngine->PostCommand(playerBefore, cmdIndex, averageGenerator);
|
||||
PlayerId playerAfter = childEngine->GetCurrentPlayerId();
|
||||
|
||||
// Create child node
|
||||
auto child = std::make_unique<MCTSNode>();
|
||||
child->commandIndex = cmdIndex;
|
||||
child->resultingGameState = childEngine->GetCurrentGameState();
|
||||
child->currentPlayer = playerAfter;
|
||||
child->isOurTurn = (playerAfter == ourPlayerId);
|
||||
|
||||
// Track player flips
|
||||
child->playerFlipsFromRoot = node->playerFlipsFromRoot;
|
||||
if (playerBefore != playerAfter) {
|
||||
child->playerFlipsFromRoot++;
|
||||
}
|
||||
|
||||
// Score from our perspective
|
||||
child->immediateScore = ScoreFromOurPerspective(child->resultingGameState);
|
||||
|
||||
return child;
|
||||
}
|
||||
```
|
||||
|
||||
## Simulation Algorithm
|
||||
|
||||
```cpp
|
||||
double Simulate(const GameStateW& startState, PlayerId startPlayer, int startFlips) {
|
||||
auto simEngine = CreateEngine(startState);
|
||||
int currentFlips = startFlips;
|
||||
|
||||
while (currentFlips < maxPlayerFlips) {
|
||||
PlayerId currentPlayer = simEngine->GetCurrentPlayerId();
|
||||
bool isOurTurn = (currentPlayer == ourPlayerId);
|
||||
|
||||
// Get available commands
|
||||
auto commands = simEngine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
if (commands->empty()) break;
|
||||
|
||||
// Pick best command based on whose turn it is
|
||||
size_t bestCmd = 0;
|
||||
double bestScore = isOurTurn ? -infinity : +infinity;
|
||||
|
||||
for (size_t i = 0; i < commands->size(); ++i) {
|
||||
auto testEngine = CreateEngine(simEngine);
|
||||
testEngine->PostCommand(currentPlayer, i, averageGenerator);
|
||||
|
||||
// Always score from our perspective
|
||||
double score = ScoreFromOurPerspective(testEngine->GetCurrentGameState());
|
||||
|
||||
// Our turn: maximize our score, Opponent turn: minimize our score
|
||||
bool shouldSelect = isOurTurn ? (score > bestScore) : (score < bestScore);
|
||||
if (shouldSelect) {
|
||||
bestScore = score;
|
||||
bestCmd = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute chosen command
|
||||
PlayerId playerBefore = simEngine->GetCurrentPlayerId();
|
||||
simEngine->PostCommand(currentPlayer, bestCmd, averageGenerator);
|
||||
PlayerId playerAfter = simEngine->GetCurrentPlayerId();
|
||||
|
||||
// Track player flips
|
||||
if (playerBefore != playerAfter) {
|
||||
currentFlips++;
|
||||
}
|
||||
}
|
||||
|
||||
return ScoreFromOurPerspective(simEngine->GetCurrentGameState());
|
||||
}
|
||||
```
|
||||
|
||||
## Selection Algorithm
|
||||
|
||||
```cpp
|
||||
MCTSNode* SelectChild(MCTSNode* node) {
|
||||
MCTSNode* bestChild = nullptr;
|
||||
double bestUCB1 = node->isOurTurn ? -infinity : +infinity;
|
||||
|
||||
for (auto& child : node->children) {
|
||||
double ucb1 = child->averageScore + explorationTerm;
|
||||
|
||||
// Our turn: pick highest UCB1, Opponent turn: pick lowest UCB1
|
||||
bool shouldSelect = node->isOurTurn ? (ucb1 > bestUCB1) : (ucb1 < bestUCB1);
|
||||
|
||||
if (shouldSelect) {
|
||||
bestUCB1 = ucb1;
|
||||
bestChild = child.get();
|
||||
}
|
||||
}
|
||||
|
||||
return bestChild;
|
||||
}
|
||||
```
|
||||
|
||||
## Backpropagation Algorithm
|
||||
|
||||
```cpp
|
||||
void Backpropagate(MCTSNode* node, double score) {
|
||||
while (node != nullptr) {
|
||||
node->visitCount++;
|
||||
node->totalScore += score; // Always from our perspective
|
||||
node->averageScore = node->totalScore / node->visitCount;
|
||||
node = node->parent;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Simplifications
|
||||
|
||||
1. **No END_TURN special cases** - just check if currentPlayer changed after any command
|
||||
2. **Consistent scoring** - always from our AI's perspective throughout
|
||||
3. **Clear minimax** - our nodes maximize, opponent nodes minimize
|
||||
4. **Simple state tracking** - just count player flips, no complex inheritance
|
||||
5. **Unified command handling** - all commands handled the same way
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
1. **Refactor MCTSNode structure** - simplify to core fields needed
|
||||
2. **Rewrite MCTSExpansion** - remove END_TURN special cases, just track player changes
|
||||
3. **Fix MCTSSimulation** - ensure consistent perspective and proper minimax
|
||||
4. **Simplify MCTSSelection** - clean minimax logic
|
||||
5. **Clean up MCTSBackpropagation** - single perspective throughout
|
||||
6. **Add comprehensive testing** - verify player flips are tracked correctly
|
||||
|
||||
This design eliminates the confusion around perspectives and special cases, making the algorithm much easier to understand and debug.
|
||||
+9
-1
@@ -20,7 +20,13 @@ bazel_dep(name = "rules_pkg", version = "1.1.0")
|
||||
# Language Support - Scala
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_scala", version = "7.1.1")
|
||||
bazel_dep(name = "rules_scala")
|
||||
|
||||
git_override(
|
||||
commit = "f4523243ab5f946600c2b0d7a4b5791bb8758799",
|
||||
module_name = "rules_scala",
|
||||
remote = "https://github.com/bazel-contrib/rules_scala.git",
|
||||
)
|
||||
|
||||
scala_config = use_extension(
|
||||
"@rules_scala//scala/extensions:config.bzl",
|
||||
@@ -77,6 +83,8 @@ use_repo(
|
||||
"com_github_aws_aws_sdk_go_v2_credentials",
|
||||
"com_github_aws_aws_sdk_go_v2_service_s3",
|
||||
"org_golang_google_protobuf",
|
||||
"org_golang_x_text",
|
||||
"com_github_google_go_cmp",
|
||||
)
|
||||
|
||||
#
|
||||
|
||||
Generated
+3
-5
@@ -317,8 +317,6 @@
|
||||
"https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.0.0/source.json": "b0162a65c6312e45e7912e39abd1a7f8856c2c7e41ecc9b6dc688a6f6400a917",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.45.1/MODULE.bazel": "a69d0db3a958fab2c6520961e1b2287afcc8b36690fd31bbc4f6f7391397150d",
|
||||
"https://bcr.bazel.build/modules/rules_scala/7.1.1/MODULE.bazel": "b1f80c52ae49b27d41b9291d8b328b69247de2b7596d35d09afe6147b82cf562",
|
||||
"https://bcr.bazel.build/modules/rules_scala/7.1.1/source.json": "5038cb231d4020c5965c920681cf961a7bf137b40315025e40f3a7b6a0ac1f0f",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3",
|
||||
@@ -1268,7 +1266,7 @@
|
||||
"@@rules_scala~//scala/extensions:config.bzl%scala_config": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "TdBxhkZTM7VU6teIFS+KoonKU7wmb5BL7leCWWx7yX8=",
|
||||
"usagesDigest": "z8BblsnuAs+q3LEu90nfjZZasRDCB2K31wb7NXFSZVM=",
|
||||
"usagesDigest": "6V8NLCEm/+v54a4/jhHtdftOmlUdkW4PPxw+IISD0Eo=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {
|
||||
@@ -1293,8 +1291,8 @@
|
||||
},
|
||||
"@@rules_scala~//scala/extensions:deps.bzl%scala_deps": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "F2PMm61fmZ/IE+VSw1rigJ71hBDD7k3vqyYR1/GgXeA=",
|
||||
"usagesDigest": "kwo8oolISmSSITnit4b4S0vBiUtHlHK0WLDUwScxmOg=",
|
||||
"bzlTransitiveDigest": "auQqXmoQ01Gebqkny5AjUoLJjButvOXfi1JYq7gucsI=",
|
||||
"usagesDigest": "086XZ0aCCfOZmHhDfMCTOlr5OcQC3zEr2jiL87XtSm8=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
# Actions and Commands Model Usage Analysis
|
||||
|
||||
This document analyzes all actions and commands in `src/main/scala/net/eagle0/eagle/library/actions/impl` to determine which use Scala models vs protobuf models, based on BUILD.bazel dependencies.
|
||||
|
||||
**Legend:**
|
||||
- ✅ **Scala Models Only** - Uses only `//src/main/scala/net/eagle0/eagle/model` dependencies
|
||||
- ❌ **Uses Protobuf** - Has dependencies on `//src/main/protobuf` targets
|
||||
- 🔄 **Partial Conversion** - Conversion attempted but blocked by dependencies
|
||||
|
||||
## Summary
|
||||
|
||||
Based on BUILD.bazel dependency analysis (2025-09-16, updated 2025-09-17):
|
||||
- **Total Commands Analyzed:** 41
|
||||
- **Commands Fully Migrated (No Protobuf):** 41 (100%) ✅
|
||||
- **Commands Still Using Protobuf:** 0 (0%) ✅
|
||||
- **Total Actions Analyzed:** 48
|
||||
- **Actions Fully Migrated (No Protobuf):** 5 (10.4%)
|
||||
- **Actions Partially Migrated:** 19 (39.6%)
|
||||
- **Actions Still Using Protobuf:** 24 (50%)
|
||||
- **Base Classes:** 8 protoless variants available, 6 still use protobuf
|
||||
- **Shared Components:** `ResolvedEagleUnit` migrated to use `Option[BattalionT]` for proper null handling
|
||||
|
||||
## Conversion Insights
|
||||
|
||||
Based on conversion attempt of `ResolveTruceOfferCommand` (see [PR #4379](https://github.com/nolen777/eagle0/pull/4379)):
|
||||
|
||||
### Key Challenges Discovered
|
||||
|
||||
1. **LLM Integration Dependencies**: Commands that use `DiplomacyResolutionLlmRequestGenerator` face challenges because the LLM system still expects protobuf enum types, not Scala model enums.
|
||||
|
||||
2. **Inconsistent Package Naming**: Some files have inconsistent package declarations vs BUILD file locations (e.g., `generated_text_request_generators` in package vs `llm_request_generators` in BUILD).
|
||||
|
||||
3. **Model Constructor Differences**: Scala model constructors (e.g., `TruceOffer`) have different required parameters than their protobuf counterparts, requiring more complex data mapping.
|
||||
|
||||
4. **Type System Complexity**: Union types and type constraints become more complex when mixing protobuf and Scala model types during transition.
|
||||
|
||||
5. **Cascading Dependency Issues**: Converting to `ActionResultC` requires extensive trait dependencies (`ChangedBattalionT`, `ChangedHeroT`, `GeneratedTextRequestT`, etc.) that create complex BUILD dependency graphs, unlike simple protobuf `ActionResult`.
|
||||
|
||||
6. **BUILD Complexity**: Each Scala model conversion requires significantly more BUILD dependencies than protobuf equivalents, making incremental conversion difficult.
|
||||
|
||||
7. **Build Verification Critical**: Any conversion must maintain working build state - even simple commands like `DefendCommand` can break main server build due to dependency cascades.
|
||||
### Successful Conversion Elements
|
||||
|
||||
- ✅ Base class conversion (`SimpleAction` → `ProtolessSimpleAction`)
|
||||
- ✅ Import updates for most Scala model types
|
||||
- ✅ BUILD.bazel dependency updates for core action result types
|
||||
- ✅ Basic type conversions for simple cases
|
||||
|
||||
### Recommended Conversion Strategy
|
||||
|
||||
1. **Architecture-First Approach**: Convert base infrastructure (LLM generators, action result builders) before individual commands
|
||||
2. **Wrapper Pattern**: Use existing `Protoless*ActionWrapper` classes as templates for gradual transition
|
||||
3. **Dependency Analysis**: Map full dependency trees before attempting conversions to avoid cascading build failures
|
||||
4. **Batch Conversions**: Convert related commands together to minimize dependency conflicts
|
||||
5. **Build Verification**: **ALWAYS** verify `//src/main/scala/net/eagle0/eagle:eagle_server` and test suite build before creating PRs
|
||||
|
||||
### Conversion Requirements
|
||||
|
||||
**Before creating any PR:**
|
||||
- ✅ `bazel build //src/main/scala/net/eagle0/eagle:eagle_server` succeeds
|
||||
- ✅ `bazel test //src/test/scala/... --keep_going` passes (or doesn't introduce new failures)
|
||||
- ✅ All BUILD dependencies are correctly specified
|
||||
- ✅ Scalafmt and other linters pass
|
||||
|
||||
---
|
||||
|
||||
## Common Base Classes
|
||||
|
||||
| File | Type | Model Usage | Notes |
|
||||
|------|------|-------------|-------|
|
||||
| Action.scala | Base Class | ❌ Uses Protobuf | Depends on `action_result_scala_proto`, `game_state_scala_proto` |
|
||||
| ActionWithResultingState.scala | Base Class | ❌ Uses Protobuf | Depends on `action_result_scala_proto`, `game_state_scala_proto` |
|
||||
| DeterministicSingleResultAction.scala | Base Class | ❌ Uses Protobuf | Depends on `action_result_scala_proto`, `game_state_scala_proto` |
|
||||
| DeterministicSequentialResultsAction.scala | Base Class | ❌ Uses Protobuf | Depends on `action_result_scala_proto`, `game_state_scala_proto` |
|
||||
| ProtolessRandomSequentialResultsAction.scala | Base Class | ✅ Scala Models Only | Uses `//src/main/scala/net/eagle0/eagle/model/action_result` |
|
||||
| ProtolessRandomSimpleAction.scala | Base Class | ✅ Scala Models Only | Uses `//src/main/scala/net/eagle0/eagle/model/action_result` |
|
||||
| ProtolessSequentialResultsAction.scala | Base Class | ✅ Scala Models Only | Uses `//src/main/scala/net/eagle0/eagle/model/action_result` |
|
||||
| ProtolessSimpleAction.scala | Base Class | ✅ Scala Models Only | Uses `//src/main/scala/net/eagle0/eagle/model/action_result` |
|
||||
| RandomSequentialResultsAction.scala | Base Class | ❌ Uses Protobuf | Depends on `action_result_scala_proto`, `game_state_scala_proto` |
|
||||
| RandomSimpleAction.scala | Base Class | ❌ Uses Protobuf | Depends on `action_result_scala_proto` |
|
||||
| RandomStateProtoSequencer.scala | Sequencer | ❌ Uses Protobuf | Bridge class, depends on both protobuf and Scala models |
|
||||
| RandomStateTSequencer.scala | Sequencer | ❌ Uses Protobuf | Bridge class, depends on both protobuf and Scala models |
|
||||
| SimpleAction.scala | Base Class | ❌ Uses Protobuf | Depends on `action_result_scala_proto` |
|
||||
| VigorXPApplier.scala | Utility | ❌ Uses Protobuf | Depends on `action_result_scala_proto` |
|
||||
|
||||
---
|
||||
|
||||
## Actions
|
||||
|
||||
### ✅ Fully Migrated Actions (No Protobuf Dependencies)
|
||||
|
||||
These actions have been successfully migrated to use Scala models only:
|
||||
|
||||
| File | Base Class | Notes |
|
||||
|------|------------|-------|
|
||||
| HeroBackstoryUpdateAction.scala | ProtolessSequentialResultsAction | Processes hero backstory updates with LLM integration |
|
||||
| ProvinceConqueredAction.scala | ProtolessSimpleAction | Uses component-based design (gameId, currentRoundId, currentDate, Scala models) |
|
||||
| ProvinceHeldAction.scala | ProtolessSimpleAction | Uses specific components (gameId, currentRoundId, defendingProvince, etc.) instead of full GameState |
|
||||
| UnaffiliatedHeroAppearedAction.scala | ProtolessSimpleAction | Handles unaffiliated hero appearance with name generation |
|
||||
| WithdrawnArmiesReturnHomeAction.scala | ProtolessSequentialResultsAction | Manages army withdrawal and return mechanics |
|
||||
|
||||
### 🔄 Actions Partially Migrated (Using Protoless Base Classes)
|
||||
|
||||
These actions use protoless base classes but still have some protobuf dependencies:
|
||||
|
||||
| File | Model Usage | Notes |
|
||||
|------|-------------|-------|
|
||||
| CheckForFactionChangesAction.scala | ProtolessSequentialResultsAction | Still has some protobuf dependencies |
|
||||
| CheckForFailedQuestsAction.scala | ProtolessSequentialResultsAction | Depends on `unaffiliated_hero_quest_scala_proto` |
|
||||
| CheckForFulfilledQuestsAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| EndAttackDecisionPhaseAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| EndBattleAftermathPhaseAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| EndFreeForAllDecisionPhaseAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| EndPlayerCommandsPhaseAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| EndUnaffiliatedHeroActionsPhaseAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| EndVassalCommandsPhaseAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| FreeForAllDrawAction.scala | ProtolessSimpleAction | Depends on multiple protobuf targets |
|
||||
| FriendlyMoveAction.scala | ProtolessSimpleAction | Depends on multiple protobuf targets |
|
||||
| PerformUncontestedConquestAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| ProvinceConqueredAction.scala | ProtolessSimpleAction | **CONVERTED** - Uses specific components (gameId, currentRoundId, currentDate, Scala models) |
|
||||
| SafePassageArmiesProceedAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| ShipmentArrivedAction.scala | ProtolessSimpleAction | Depends on multiple protobuf targets |
|
||||
| TruceTurnBackPhaseAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
|
||||
| UnaffiliatedHeroRejoinedAction.scala | ProtolessSimpleAction | Depends on multiple protobuf targets |
|
||||
| WonFreeForAllAction.scala | ProtolessSimpleAction | Depends on multiple protobuf targets |
|
||||
|
||||
### ❌ Actions Still Using Protobuf (Not Yet Using Protoless Base Classes)
|
||||
|
||||
| File | Notes |
|
||||
|------|-------|
|
||||
| ChronicleEventGenerator.scala | Depends on multiple protobuf targets |
|
||||
| EndBattleRequestPhaseAction.scala | Depends on `diplomacy_offer_status_scala_proto` |
|
||||
| EndBattleResolutionPhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| EndDefenseDecisionPhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| EndDiplomacyResolutionPhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| EndFreeForAllBattleRequestPhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| EndFreeForAllBattleResolutionPhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| EndHandleRiotsPhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| EndPleaseRecruitMePhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| EndProvinceMoveResolutionPhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| NewRoundAction.scala | Depends on multiple protobuf targets |
|
||||
| NewYearAction.scala | Depends on multiple protobuf targets |
|
||||
| PerformFoodConsumptionPhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| PerformForcedTurnBackAction.scala | Depends on multiple protobuf targets |
|
||||
| PerformHeroDeparturesAction.scala | Depends on multiple protobuf targets |
|
||||
| PerformHostileArmySetupAction.scala | Depends on multiple protobuf targets |
|
||||
| PerformProvinceEventsAction.scala | Depends on `province_event_scala_proto` |
|
||||
| PerformProvinceMoveResolutionAction.scala | Depends on multiple protobuf targets |
|
||||
| PerformReconResolutionAction.scala | Depends on multiple protobuf targets |
|
||||
| PerformUnaffiliatedHeroesAction.scala | Depends on `unaffiliated_hero_quest_scala_proto` |
|
||||
| PerformVassalCommandsPhaseAction.scala | Depends on multiple protobuf targets |
|
||||
| PerformVassalDefenseDecisionsAction.scala | Depends on multiple protobuf targets |
|
||||
| PrisonerEscapeAction.scala | Depends on `game_state_scala_proto` |
|
||||
| PrisonerExchangeAction.scala | Depends on multiple protobuf targets |
|
||||
| RequestBattlesAction.scala | Depends on multiple protobuf targets |
|
||||
| RequestFreeForAllBattlesAction.scala | Depends on multiple protobuf targets |
|
||||
| ResolveBattleAction.scala | Depends on `shardok_internal_interface_scala_grpc` |
|
||||
| UnaffiliatedHeroMovedAction.scala | Depends on multiple protobuf targets |
|
||||
| UnaffiliatedHeroesChangedAction.scala | Depends on multiple protobuf targets |
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
✅ **ALL COMMANDS FULLY MIGRATED** (100% - 41/41 commands)
|
||||
|
||||
All 41 commands in the codebase have been successfully migrated to use Scala models only, with no protobuf dependencies. This includes:
|
||||
|
||||
- **Simple Actions**: Use `ProtolessSimpleAction` base class
|
||||
- **Random Actions**: Use `ProtolessRandomSimpleAction` base class
|
||||
- **Complex Domain Models**: Successfully integrated with LLM systems, diplomacy, quest fulfillment, and state management
|
||||
- **Complete Type Safety**: All commands now use type-safe Scala domain models
|
||||
|
||||
**Key Migration Achievements:**
|
||||
- ✅ All military commands (ArmTroops, Train, Organize, etc.)
|
||||
- ✅ All diplomacy commands (Resolve Alliance/Truce/Ransom offers, etc.)
|
||||
- ✅ All LLM-integrated commands (backstory generation, diplomacy resolution)
|
||||
- ✅ All quest and event commands
|
||||
- ✅ Final remaining command (FreeForAllDecisionCommand) migrated
|
||||
|
||||
---
|
||||
|
||||
## Diplomacy Helpers
|
||||
|
||||
All diplomacy helpers use **Scala models only**:
|
||||
|
||||
| File | Model Usage | Notes |
|
||||
|------|-------------|-------|
|
||||
| AllianceResolutionHelpers.scala | ✅ Scala Models Only | Uses `//src/main/scala/net/eagle0/eagle/model` only |
|
||||
| BreakAllianceResolutionHelpers.scala | ✅ Scala Models Only | Uses `//src/main/scala/net/eagle0/eagle/model` only |
|
||||
| InvitationResolutionHelpers.scala | ✅ Scala Models Only | Uses `//src/main/scala/net/eagle0/eagle/model` only |
|
||||
| RansomResolutionHelpers.scala | ✅ Scala Models Only | Uses `//src/main/scala/net/eagle0/eagle/model` only |
|
||||
| TruceResolutionHelpers.scala | ✅ Scala Models Only | Uses `//src/main/scala/net/eagle0/eagle/model` only |
|
||||
|
||||
---
|
||||
|
||||
## Migration Priority Analysis
|
||||
|
||||
Based on the BUILD.bazel dependency analysis, here are the key findings and recommendations:
|
||||
|
||||
### 🎯 High Impact Migration Targets
|
||||
|
||||
**Core Dependencies Blocking Multiple Commands:**
|
||||
|
||||
1. **`action_result_scala_proto`** - Used by 12+ commands
|
||||
- Blocks: `DefendCommand`, `FreeForAllDecisionCommand`, diplomacy resolvers
|
||||
- Impact: Would unlock many command migrations
|
||||
|
||||
2. **`available_command_scala_proto` / `selected_command_scala_proto`** - Used by 10+ commands
|
||||
- Blocks: All UI-interactive commands
|
||||
- Impact: Would enable client-server interaction model migration
|
||||
|
||||
3. **`game_state_scala_proto`** - Used by 8+ commands
|
||||
- Blocks: Complex state-dependent commands
|
||||
- Impact: Core state representation migration
|
||||
|
||||
### 📊 Migration Tiers by Complexity
|
||||
|
||||
**Tier 1 - Quick Wins (2 commands):**
|
||||
- `ArmTroopsCommand` - Only `battalion_type` dependency
|
||||
- `TrainCommand` - Only `battalion_type` dependency
|
||||
- **Effort:** Low, **Impact:** Demonstrates battalion model usage
|
||||
|
||||
**Tier 2 - API Layer (5 commands):**
|
||||
- Commands blocked by `available_command`/`selected_command`
|
||||
- **Effort:** Medium, **Impact:** High (enables UI interaction models)
|
||||
|
||||
**Tier 3 - Diplomacy Suite (6 commands):**
|
||||
- All `Resolve*Command` diplomacy commands
|
||||
- **Effort:** High, **Impact:** High (complete diplomacy model migration)
|
||||
- **Strategy:** Migrate as a group after diplomacy models are ready
|
||||
|
||||
### 🏆 Success Metrics
|
||||
|
||||
**Current Status:**
|
||||
- ✅ **100% of commands fully migrated** (41/41) 🎉
|
||||
- ✅ **All diplomacy helpers use Scala models**
|
||||
- ✅ **All protoless base classes available**
|
||||
- ✅ **ALL command migration completed**
|
||||
|
||||
**Completed Milestones:**
|
||||
- ✅ **70% target:** Migrate Tier 1 + some Tier 2 commands **COMPLETED**
|
||||
- ✅ **80% target:** Continue with remaining non-diplomacy commands **COMPLETED**
|
||||
- ✅ **85% target:** Complete API layer migration **COMPLETED**
|
||||
- ✅ **95% target:** Complete diplomacy migration **COMPLETED**
|
||||
- ✅ **100% target:** Migrate final remaining command (FreeForAllDecisionCommand) **COMPLETED**
|
||||
|
||||
### 🎯 Action Migration Progress
|
||||
|
||||
**Migration Statistics:**
|
||||
- 5/48 Actions fully migrated (10.4%)
|
||||
- 20/48 Actions using protoless base classes but with protobuf dependencies (41.7%)
|
||||
- 24/48 Actions still fully on protobuf (50%)
|
||||
|
||||
**Successfully Migrated Actions:**
|
||||
1. **HeroBackstoryUpdateAction** - LLM integration for hero backstories
|
||||
2. **ProvinceConqueredAction** - Component-based design with prisoner handling and province conquest
|
||||
3. **ProvinceHeldAction** - Component-based design pattern (gameId, currentRoundId, specific models)
|
||||
4. **UnaffiliatedHeroAppearedAction** - Hero appearance with name generation
|
||||
5. **WithdrawnArmiesReturnHomeAction** - Army withdrawal mechanics
|
||||
|
||||
**Recent Migration Updates (2025-09-17):**
|
||||
- **ResolvedEagleUnit** - Changed `battalion: BattalionT` to `battalion: Option[BattalionT]`
|
||||
- Properly handles units without battalions (battalion ID -1)
|
||||
- Updated `ShardokInterfaceGrpcClient` to check for `defaultBattalionId` and use `None`
|
||||
- Updated `ResolveBattleAction`, `ProvinceConqueredAction`, `RequestBattlesAction`
|
||||
- All tests updated to handle optional battalions
|
||||
|
||||
**Key Migration Patterns:**
|
||||
- ✅ Use specific components instead of full GameState (see ProvinceHeldAction, ProvinceConqueredAction)
|
||||
- ✅ Convert protobuf models to Scala models at Action boundaries
|
||||
- ✅ Update BUILD.bazel to remove protobuf dependencies
|
||||
- ✅ Update all call sites and tests
|
||||
- ✅ Use `Option[T]` for optional fields instead of special sentinel values (e.g., battalion ID -1)
|
||||
|
||||
**Next Migration Candidates (Simple Actions with Protoless Base):**
|
||||
1. **FreeForAllDrawAction** - Already uses ProtolessSimpleAction
|
||||
2. **FriendlyMoveAction** - Already uses ProtolessSimpleAction
|
||||
3. **ShipmentArrivedAction** - Already uses ProtolessSimpleAction
|
||||
4. **WonFreeForAllAction** - Already uses ProtolessSimpleAction
|
||||
5. **ProvinceConqueredAction** - Already uses ProtolessSimpleAction, only needs `common_unit` migration
|
||||
|
||||
### 🔄 Conversion Strategy Updates
|
||||
|
||||
**Revised Approach Based on Analysis:**
|
||||
|
||||
1. **Focus on Core Dependencies First**
|
||||
- Migrate `battalion_type` model (unlocks 2 commands immediately)
|
||||
- Migrate `action_result` model (unlocks 12+ commands)
|
||||
- Migrate `available_command`/`selected_command` (unlocks UI layer)
|
||||
|
||||
2. **Leverage Existing Success**
|
||||
- 77.5% of commands already fully migrated
|
||||
- Use migrated commands as reference implementations
|
||||
- Diplomacy helpers prove complex business logic can work with Scala models
|
||||
|
||||
3. **Group Related Migrations**
|
||||
- Military commands: `ArmTroopsCommand`, `TrainCommand`, `OrganizeTroopsCommand`
|
||||
- UI commands: All using `available_command`/`selected_command`
|
||||
- Diplomacy commands: All `Resolve*Command` variants
|
||||
|
||||
---
|
||||
|
||||
*Updated on 2025-09-17 - Analysis based on BUILD.bazel dependencies and code review*
|
||||
*Latest update: ResolvedEagleUnit migrated to use Option[BattalionT] for proper battalion handling*
|
||||
@@ -0,0 +1,5 @@
|
||||
//src/test/scala/net/eagle0/eagle/ai:ai_client_utils_test ✓ FIXED
|
||||
//src/test/scala/net/eagle0/eagle/ai:faction_leader_province_ranker_test ✓ FIXED
|
||||
//src/test/scala/net/eagle0/eagle/ai:fix_leader_alone_command_selector_test ✓ FIXED
|
||||
//src/test/scala/net/eagle0/eagle/ai:invitation_command_selector_test ✓ FIXED
|
||||
//src/test/scala/net/eagle0/eagle/ai:mid_game_ai_client_test
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// AI System Types and Configuration
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AI_CONFIG_HPP
|
||||
#define EAGLE0_AI_CONFIG_HPP
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Enum for AI algorithm selection
|
||||
enum class AIAlgorithmType {
|
||||
ITERATIVE_DEEPENING, // Default: Minimax with sophisticated randomness
|
||||
MCTS, // Monte Carlo Tree Search with multithreading (original)
|
||||
MCTS_CLEAN // Clean MCTS implementation with fixed simulation perspective
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AI_CONFIG_HPP
|
||||
File diff suppressed because it is too large
Load Diff
@@ -89,8 +89,8 @@ auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue {
|
||||
break;
|
||||
}
|
||||
|
||||
const double battalionValue = battalionTypeMultiplier * (1.0 + armament / 100.0) *
|
||||
(1.0 + training / 100.0) * (0.5 + morale / 100.0) *
|
||||
const double battalionValue = battalionTypeMultiplier * (0.5 + armament / 100.0) *
|
||||
(0.5 + training / 100.0) * (0.5 + morale / 100.0) *
|
||||
unit->battalion().size();
|
||||
|
||||
const double heroValue =
|
||||
|
||||
@@ -210,556 +210,4 @@ Where:
|
||||
- **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.
|
||||
|
||||
## AIScoreCalculator Function Reference
|
||||
|
||||
### Public Interface Functions
|
||||
|
||||
#### `GuessedStateScore`
|
||||
**Purpose**: Evaluates the score of a game state from the perspective of the current AI strategy without performing any commands.
|
||||
|
||||
**Parameters**:
|
||||
- `isDefender`: Whether the AI is playing as defender
|
||||
- `state`: Current game state to evaluate
|
||||
- `aiStrategy`: Strategy being used (attack castles, hold castles, scatter, etc.)
|
||||
- `allCastleCoords`: Set of all castle coordinates on the map
|
||||
- `settingsGetter`: Game configuration and rules
|
||||
- `apdCache`: Cached action point distances for movement calculations
|
||||
- `alCache`: Cached attack locations for combat calculations
|
||||
|
||||
**Returns**: Score value representing how favorable the state is for the evaluating player (positive = good, negative = bad)
|
||||
|
||||
#### `CommandScore`
|
||||
**Purpose**: Evaluates the score for a specific command using lookahead search to consider future consequences.
|
||||
|
||||
**Parameters**:
|
||||
- `pid`: Player ID executing the command
|
||||
- `isDefender`: Whether the player is a defender
|
||||
- `remainingLookahead`: Depth of recursive search remaining
|
||||
- `maxRepeatCount`: Number of random simulations for non-deterministic commands
|
||||
- `guessedEngine`: Current game engine state
|
||||
- `attackerStrategy`: Strategy being used by attackers
|
||||
- `currentUtility`: Current game state score before command execution
|
||||
- `settingsGetter`: Game configuration
|
||||
- `allCastleCoords`: Castle locations
|
||||
- `apdCache` & `alCache`: Cached distance/attack calculations
|
||||
- `commandIndex`: Index of command to evaluate
|
||||
- `deadline`: Time limit for computation
|
||||
|
||||
**Returns**: Future containing the final score after lookahead evaluation
|
||||
|
||||
### Internal Core Functions
|
||||
|
||||
#### `BuildDecisionTree` (NEW)
|
||||
**Purpose**: Builds a complete decision tree containing all evaluated command paths up to the specified depth.
|
||||
|
||||
**Process**:
|
||||
1. Filters commands using `AICommandFilter` to reduce search space
|
||||
2. For each command, calls `ExecuteCommandForTree` to build complete subtrees
|
||||
3. Returns full tree with all possible moves and their consequences
|
||||
4. Identifies best command within the complete tree structure
|
||||
|
||||
**Returns**: `std::future<CommandDecisionTree>` containing the complete decision tree
|
||||
|
||||
#### `BestCommandIndex` (Legacy - Wrapper)
|
||||
**Purpose**: Backward compatibility wrapper that uses `BuildDecisionTree` but returns traditional `IndexAndScore`.
|
||||
|
||||
**Process**:
|
||||
1. Calls `BuildDecisionTree` to get complete tree
|
||||
2. Extracts best command information for compatibility
|
||||
3. Returns only the optimal command details in legacy format
|
||||
|
||||
#### `ExecuteCommandForTree` (NEW)
|
||||
**Purpose**: Executes a command and creates a tree node with the resulting game state and scores.
|
||||
|
||||
**Process**:
|
||||
1. Creates engine copy and executes the command with given random seed
|
||||
2. Creates `CommandTreeNode` with command results and game state
|
||||
3. Calculates immediate score using `GuessedStateScore`
|
||||
4. Calls `RecursiveTreeBuilder` to populate child nodes if depth allows
|
||||
5. Calculates lookahead score from children (or uses immediate score)
|
||||
|
||||
**Returns**: `std::unique_ptr<CommandTreeNode>` containing the command execution results and subtree
|
||||
|
||||
#### `RecursiveTreeBuilder` (NEW)
|
||||
**Purpose**: Recursively populates child nodes of a tree node by building subtrees for subsequent moves.
|
||||
|
||||
**Process**:
|
||||
1. Gets available commands for the next player
|
||||
2. Filters commands to reduce search space
|
||||
3. For each command, calls `ExecuteCommandForTree` to create child nodes
|
||||
4. Handles different command types (deterministic, odds-based, random)
|
||||
5. Populates the parent node's children vector with complete subtrees
|
||||
|
||||
#### `CalcOne` (Legacy)
|
||||
**Purpose**: Executes a single command simulation with specified randomness and returns both immediate and lookahead scores.
|
||||
|
||||
**Process**:
|
||||
1. Creates engine copy and executes the command with given random seed
|
||||
2. Calculates immediate score using `GuessedStateScore`
|
||||
3. Initiates recursive lookahead calculation if depth remains
|
||||
4. Handles timeouts gracefully by returning default scores
|
||||
|
||||
#### `EvaluateCommand`
|
||||
**Purpose**: Lower-level command evaluation that handles different command types appropriately.
|
||||
|
||||
**Command Type Handling**:
|
||||
- **Deterministic**: Single evaluation with average randomness (0.5)
|
||||
- **Odds-based**: Two evaluations (success/failure) weighted by success probability
|
||||
- **Non-deterministic**: Multiple evaluations with distributed random values, averaged
|
||||
|
||||
#### `BasicLookaheadCalculator`
|
||||
**Purpose**: Recursive lookahead search that finds the best future command sequence and propagates scores backward.
|
||||
|
||||
**Features**:
|
||||
- Uses transposition table to cache previously computed positions
|
||||
- Handles depth limits and terminal states
|
||||
- Returns futures for asynchronous computation
|
||||
- Stores results in transposition table for reuse
|
||||
|
||||
### Strategy-Specific Scoring Functions
|
||||
|
||||
#### `AttackerScoreForState`
|
||||
**Purpose**: Calculates state score from attacker perspective based on strategy type.
|
||||
|
||||
**Strategy Support**:
|
||||
- `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
|
||||
|
||||
#### `DefenderScoreForState`
|
||||
**Purpose**: Calculates state score from defender perspective.
|
||||
|
||||
**Strategy Support**:
|
||||
- `STRATEGY_HOLD_CASTLES`: Defend critical castle positions
|
||||
- `STRATEGY_SCATTER`: Spread units to avoid elimination
|
||||
- `STRATEGY_FLEE`: Escape-focused scoring
|
||||
|
||||
#### `AttackerUnitsScore`
|
||||
**Purpose**: Core unit valuation function that calculates total value of all units on the board with contextual modifiers.
|
||||
|
||||
**Features**:
|
||||
- Uses `UnitValue` for individual unit calculations
|
||||
- Applies distance multipliers based on proximity to objectives
|
||||
- Handles special cases like undead, VIP units, and scattered defenders
|
||||
- Incorporates castle bonuses and environmental penalties
|
||||
|
||||
### Specialized Strategy Functions
|
||||
|
||||
#### `DefenderScatterStrategyScoreForState`
|
||||
**Purpose**: Implements scatter strategy scoring that rewards defensive units for staying far from enemies and friendlies.
|
||||
|
||||
#### `DefenderHoldCastlesStrategyScoreForState`
|
||||
**Purpose**: Implements castle defense strategy with victory condition scoring.
|
||||
|
||||
#### `FleeStrategyScoreForState`
|
||||
**Purpose**: Implements flee strategy that heavily penalizes remaining on the battlefield.
|
||||
|
||||
### Utility Functions
|
||||
|
||||
#### `AttackerMultiplierForTargetDistance`
|
||||
**Purpose**: Calculates distance-based scoring multipliers for attackers based on proximity to priority targets.
|
||||
|
||||
**Features**:
|
||||
- Uses recursive priority list evaluation
|
||||
- Accounts for occupied vs. unoccupied targets
|
||||
- Incorporates brave water crossing capabilities
|
||||
- Uses cached action point distances for efficiency
|
||||
|
||||
#### `CommandSorter`
|
||||
**Purpose**: Comparison function for ranking commands by lookahead score (primary) and immediate score (tiebreaker).
|
||||
|
||||
#### `IsDeterministic`
|
||||
**Purpose**: Determines if a command type has predictable outcomes or requires random simulation.
|
||||
|
||||
### Performance and Caching
|
||||
|
||||
#### `EffectiveDistanceCache`
|
||||
**Purpose**: Memoization cache for expensive distance calculations between units and targets.
|
||||
|
||||
#### `AttackerScorePerformanceLogger`
|
||||
**Purpose**: Performance monitoring system that tracks call frequency and timing for `AttackerScoreForState`.
|
||||
|
||||
The function architecture supports parallel evaluation, caching, and recursive lookahead while maintaining separation between strategy-specific logic and core evaluation mechanics.
|
||||
|
||||
## Decision Tree Data Structures (NEW)
|
||||
|
||||
### CommandTreeNode
|
||||
**Purpose**: Represents a single command execution and its consequences in the decision tree.
|
||||
|
||||
**Key Fields**:
|
||||
- `commandIndex`: Index of the command in the original command list
|
||||
- `commandType`: Type of command (MOVE, MELEE, END_TURN, etc.)
|
||||
- `immediateScore`: Score of the game state immediately after this command
|
||||
- `lookaheadScore`: Best achievable score considering future moves
|
||||
- `resultingGameState`: Game state after command execution
|
||||
- `children`: Vector of child nodes representing subsequent possible moves
|
||||
- `playerId`, `depth`, `isDefender`: Metadata about the command context
|
||||
|
||||
**Features**:
|
||||
- Stores complete game state for each decision point
|
||||
- Maintains parent-child relationships for tree traversal
|
||||
- Supports both immediate and lookahead scoring
|
||||
- Contains metadata for debugging and analysis
|
||||
|
||||
### CommandDecisionTree
|
||||
**Purpose**: Complete decision tree containing all evaluated command paths from a given position.
|
||||
|
||||
**Key Fields**:
|
||||
- `rootNodes`: All possible first moves from the starting position
|
||||
- `bestCommand`: Pointer to the optimal root command
|
||||
- `maxDepth`: Maximum lookahead depth of the tree
|
||||
- `totalNodes`: Total number of nodes in the tree (for statistics)
|
||||
|
||||
**Features**:
|
||||
- Provides complete visibility into AI decision-making process
|
||||
- Enables analysis of alternative moves and their consequences
|
||||
- Supports tree statistics and debugging information
|
||||
- Maintains backward compatibility through `GetBestCommandIndex()`
|
||||
|
||||
**Memory Management**:
|
||||
- Uses `std::unique_ptr` for automatic memory cleanup
|
||||
- `GameStateW` objects are stored directly (not shared pointers for simplicity)
|
||||
- Tree structure ensures proper cleanup when nodes go out of scope
|
||||
|
||||
### Tree vs. Legacy Approach Comparison
|
||||
|
||||
| Aspect | Legacy (Single Best) | Tree-Based (Complete) |
|
||||
|--------|---------------------|----------------------|
|
||||
| **Output** | Best command only | Complete decision tree |
|
||||
| **Memory** | Minimal | Higher (stores all paths) |
|
||||
| **Analysis** | Limited visibility | Full decision transparency |
|
||||
| **Debugging** | Single command info | Complete move sequences |
|
||||
| **Performance** | Slightly faster | Comparable (same calculations) |
|
||||
| **Compatibility** | Direct usage | Wrapper maintains compatibility |
|
||||
|
||||
### Usage Patterns
|
||||
|
||||
**For AI Decision Making**:
|
||||
```cpp
|
||||
auto treeFuture = BuildDecisionTree(pid, isDefender, depth, maxRepeat,
|
||||
engine, strategy, utility, settings,
|
||||
castles, apdCache, alCache, deadline);
|
||||
CommandDecisionTree tree = treeFuture.get();
|
||||
size_t bestCommand = tree.bestCommand->commandIndex;
|
||||
```
|
||||
|
||||
**For Analysis and Debugging**:
|
||||
```cpp
|
||||
CommandDecisionTree tree = treeFuture.get();
|
||||
// Examine all possible moves
|
||||
for (const auto& rootNode : tree.rootNodes) {
|
||||
std::cout << "Command " << rootNode->commandIndex
|
||||
<< " Score: " << rootNode->lookaheadScore << std::endl;
|
||||
// Traverse children to see consequences
|
||||
for (const auto& child : rootNode->children) {
|
||||
// ... analyze child moves
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Legacy Compatibility**:
|
||||
```cpp
|
||||
// Existing code continues to work unchanged
|
||||
auto indexScoreFuture = BestCommandIndex(pid, isDefender, ...);
|
||||
IndexAndScore result = indexScoreFuture.get();
|
||||
size_t bestCommand = result.index;
|
||||
```
|
||||
|
||||
The tree-based approach provides complete decision transparency while maintaining full backward compatibility with existing AI code.
|
||||
|
||||
## MCTS Alternative: Randomness Handling Recommendations
|
||||
|
||||
The new MCTS-based AI system is available in `MCTSAI.hpp/.cpp` and provides an alternative to the iterative deepening approach. However, the current MCTS implementation uses simplified randomness handling compared to the sophisticated approach in the original system.
|
||||
|
||||
### Current MCTS Limitations
|
||||
|
||||
1. **Expansion Phase**: Uses average rolls (0.5) for all commands during tree expansion
|
||||
2. **Simulation Phase**: Uses random command selection with average rolls
|
||||
3. **Missing**: No explicit chance nodes for commands with `HasOdds()`
|
||||
4. **Missing**: No multi-sample evaluation for stochastic commands
|
||||
|
||||
### Recommended Improvements: Chance Node Integration
|
||||
|
||||
#### 1. **Explicit Chance Nodes** (Highest Priority)
|
||||
|
||||
For commands with `HasOdds()`, create explicit chance nodes in the MCTS tree:
|
||||
|
||||
```cpp
|
||||
// During MCTSExpansion
|
||||
if (descriptor->HasOdds()) {
|
||||
// Create TWO child nodes: success and failure
|
||||
auto successNode = CreateMCTSNode(commandIndex, SUCCESS_VARIANT);
|
||||
auto failureNode = CreateMCTSNode(commandIndex, FAILURE_VARIANT);
|
||||
|
||||
// Execute with deterministic rolls (matching original system)
|
||||
ExecuteWithRoll(successNode, 1.0 - successChance/2.0); // High roll
|
||||
ExecuteWithRoll(failureNode, (1.0 - successChance)/2.0); // Low roll
|
||||
|
||||
// Set probability weights for selection
|
||||
successNode->probabilityWeight = successChance;
|
||||
failureNode->probabilityWeight = 1.0 - successChance;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. **Weighted Selection for Chance Nodes**
|
||||
|
||||
Modify `MCTSSelection` to handle chance nodes:
|
||||
|
||||
```cpp
|
||||
if (node->isChanceNode) {
|
||||
// Select based on probability distribution, not UCB1
|
||||
return SelectByProbability(node->children);
|
||||
} else {
|
||||
// Normal UCB1 selection for decision nodes
|
||||
return node->GetBestChild(explorationConstant);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. **Probability-Weighted Backpropagation**
|
||||
|
||||
Update backpropagation to account for chance node probabilities:
|
||||
|
||||
```cpp
|
||||
void MCTSBackpropagation(MCTSNode* node, double reward) {
|
||||
while (node) {
|
||||
node->visitCount++;
|
||||
|
||||
// Weight reward by probability for chance nodes
|
||||
double weightedReward = reward;
|
||||
if (node->parent && node->parent->isChanceNode) {
|
||||
weightedReward *= node->probabilityWeight;
|
||||
}
|
||||
|
||||
node->totalReward += weightedReward;
|
||||
node->averageReward = node->totalReward / node->visitCount;
|
||||
node = node->parent;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. **Multi-Sample Commands**
|
||||
|
||||
For commands without explicit odds but with randomness, use stratified sampling:
|
||||
|
||||
```cpp
|
||||
// During expansion, create multiple child nodes with different rolls
|
||||
for (int sample = 0; sample < numSamples; ++sample) {
|
||||
double roll = static_cast<double>(sample) / (numSamples - 1);
|
||||
auto sampleNode = CreateMCTSNodeWithRoll(commandIndex, roll);
|
||||
sampleNode->probabilityWeight = 1.0 / numSamples;
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits of Chance Node Integration
|
||||
|
||||
1. **Accurate Evaluation**: Preserves the sophisticated randomness handling from the original system
|
||||
2. **Better Convergence**: MCTS can properly explore both success/failure outcomes
|
||||
3. **Realistic Simulations**: Tree accurately represents game's probability distributions
|
||||
4. **Comparable Results**: Makes MCTS results directly comparable to iterative deepening
|
||||
|
||||
### Implementation Priority
|
||||
|
||||
1. **Phase 1**: Add explicit chance nodes for `HasOdds()` commands
|
||||
2. **Phase 2**: Implement probability-weighted selection and backpropagation
|
||||
3. **Phase 3**: Add multi-sample support for general stochastic commands
|
||||
4. **Phase 4**: Optimize performance with lazy expansion of chance nodes
|
||||
|
||||
### Alternative: Determinization Approach
|
||||
|
||||
If explicit chance nodes prove too complex, consider **determinization**:
|
||||
|
||||
- Run multiple MCTS trees with different fixed random seeds
|
||||
- Aggregate results across all determinizations
|
||||
- Simpler to implement but potentially less accurate than explicit chance nodes
|
||||
|
||||
### Switching Between AI Systems
|
||||
|
||||
Both AI systems (`IterativeDeepeningAI` and `MCTSAI`) implement compatible interfaces. The algorithm is selected at **runtime** via the ShardokAIClient constructor:
|
||||
|
||||
```cpp
|
||||
// Using Iterative Deepening (default)
|
||||
ShardokAIClient client(playerId, isDefender, hexMap, settings);
|
||||
// Or explicitly:
|
||||
ShardokAIClient client(playerId, isDefender, hexMap, settings,
|
||||
AIAlgorithmType::ITERATIVE_DEEPENING);
|
||||
|
||||
// Using MCTS
|
||||
ShardokAIClient client(playerId, isDefender, hexMap, settings,
|
||||
AIAlgorithmType::MCTS);
|
||||
|
||||
// Note: MCTS configuration can be customized via MCTSConfig:
|
||||
// - maxIterations: 10000 (max MCTS iterations per move)
|
||||
// - maxSimulationDepth: 10 (depth for rollout phase)
|
||||
// - maxTreeDepth: 20 (max tree depth to prevent stack overflow)
|
||||
// - explorationConstant: 1.414 (UCB1 exploration vs exploitation)
|
||||
// - useMultithreading: true (APD cache is thread-safe with TLS + mutex protection)
|
||||
// - numThreads: 4
|
||||
```
|
||||
|
||||
The selection is made per AI client instance, allowing different algorithms for different players or game situations within the same server process.
|
||||
|
||||
#### Direct AI Usage (Lower Level)
|
||||
|
||||
Both AI systems can also be used directly:
|
||||
|
||||
```cpp
|
||||
// Using Iterative Deepening directly
|
||||
auto iterativeAI = IterativeDeepeningAI(playerId, isDefender, strategy,
|
||||
castleCoords, apdCache, alCache);
|
||||
auto result = iterativeAI.IterativeSearch(settings, state, commands, budget);
|
||||
|
||||
// Using MCTS directly
|
||||
auto mctsAI = MCTSAI(playerId, isDefender, strategy,
|
||||
castleCoords, apdCache, alCache);
|
||||
auto result = mctsAI.Search(settings, state, commands, budget);
|
||||
```
|
||||
|
||||
#### Algorithm Comparison
|
||||
| Feature | Iterative Deepening | MCTS |
|
||||
|---------|-------------------|------|
|
||||
| **Randomness Handling** | Sophisticated (chance nodes, multi-sample) | Simplified (average rolls) |
|
||||
| **Performance** | Single-threaded | Multithreaded |
|
||||
| **Search Type** | Fixed depth with iterative deepening | Adaptive with time budget |
|
||||
| **Memory Usage** | Lower | Higher (maintains tree) |
|
||||
| **Max Tree Depth** | Limited by lookahead setting | Limited by `maxTreeDepth` config (default: 20) |
|
||||
| **Tree Destruction** | Not applicable | Iterative (avoids stack overflow) |
|
||||
| **Best For** | Precise evaluation, production | Performance testing, fast decisions |
|
||||
|
||||
The MCTS implementation provides a solid foundation. Known limitations:
|
||||
1. **Randomness Handling**: Simplified compared to iterative deepening (no explicit chance nodes)
|
||||
2. **Simulation Quality**: Uses random rollouts instead of sophisticated evaluation
|
||||
|
||||
Note: The APD cache is fully thread-safe using thread-local storage and mutex-protected shared cache.
|
||||
|
||||
<<<<<<< HEAD
|
||||
Adding chance node handling and ensuring thread safety would make it a superior replacement for the iterative deepening approach while maintaining the sophisticated randomness evaluation that makes the current system effective.
|
||||
|
||||
## MCTS Configuration Options
|
||||
|
||||
The MCTS AI system provides extensive configuration through the `MCTSConfig` structure:
|
||||
|
||||
### Core MCTS Parameters
|
||||
|
||||
```cpp
|
||||
struct MCTSConfig {
|
||||
double explorationConstant = 1.414; // UCB1 constant (sqrt(2) by default)
|
||||
int maxSimulationDepth = 1000; // Maximum depth for rollout
|
||||
int maxTreeDepth = 2000; // Maximum tree depth to prevent stack overflow
|
||||
bool useMultithreading = true; // Enable parallel MCTS
|
||||
int numThreads = 16; // Number of threads for parallel MCTS (when enabled)
|
||||
MCTSSimulationPolicy simulationPolicy = MCTSSimulationPolicy::BEST_IMMEDIATE;
|
||||
bool enableTranspositionDetection = true; // Enable pruning of duplicate states
|
||||
double immediateScoreTieBreakThreshold = 5.0; // When avg rewards differ by less than this, prefer higher immediate score
|
||||
double visitCountTolerance = 0.05; // Treat visit counts as equal if within this % of best count
|
||||
bool enableImmediateScoreInUCB1 = true; // Apply immediate score tie-breaking in UCB1 selection too
|
||||
};
|
||||
```
|
||||
|
||||
### Exploration vs Exploitation
|
||||
|
||||
- **`explorationConstant`**: Controls the exploration vs exploitation balance in UCB1 selection
|
||||
- Higher values (>1.414): More exploration of unvisited nodes
|
||||
- Lower values (<1.414): More exploitation of known good moves
|
||||
- Default: 1.414 (√2, theoretical optimum for UCB1)
|
||||
|
||||
### Tree Structure Limits
|
||||
|
||||
- **`maxTreeDepth`**: Prevents stack overflow in deep game trees
|
||||
- Default: 2000 (very high limit for most tactical scenarios)
|
||||
- Terminal detection stops expansion when this depth is reached
|
||||
|
||||
- **`maxSimulationDepth`**: Controls rollout length during simulation phase
|
||||
- Default: 1000 (sufficient for most tactical scenarios)
|
||||
- Longer simulations provide more accurate estimates but use more time
|
||||
|
||||
### Multithreading Configuration
|
||||
|
||||
- **`useMultithreading`**: Enable/disable parallel MCTS execution
|
||||
- Default: true (takes advantage of modern multi-core CPUs)
|
||||
- Requires thread-safe game engine and scoring components
|
||||
|
||||
- **`numThreads`**: Number of worker threads for parallel tree building
|
||||
- Default: 16 (adjust based on available CPU cores)
|
||||
- More threads can improve search speed but with diminishing returns
|
||||
|
||||
### Simulation Policies
|
||||
|
||||
The `MCTSSimulationPolicy` enum controls how commands are selected during the rollout phase:
|
||||
|
||||
- **`RANDOM`**: Pure random selection from all available commands
|
||||
- Fastest but least informed simulations
|
||||
- Good baseline for testing MCTS convergence
|
||||
|
||||
- **`FILTERED_RANDOM`**: Random selection from AICommandFilter-approved commands
|
||||
- Eliminates obviously bad moves (moving away from objectives, etc.)
|
||||
- Better simulation quality with minimal overhead
|
||||
|
||||
- **`BEST_IMMEDIATE`**: Always choose command with highest immediate score
|
||||
- Most informed simulations
|
||||
- Slower but higher quality rollouts
|
||||
- Default setting for production use
|
||||
|
||||
- **`WEIGHTED_BEST_IMMEDIATE`**: Random selection weighted by immediate score ranking
|
||||
- Balances exploration with informed choice
|
||||
- Alternative to pure greedy selection
|
||||
|
||||
### Transposition Detection
|
||||
|
||||
- **`enableTranspositionDetection`**: Enable pruning of duplicate game states
|
||||
- Default: true (improves search efficiency)
|
||||
- Uses hash-based state identification
|
||||
- Prevents wasted computation on equivalent positions reached via different move sequences
|
||||
|
||||
### Immediate Score Tie-Breaking
|
||||
|
||||
These settings address MCTS's tendency to choose indirect paths when direct paths lead to the same outcome:
|
||||
|
||||
- **`immediateScoreTieBreakThreshold`**: Score difference threshold for tie-breaking
|
||||
- Default: 5.0 (when backpropagated rewards differ by less than this, prefer immediate score)
|
||||
- Helps AI choose direct moves over equivalent indirect sequences
|
||||
- Improves user experience by reducing unnecessary intermediate moves
|
||||
|
||||
- **`visitCountTolerance`**: Visit count equality threshold for tie-breaking
|
||||
- Default: 0.05 (5% tolerance - visit counts within this percentage are considered equal)
|
||||
- Prevents minor visit count differences from overriding immediate score preferences
|
||||
|
||||
- **`enableImmediateScoreInUCB1`**: Apply immediate score tie-breaking during exploration
|
||||
- Default: true (consistent tie-breaking in both exploration and final selection)
|
||||
- When UCB1 values are very close, prefer nodes with higher immediate scores
|
||||
- Improves convergence on direct paths to objectives
|
||||
|
||||
### Usage Example
|
||||
|
||||
```cpp
|
||||
// Custom MCTS configuration for performance testing
|
||||
MCTSConfig config;
|
||||
config.explorationConstant = 2.0; // More exploration
|
||||
config.simulationPolicy = MCTSSimulationPolicy::FILTERED_RANDOM; // Faster rollouts
|
||||
config.numThreads = 8; // Reduce threads for testing environment
|
||||
config.immediateScoreTieBreakThreshold = 10.0; // More aggressive tie-breaking
|
||||
|
||||
MCTSAI ai(playerId, isDefender, strategy, castleCoords, apdCache, alCache, config);
|
||||
```
|
||||
|
||||
### Configuration Recommendations
|
||||
|
||||
**For Production Use:**
|
||||
- Use default settings for balanced performance and quality
|
||||
- Consider reducing `numThreads` on systems with limited CPU cores
|
||||
- `BEST_IMMEDIATE` simulation policy provides highest quality decisions
|
||||
|
||||
**For Performance Testing:**
|
||||
- `FILTERED_RANDOM` or `RANDOM` simulation policies for faster rollouts
|
||||
- Lower `explorationConstant` (1.0) for more exploitation
|
||||
- Disable transposition detection for baseline comparison
|
||||
|
||||
**For Analysis/Debugging:**
|
||||
- Single-threaded execution (`useMultithreading = false`) for deterministic results
|
||||
- Higher `immediateScoreTieBreakThreshold` to emphasize direct paths
|
||||
- `BEST_IMMEDIATE` simulation for most predictable behavior
|
||||
|
||||
The configuration system allows fine-tuning MCTS behavior for different scenarios while maintaining compatibility with the existing AI infrastructure.
|
||||
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.
|
||||
@@ -154,7 +154,6 @@ cc_library(
|
||||
hdrs = ["AICommandFilter.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
@@ -189,7 +188,6 @@ cc_library(
|
||||
hdrs = ["AIScoreCalculator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
@@ -211,7 +209,6 @@ cc_library(
|
||||
hdrs = ["AIStrategy.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
|
||||
],
|
||||
@@ -299,7 +296,6 @@ cc_library(
|
||||
hdrs = ["AITimeBudget.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
@@ -318,7 +314,6 @@ cc_library(
|
||||
hdrs = ["IterativeDeepeningAI.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
@@ -335,53 +330,6 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_config",
|
||||
hdrs = ["AIConfig.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_mcts_clean",
|
||||
srcs = ["MCTSCleanAI.cpp"],
|
||||
hdrs = ["MCTSCleanAI.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":ai_attack_locations",
|
||||
":ai_config",
|
||||
":ai_iterative_deepening", # For SearchResult compatibility
|
||||
":ai_score_calculator",
|
||||
":ai_strategy",
|
||||
":ai_time_budget",
|
||||
"//src/main/cpp/net/eagle0/common:random_generator",
|
||||
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
|
||||
"//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/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//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_config",
|
||||
hdrs = ["AIConfig.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "shardok_ai_client",
|
||||
srcs = ["ShardokAIClient.cpp"],
|
||||
@@ -390,15 +338,13 @@ cc_library(
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":ai_attacker_strategy_selector",
|
||||
":ai_config",
|
||||
":ai_defender_strategy_selector",
|
||||
":ai_flee_decision_calculator",
|
||||
":ai_iterative_deepening", # Direct dependency for runtime selection
|
||||
":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/ai/mcts:mcts_ai", # Direct dependency for runtime selection
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"@com_google_protobuf//:protobuf",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,105 +0,0 @@
|
||||
//
|
||||
// MCTS-based AI system for Shardok
|
||||
// Alternative to IterativeDeepeningAI using Monte Carlo Tree Search
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_MCTSAI_HPP
|
||||
#define EAGLE0_MCTSAI_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "AIStrategy.hpp"
|
||||
#include "AITimeBudget.hpp"
|
||||
#include "IterativeDeepeningAI.hpp" // For SearchResult compatibility
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class ShardokEngine;
|
||||
struct MCTSNode;
|
||||
|
||||
// Configuration for MCTS algorithm
|
||||
struct MCTSConfig {
|
||||
double explorationConstant = 1.414; // UCB1 constant (sqrt(2) by default)
|
||||
int maxPlayerFlips = 1; // Number of player turn changes to evaluate
|
||||
bool useMultithreading = true; // Enable parallel MCTS
|
||||
int numThreads = 16; // Number of threads for parallel MCTS (when enabled)
|
||||
};
|
||||
|
||||
class MCTSAI {
|
||||
public:
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
using SearchResult = IterativeDeepeningAI::SearchResult;
|
||||
|
||||
MCTSAI(PlayerId playerId,
|
||||
bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
MCTSConfig config = MCTSConfig{});
|
||||
|
||||
// Main search interface - compatible with IterativeDeepeningAI
|
||||
[[nodiscard]] auto Search(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
const std::vector<CommandProto>& commands,
|
||||
const AITimeBudget& budget) const -> SearchResult;
|
||||
|
||||
// Get/set configuration
|
||||
[[nodiscard]] auto GetConfig() const -> const MCTSConfig& { return config; }
|
||||
void SetConfig(const MCTSConfig& newConfig) { config = newConfig; }
|
||||
|
||||
private:
|
||||
PlayerId playerId;
|
||||
bool isDefender;
|
||||
AIStrategy strategy;
|
||||
const CoordsSet& castleCoords;
|
||||
const APDCache& apdCache;
|
||||
const ALCache& alCache;
|
||||
MCTSConfig config;
|
||||
|
||||
// Internal MCTS tree building
|
||||
[[nodiscard]] auto BuildMCTSTree(
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::unique_ptr<MCTSNode>;
|
||||
|
||||
// MCTS algorithm phases
|
||||
auto MCTSSelection(MCTSNode* root) const -> MCTSNode*;
|
||||
auto MCTSExpansion(
|
||||
MCTSNode* node,
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter) const -> MCTSNode*;
|
||||
auto MCTSSimulation(
|
||||
const ShardokEngine& engineState,
|
||||
PlayerId currentPlayer,
|
||||
const SettingsGetter& settingsGetter) const -> double;
|
||||
auto MCTSBackpropagation(MCTSNode* node, double reward) const -> void;
|
||||
|
||||
// Helper functions
|
||||
[[nodiscard]] auto IsTerminalForPlayer(
|
||||
const GameStateW& gameState,
|
||||
PlayerId currentPlayer,
|
||||
const SettingsGetter& settingsGetter) const -> bool;
|
||||
|
||||
// Get coordinate information for logging
|
||||
[[nodiscard]] std::string GetCommandCoordinateInfo(
|
||||
net::eagle0::shardok::common::CommandType commandType,
|
||||
size_t commandIndex,
|
||||
const GameStateW& gameState,
|
||||
const GameSettingsSPtr& settings) const;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_MCTSAI_HPP
|
||||
@@ -1,292 +0,0 @@
|
||||
#include "MCTSCleanAI.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include "AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
MCTSCleanAI::MCTSCleanAI(
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
const AIStrategy& strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache)
|
||||
: ourPlayerId(playerId),
|
||||
isDefender(isDefender),
|
||||
strategy(strategy),
|
||||
castleCoords(castleCoords),
|
||||
apdCache(apdCache),
|
||||
alCache(alCache),
|
||||
rng(std::chrono::steady_clock::now().time_since_epoch().count()) {}
|
||||
|
||||
IterativeDeepeningAI::SearchResult MCTSCleanAI::Search(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& gameState,
|
||||
const std::vector<CommandProto>& availableCommands,
|
||||
const AITimeBudget& timeBudget) {
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
const auto maxTime = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
timeBudget.remainingBudget * 0.9); // 90% of budget
|
||||
|
||||
// Create root node
|
||||
auto root = std::make_unique<MCTSNode>();
|
||||
root->resultingGameState = gameState;
|
||||
root->currentPlayer = ourPlayerId;
|
||||
root->isOurTurn = true;
|
||||
root->playerFlipsFromRoot = 0;
|
||||
|
||||
// Initialize untried commands for root
|
||||
for (size_t i = 0; i < availableCommands.size(); ++i) { root->untriedCommands.push_back(i); }
|
||||
|
||||
int iterationCount = 0;
|
||||
|
||||
// Main MCTS loop
|
||||
while (std::chrono::steady_clock::now() - startTime < maxTime) {
|
||||
// 1. Selection - find leaf node to expand
|
||||
MCTSNode* selected = Selection(root.get());
|
||||
|
||||
// 2. Expansion - add new child if possible
|
||||
MCTSNode* expanded = Expansion(selected, settings, availableCommands);
|
||||
|
||||
// 3. Simulation - run random playout
|
||||
double reward = Simulation(
|
||||
expanded->resultingGameState,
|
||||
expanded->currentPlayer,
|
||||
expanded->playerFlipsFromRoot,
|
||||
settings);
|
||||
|
||||
// 4. Backpropagation - update statistics
|
||||
Backpropagation(expanded, reward);
|
||||
|
||||
iterationCount++;
|
||||
|
||||
// Early exit if all commands tried
|
||||
if (root->untriedCommands.empty() && root->children.size() == availableCommands.size()) {
|
||||
bool allChildrenFullyExplored = true;
|
||||
for (const auto& child : root->children) {
|
||||
if (child->visitCount < 10) { // Minimum visits per child
|
||||
allChildrenFullyExplored = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allChildrenFullyExplored) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Select best command based on visit counts
|
||||
size_t bestCommandIndex = 0;
|
||||
int maxVisits = 0;
|
||||
|
||||
for (size_t i = 0; i < root->children.size(); ++i) {
|
||||
if (root->children[i]->visitCount > maxVisits) {
|
||||
maxVisits = root->children[i]->visitCount;
|
||||
bestCommandIndex = root->children[i]->commandIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Create search result
|
||||
IterativeDeepeningAI::SearchResult result;
|
||||
result.bestCommandIndex = bestCommandIndex;
|
||||
result.availableCommandCount = availableCommands.size();
|
||||
result.depthAchieved = maxPlayerFlips; // Our max search depth
|
||||
result.commandCountEvaluated = iterationCount;
|
||||
result.completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
|
||||
|
||||
printf("MCTS Clean: %d iterations, best command %zu with %d visits\n",
|
||||
iterationCount,
|
||||
bestCommandIndex,
|
||||
maxVisits);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Selection - navigate to leaf using UCB1
|
||||
MCTSCleanAI::MCTSNode* MCTSCleanAI::Selection(MCTSNode* root) {
|
||||
MCTSNode* current = root;
|
||||
|
||||
while (!current->children.empty() && current->untriedCommands.empty()) {
|
||||
current = SelectChild(current);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
// Expansion - add new child node
|
||||
MCTSCleanAI::MCTSNode* MCTSCleanAI::Expansion(
|
||||
MCTSNode* node,
|
||||
const GameSettingsSPtr& settings,
|
||||
const std::vector<CommandProto>& availableCommands) {
|
||||
// If we've reached max player flips, don't expand
|
||||
if (node->playerFlipsFromRoot >= maxPlayerFlips) { return node; }
|
||||
|
||||
// If no untried commands, return current node
|
||||
if (node->untriedCommands.empty()) { return node; }
|
||||
|
||||
// Pick an untried command
|
||||
size_t cmdIndex = PickUntriedCommand(node);
|
||||
|
||||
// Create child engine and execute command
|
||||
auto childEngine = CreateEngine(node->resultingGameState, settings);
|
||||
PlayerId playerBefore = childEngine->GetCurrentPlayerId();
|
||||
|
||||
// Use sequence random generator for deterministic results
|
||||
auto averageGenerator = std::make_shared<SequenceRandomGenerator>(std::vector<double>{0.5});
|
||||
childEngine->PostCommand(playerBefore, cmdIndex, averageGenerator);
|
||||
|
||||
PlayerId playerAfter = childEngine->GetCurrentPlayerId();
|
||||
|
||||
// Create child node
|
||||
bool isOurTurnAfter = (playerAfter == ourPlayerId);
|
||||
auto child = std::make_unique<MCTSNode>(
|
||||
cmdIndex,
|
||||
availableCommands[cmdIndex].type(),
|
||||
playerAfter,
|
||||
isOurTurnAfter);
|
||||
child->resultingGameState = childEngine->GetCurrentGameState();
|
||||
child->parent = node;
|
||||
|
||||
// Track player flips
|
||||
child->playerFlipsFromRoot = node->playerFlipsFromRoot;
|
||||
if (playerBefore != playerAfter) { child->playerFlipsFromRoot++; }
|
||||
|
||||
// Initialize child's untried commands if we haven't hit max flips
|
||||
if (child->playerFlipsFromRoot < maxPlayerFlips) {
|
||||
auto childCommands = childEngine->GetAvailableCommandsForAIPlayer(playerAfter);
|
||||
for (size_t i = 0; i < childCommands->size(); ++i) { child->untriedCommands.push_back(i); }
|
||||
}
|
||||
|
||||
MCTSNode* childPtr = child.get();
|
||||
node->children.push_back(std::move(child));
|
||||
|
||||
return childPtr;
|
||||
}
|
||||
|
||||
// Simulation - run random playout from current state
|
||||
double MCTSCleanAI::Simulation(
|
||||
const GameStateW& startState,
|
||||
PlayerId /* startPlayer */,
|
||||
int startFlips,
|
||||
const GameSettingsSPtr& settings) {
|
||||
auto simEngine = CreateEngine(startState, settings);
|
||||
int currentFlips = startFlips;
|
||||
auto averageGenerator = std::make_shared<SequenceRandomGenerator>(std::vector<double>{0.5});
|
||||
|
||||
while (currentFlips < maxPlayerFlips) {
|
||||
PlayerId currentPlayer = simEngine->GetCurrentPlayerId();
|
||||
bool isOurTurn = (currentPlayer == ourPlayerId);
|
||||
|
||||
// Get available commands
|
||||
auto commands = simEngine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
if (commands->empty()) break;
|
||||
|
||||
// Pick best command based on whose turn it is
|
||||
size_t bestCmd = 0;
|
||||
double bestScore = isOurTurn ? -std::numeric_limits<double>::infinity()
|
||||
: std::numeric_limits<double>::infinity();
|
||||
|
||||
for (size_t i = 0; i < commands->size(); ++i) {
|
||||
auto testEngine = CreateEngine(simEngine->GetCurrentGameState(), settings);
|
||||
testEngine->PostCommand(currentPlayer, i, averageGenerator);
|
||||
|
||||
// Always score from our perspective
|
||||
double score = ScoreFromOurPerspective(
|
||||
testEngine->GetCurrentGameState(),
|
||||
settings->GetGetter());
|
||||
|
||||
// Our turn: maximize our score, Opponent turn: minimize our score
|
||||
bool shouldSelect = isOurTurn ? (score > bestScore) : (score < bestScore);
|
||||
if (shouldSelect) {
|
||||
bestScore = score;
|
||||
bestCmd = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute chosen command
|
||||
PlayerId playerBefore = simEngine->GetCurrentPlayerId();
|
||||
simEngine->PostCommand(currentPlayer, bestCmd, averageGenerator);
|
||||
PlayerId playerAfter = simEngine->GetCurrentPlayerId();
|
||||
|
||||
// Track player flips
|
||||
if (playerBefore != playerAfter) { currentFlips++; }
|
||||
}
|
||||
|
||||
return ScoreFromOurPerspective(simEngine->GetCurrentGameState(), settings->GetGetter());
|
||||
}
|
||||
|
||||
// Backpropagation - update node statistics
|
||||
void MCTSCleanAI::Backpropagation(MCTSNode* node, double score) {
|
||||
while (node != nullptr) {
|
||||
node->visitCount++;
|
||||
node->totalScore += score; // Always from our perspective
|
||||
node->averageScore = node->totalScore / node->visitCount;
|
||||
node = node->parent;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
double MCTSCleanAI::ScoreFromOurPerspective(
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settingsGetter) {
|
||||
return AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
gameState,
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
}
|
||||
|
||||
MCTSCleanAI::MCTSNode* MCTSCleanAI::SelectChild(MCTSNode* node) {
|
||||
MCTSNode* bestChild = nullptr;
|
||||
double bestUCB1 = node->isOurTurn ? -std::numeric_limits<double>::infinity()
|
||||
: std::numeric_limits<double>::infinity();
|
||||
|
||||
for (const auto& child : node->children) {
|
||||
double exploitation = child->averageScore;
|
||||
double exploration =
|
||||
explorationConstant * std::sqrt(std::log(node->visitCount) / child->visitCount);
|
||||
double ucb1 = exploitation + exploration;
|
||||
|
||||
// Our turn: pick highest UCB1, Opponent turn: pick lowest UCB1
|
||||
bool shouldSelect = node->isOurTurn ? (ucb1 > bestUCB1) : (ucb1 < bestUCB1);
|
||||
|
||||
if (shouldSelect) {
|
||||
bestUCB1 = ucb1;
|
||||
bestChild = child.get();
|
||||
}
|
||||
}
|
||||
|
||||
return bestChild;
|
||||
}
|
||||
|
||||
size_t MCTSCleanAI::PickUntriedCommand(MCTSNode* node) {
|
||||
if (node->untriedCommands.empty()) {
|
||||
return 0; // Should not happen
|
||||
}
|
||||
|
||||
// Pick random untried command
|
||||
std::uniform_int_distribution<size_t> indexDist(0, node->untriedCommands.size() - 1);
|
||||
size_t randomIndex = indexDist(rng);
|
||||
|
||||
size_t cmdIndex = node->untriedCommands[randomIndex];
|
||||
node->untriedCommands.erase(node->untriedCommands.begin() + randomIndex);
|
||||
|
||||
return cmdIndex;
|
||||
}
|
||||
|
||||
std::shared_ptr<ShardokEngine> MCTSCleanAI::CreateEngine(
|
||||
const GameStateW& gameState,
|
||||
const GameSettingsSPtr& settings) {
|
||||
return std::make_shared<ShardokEngine>(settings, gameState);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,133 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#include "AIAttackLocations.hpp"
|
||||
#include "AIConfig.hpp"
|
||||
#include "AIStrategy.hpp"
|
||||
#include "AITimeBudget.hpp"
|
||||
#include "IterativeDeepeningAI.hpp" // For SearchResult
|
||||
#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/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/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declare cache type aliases
|
||||
using APDCache = std::shared_ptr<ActionPointDistancesCache>;
|
||||
using ALCache = std::unique_ptr<AttackLocationsCache>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Clean MCTS implementation following the design document
|
||||
class MCTSCleanAI {
|
||||
public:
|
||||
// Clean node structure with single perspective scoring
|
||||
struct MCTSNode {
|
||||
// Command that led to this state
|
||||
size_t commandIndex;
|
||||
net::eagle0::shardok::common::CommandType commandType;
|
||||
|
||||
// Game state after executing the command
|
||||
GameStateW resultingGameState;
|
||||
PlayerId currentPlayer; // Whose turn it is in this state
|
||||
|
||||
// Tree position
|
||||
int playerFlipsFromRoot; // Number of player changes from root
|
||||
bool isOurTurn; // currentPlayer == our AI's playerId
|
||||
|
||||
// MCTS statistics (always from our perspective)
|
||||
int visitCount = 0;
|
||||
double totalScore = 0.0;
|
||||
double averageScore = 0.0;
|
||||
|
||||
// Tree structure
|
||||
std::vector<std::unique_ptr<MCTSNode>> children;
|
||||
std::vector<size_t> untriedCommands;
|
||||
MCTSNode* parent = nullptr;
|
||||
|
||||
// Constructor
|
||||
MCTSNode(
|
||||
size_t cmdIndex,
|
||||
net::eagle0::shardok::common::CommandType cmdType,
|
||||
PlayerId currentPlayerId,
|
||||
bool ourTurn)
|
||||
: commandIndex(cmdIndex),
|
||||
commandType(cmdType),
|
||||
currentPlayer(currentPlayerId),
|
||||
playerFlipsFromRoot(0),
|
||||
isOurTurn(ourTurn) {}
|
||||
|
||||
// Root constructor
|
||||
MCTSNode()
|
||||
: commandIndex(0),
|
||||
commandType(net::eagle0::shardok::common::UNKNOWN_COMMAND),
|
||||
currentPlayer(0),
|
||||
playerFlipsFromRoot(0),
|
||||
isOurTurn(true) {}
|
||||
};
|
||||
|
||||
MCTSCleanAI(
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
const AIStrategy& strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache);
|
||||
|
||||
// Main search function compatible with existing interface
|
||||
IterativeDeepeningAI::SearchResult Search(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& gameState,
|
||||
const std::vector<CommandProto>& availableCommands,
|
||||
const AITimeBudget& timeBudget);
|
||||
|
||||
private:
|
||||
PlayerId ourPlayerId;
|
||||
bool isDefender;
|
||||
AIStrategy strategy;
|
||||
CoordsSet castleCoords;
|
||||
const APDCache& apdCache;
|
||||
const ALCache& alCache;
|
||||
|
||||
// MCTS parameters
|
||||
static constexpr int maxPlayerFlips = 5; // Stop after N player changes
|
||||
static constexpr double explorationConstant = 1.414; // sqrt(2)
|
||||
|
||||
// Random number generation
|
||||
std::mt19937 rng;
|
||||
std::uniform_real_distribution<double> uniformDist{0.0, 1.0};
|
||||
|
||||
// Core MCTS algorithms
|
||||
MCTSNode* Selection(MCTSNode* root);
|
||||
MCTSNode* Expansion(
|
||||
MCTSNode* node,
|
||||
const GameSettingsSPtr& settings,
|
||||
const std::vector<CommandProto>& availableCommands);
|
||||
double Simulation(
|
||||
const GameStateW& startState,
|
||||
PlayerId startPlayer,
|
||||
int startFlips,
|
||||
const GameSettingsSPtr& settings);
|
||||
void Backpropagation(MCTSNode* node, double score);
|
||||
|
||||
// Helper functions
|
||||
double ScoreFromOurPerspective(
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settingsGetter);
|
||||
MCTSNode* SelectChild(MCTSNode* node);
|
||||
size_t PickUntriedCommand(MCTSNode* node);
|
||||
std::shared_ptr<ShardokEngine> CreateEngine(
|
||||
const GameStateW& gameState,
|
||||
const GameSettingsSPtr& settings);
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,147 +0,0 @@
|
||||
# MCTS Player-Flip Depth System Design
|
||||
|
||||
## Overview
|
||||
Replace command-count-based depth limits with player-turn-aware depth that naturally aligns with game structure. All nodes evaluate to the same player-flip depth to ensure comparable scores.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Player-Flip Depth
|
||||
- **Depth 1**: Evaluate until first player flip (complete our turn)
|
||||
- **Depth 2**: Continue through opponent's full turn
|
||||
- **Depth 3**: Continue through our next full turn
|
||||
- **Depth N**: N complete player turn changes
|
||||
|
||||
### Minimax Selection
|
||||
- **Always evaluate from original AI's perspective**
|
||||
- **Our turn**: Select moves that maximize our score
|
||||
- **Opponent's turn**: Select moves that minimize our score
|
||||
- Same backpropagation value regardless of whose turn
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Configuration Changes
|
||||
|
||||
```cpp
|
||||
struct MCTSConfig {
|
||||
double explorationConstant = 1.414;
|
||||
int maxPlayerFlips = 2; // How many player changes to evaluate
|
||||
bool useMultithreading = true;
|
||||
int numThreads = 16;
|
||||
// REMOVED: maxTreeDepth - all nodes go to same player-flip depth
|
||||
// REMOVED: maxSimulationDepth - replaced by maxPlayerFlips
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Node Structure Updates
|
||||
|
||||
```cpp
|
||||
struct MCTSNode {
|
||||
// Existing fields...
|
||||
|
||||
// New fields for player-flip tracking
|
||||
int playerFlipsFromRoot = 0;
|
||||
bool isMaximizingPlayer = true; // true = our turn, false = opponent's
|
||||
|
||||
// Modified selection for minimax
|
||||
MCTSNode* GetBestChild(double explorationConstant) {
|
||||
if (isMaximizingPlayer) {
|
||||
return GetChildWithHighestUCB1(explorationConstant);
|
||||
} else {
|
||||
return GetChildWithLowestUCB1(explorationConstant);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Expansion Rules
|
||||
|
||||
- **Continue expanding** until reaching `maxPlayerFlips` player changes
|
||||
- **No arbitrary depth limit** - accept theoretical stack overflow risk
|
||||
- **Mark as terminal** only when:
|
||||
- Game is over
|
||||
- Reached `maxPlayerFlips` player changes
|
||||
- No available commands
|
||||
|
||||
### 4. Key Implementation Details
|
||||
|
||||
#### Terminal Detection
|
||||
```cpp
|
||||
bool IsTerminalForExpansion() {
|
||||
return playerFlipsFromRoot >= maxPlayerFlips ||
|
||||
gameIsOver ||
|
||||
noCommandsAvailable;
|
||||
}
|
||||
```
|
||||
|
||||
#### Player Tracking
|
||||
```cpp
|
||||
// When expanding END_TURN or END_PLAYER_SETUP
|
||||
child->playerFlipsFromRoot = parent->playerFlipsFromRoot + 1;
|
||||
child->isMaximizingPlayer = !parent->isMaximizingPlayer;
|
||||
```
|
||||
|
||||
#### UCB1 for Minimax
|
||||
- Maximizing player: Choose highest UCB1
|
||||
- Minimizing player: Choose lowest UCB1
|
||||
- Unvisited nodes: Extreme values to force exploration
|
||||
|
||||
### 5. Simulation Strategy
|
||||
|
||||
Simulations run until `maxPlayerFlips` is reached:
|
||||
- Random moves for both players
|
||||
- Stop at player-flip boundaries
|
||||
- Always evaluate from original AI perspective
|
||||
|
||||
## Benefits
|
||||
|
||||
### Consistent Evaluation
|
||||
- All leaf nodes at same player-flip depth
|
||||
- Scores are directly comparable
|
||||
- No apples-to-oranges comparison issues
|
||||
|
||||
### Natural Game Structure
|
||||
- Respects turn boundaries
|
||||
- Complete tactical sequences evaluated
|
||||
- Opponent responses properly modeled
|
||||
|
||||
### Strategic Depth Control
|
||||
- **Setup/Early**: `maxPlayerFlips = 1` (fast, local tactics)
|
||||
- **Mid-game**: `maxPlayerFlips = 2` (balanced)
|
||||
- **Critical**: `maxPlayerFlips = 3+` (deep strategy)
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Phase 1**: Update MCTSConfig
|
||||
- Remove `maxTreeDepth` and `maxSimulationDepth`
|
||||
- Add `maxPlayerFlips`
|
||||
|
||||
2. **Phase 2**: Modify MCTSNode
|
||||
- Add `playerFlipsFromRoot` and `isMaximizingPlayer`
|
||||
- Update child selection for minimax
|
||||
|
||||
3. **Phase 3**: Update Expansion
|
||||
- Track player flips
|
||||
- Remove depth-based termination
|
||||
- Only terminate at player-flip boundaries
|
||||
|
||||
4. **Phase 4**: Fix Selection
|
||||
- Implement minimax selection based on `isMaximizingPlayer`
|
||||
- Modify UCB1 interpretation
|
||||
|
||||
5. **Phase 5**: Update Simulation
|
||||
- Run until `maxPlayerFlips` reached
|
||||
- Handle both player perspectives
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. Verify all evaluations reach same player-flip depth
|
||||
2. Confirm opponent chooses minimizing moves
|
||||
3. Test with different `maxPlayerFlips` settings
|
||||
4. Validate score consistency across tree
|
||||
|
||||
## Notes
|
||||
|
||||
- Stack overflow risk accepted for evaluation consistency
|
||||
- Each unit can make multiple moves per turn (move + scout + attack)
|
||||
- Maximum ~10 units per player limits practical depth
|
||||
- END_TURN and END_PLAYER_SETUP both count as player flips
|
||||
@@ -13,13 +13,11 @@
|
||||
#include <google/protobuf/util/message_differencer.h>
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AIConfig.hpp" // Must come before other AI includes
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
#include "AIFleeDecisionCalculator.hpp"
|
||||
#include "AIScoreUtilities.hpp"
|
||||
#include "AITimeBudget.hpp"
|
||||
#include "IterativeDeepeningAI.hpp"
|
||||
#include "mcts/MCTSAI.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"
|
||||
@@ -44,11 +42,9 @@ ShardokAIClient::ShardokAIClient(
|
||||
const PlayerId playerId,
|
||||
const bool isDefender,
|
||||
const HexMap *hexMap,
|
||||
const SettingsGetter &settings,
|
||||
const AIAlgorithmType aiAlgorithmType)
|
||||
const SettingsGetter &settings)
|
||||
: playerId(playerId),
|
||||
isDefender(isDefender),
|
||||
aiAlgorithmType(aiAlgorithmType),
|
||||
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
|
||||
waterCrossingCommandChooser(playerId, apdCache) {
|
||||
// Pre-generate the most common cache entries for better performance
|
||||
@@ -125,19 +121,11 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
waterCrossingCommandChooser,
|
||||
realAvailableCommands);
|
||||
|
||||
// AI implementation chosen at runtime via constructor parameter
|
||||
IterativeDeepeningAI::SearchResult search_result;
|
||||
|
||||
if (aiAlgorithmType == AIAlgorithmType::MCTS) {
|
||||
// Using Monte Carlo Tree Search AI
|
||||
MCTSAI ai(playerId, isDefender, strategy, castleCoords, apdCache, alCache);
|
||||
search_result = ai.Search(settings, guessedState, realAvailableCommands, timeBudget);
|
||||
} else {
|
||||
// Using Iterative Deepening AI (default)
|
||||
IterativeDeepeningAI ai(playerId, isDefender, strategy, castleCoords, apdCache, alCache);
|
||||
search_result =
|
||||
ai.IterativeSearch(settings, guessedState, realAvailableCommands, timeBudget);
|
||||
}
|
||||
// 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;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIConfig.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"
|
||||
@@ -39,7 +38,6 @@ class ShardokAIClient {
|
||||
private:
|
||||
const PlayerId playerId;
|
||||
const bool isDefender;
|
||||
const AIAlgorithmType aiAlgorithmType;
|
||||
|
||||
APDCache apdCache = std::make_shared<ActionPointDistancesCache>();
|
||||
ALCache alCache;
|
||||
@@ -69,8 +67,7 @@ public:
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
const HexMap* hexMap,
|
||||
const SettingsGetter& settings,
|
||||
AIAlgorithmType aiAlgorithmType = AIAlgorithmType::ITERATIVE_DEEPENING);
|
||||
const SettingsGetter& settings);
|
||||
~ShardokAIClient() = default;
|
||||
|
||||
[[nodiscard]] auto GetPlayerId() const -> PlayerId { return playerId; }
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "mcts_ai",
|
||||
srcs = ["MCTSAI.cpp"],
|
||||
hdrs = ["MCTSAI.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:random_generator",
|
||||
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_command_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_iterative_deepening", # For SearchResult compatibility
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_strategy",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_time_budget",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts/internal:mcts_node",
|
||||
"//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/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -1,869 +0,0 @@
|
||||
//
|
||||
// MCTS-based AI implementation for Shardok
|
||||
//
|
||||
|
||||
#include "MCTSAI.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
|
||||
#include "internal/MCTSNode.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.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 {
|
||||
|
||||
// Type alias for internal MCTSNode
|
||||
using MCTSNode = internal::MCTSNode;
|
||||
|
||||
// Static helper for average random generator
|
||||
static const std::vector _averageSequence = {0.5};
|
||||
static const auto _averageGenerator = std::make_shared<SequenceRandomGenerator>(_averageSequence);
|
||||
|
||||
MCTSAI::MCTSAI(
|
||||
const PlayerId playerId,
|
||||
const bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
MCTSConfig config)
|
||||
: playerId(playerId),
|
||||
isDefender(isDefender),
|
||||
strategy(std::move(strategy)),
|
||||
castleCoords(castleCoords),
|
||||
apdCache(apdCache),
|
||||
alCache(alCache),
|
||||
config(std::move(config)) {}
|
||||
|
||||
auto MCTSAI::Search(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
const std::vector<CommandProto>& commands,
|
||||
const AITimeBudget& budget) const -> SearchResult {
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
SearchResult result;
|
||||
|
||||
if (commands.empty()) {
|
||||
result.searchCompleted = true;
|
||||
result.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (commands.size() == 1) {
|
||||
result.searchCompleted = true;
|
||||
result.bestCommandIndex = 0;
|
||||
result.bestScore = 0;
|
||||
result.availableCommandCount = 1;
|
||||
result.depthAchieved = 1;
|
||||
result.commandCountEvaluated = 1;
|
||||
result.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto& settingsGetter = settings->GetGetter();
|
||||
|
||||
// Compute critical tiles once to avoid 8.5% runtime overhead in ShardokEngine construction
|
||||
const auto criticalTiles = GetCriticalTileLocations(state->hex_map());
|
||||
const auto guessedEngine = ShardokEngine(settings, state, criticalTiles);
|
||||
const auto deadline = startTime + budget.remainingBudget;
|
||||
|
||||
// Build MCTS tree
|
||||
auto rootNode = BuildMCTSTree(guessedEngine, settingsGetter, criticalTiles, deadline);
|
||||
|
||||
if (rootNode) {
|
||||
// Get best command from tree
|
||||
const MCTSNode* bestChild = rootNode->GetBestFinalChild();
|
||||
|
||||
if (bestChild) {
|
||||
result.searchCompleted = true;
|
||||
result.bestCommandIndex = bestChild->commandIndex;
|
||||
result.bestScore = bestChild->lookaheadScore;
|
||||
result.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
|
||||
// Calculate max depth reached in the tree
|
||||
std::function<int(const MCTSNode*)> getMaxDepth = [&](const MCTSNode* node) -> int {
|
||||
int maxChildDepth = node->depth;
|
||||
for (const auto& child : node->children) {
|
||||
maxChildDepth = std::max(maxChildDepth, getMaxDepth(child.get()));
|
||||
}
|
||||
return maxChildDepth;
|
||||
};
|
||||
result.depthAchieved = getMaxDepth(rootNode.get());
|
||||
|
||||
// Count total nodes visited
|
||||
std::function<size_t(const MCTSNode*)> countVisited =
|
||||
[&](const MCTSNode* node) -> size_t {
|
||||
size_t count = (node->visitCount > 0) ? 1 : 0;
|
||||
for (const auto& child : node->children) { count += countVisited(child.get()); }
|
||||
return count;
|
||||
};
|
||||
|
||||
result.commandCountEvaluated = countVisited(rootNode.get());
|
||||
result.availableCommandCount = commands.size();
|
||||
|
||||
// MCTS-specific logging
|
||||
printf("MCTS: Selected command %zu (visit:%d, reward:%.2f, lookahead:%.2f) from %zu "
|
||||
"options\n",
|
||||
bestChild->commandIndex,
|
||||
bestChild->visitCount,
|
||||
bestChild->averageReward,
|
||||
bestChild->lookaheadScore,
|
||||
rootNode->children.size());
|
||||
|
||||
// Log top 3 commands for debugging with their best sequences
|
||||
std::vector<MCTSNode*> sortedChildren;
|
||||
for (const auto& child : rootNode->children) { sortedChildren.push_back(child.get()); }
|
||||
std::ranges::sort(sortedChildren, [](const MCTSNode* a, const MCTSNode* b) {
|
||||
return a->visitCount > b->visitCount;
|
||||
});
|
||||
|
||||
printf("MCTS: Top commands by visits:\n");
|
||||
for (size_t i = 0; i < std::min(static_cast<size_t>(3), sortedChildren.size()); ++i) {
|
||||
auto* child = sortedChildren[i];
|
||||
printf(" [%zu] cmd:%zu visits:%d immediate:%.2f backprop:%.2f type:%s",
|
||||
i,
|
||||
child->commandIndex,
|
||||
child->visitCount,
|
||||
child->immediateScore,
|
||||
child->averageReward,
|
||||
CommandType_Name(child->commandType).c_str());
|
||||
|
||||
// Show unit and target info for commands that have them
|
||||
if (child->actorUnitId >= 0) { printf(" unit:%d", child->actorUnitId); }
|
||||
if (child->targetRow >= 0 && child->targetCol >= 0) {
|
||||
printf(" target:(%d,%d)", child->targetRow, child->targetCol);
|
||||
}
|
||||
|
||||
// Show sequence preview for this command
|
||||
if (!child->children.empty()) {
|
||||
// Find best child by visits
|
||||
MCTSNode* bestNext = nullptr;
|
||||
int maxVisits = 0;
|
||||
for (const auto& grandchild : child->children) {
|
||||
if (grandchild->visitCount > maxVisits) {
|
||||
maxVisits = grandchild->visitCount;
|
||||
bestNext = grandchild.get();
|
||||
}
|
||||
}
|
||||
if (bestNext) {
|
||||
printf(" -> %s", CommandType_Name(bestNext->commandType).c_str());
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
printf("MCTS: Tree stats - max depth:%zu, total nodes:%zu, root visits:%d\n",
|
||||
result.depthAchieved,
|
||||
result.commandCountEvaluated,
|
||||
rootNode->visitCount);
|
||||
|
||||
// Log the best command sequence from the chosen command
|
||||
struct SequenceNode {
|
||||
size_t commandIndex;
|
||||
std::string commandType;
|
||||
int actorUnitId;
|
||||
int targetRow;
|
||||
int targetCol;
|
||||
double immediateScore;
|
||||
double backpropScore;
|
||||
};
|
||||
std::vector<SequenceNode> bestSequence;
|
||||
bestSequence.reserve(5);
|
||||
auto current = const_cast<MCTSNode*>(bestChild);
|
||||
double sequenceScore = bestChild->averageReward;
|
||||
|
||||
// Trace the best path from chosen command (most visited child at each level)
|
||||
while (current) {
|
||||
bestSequence.push_back(
|
||||
{current->commandIndex,
|
||||
CommandType_Name(current->commandType),
|
||||
current->actorUnitId,
|
||||
current->targetRow,
|
||||
current->targetCol,
|
||||
current->immediateScore,
|
||||
current->averageReward});
|
||||
|
||||
// If no children, we've reached the end of the sequence
|
||||
if (current->children.empty()) { break; }
|
||||
|
||||
// Find most visited child
|
||||
MCTSNode* bestChildNode = nullptr;
|
||||
int maxVisits = 0;
|
||||
for (const auto& child : current->children) {
|
||||
if (child->visitCount > maxVisits) {
|
||||
maxVisits = child->visitCount;
|
||||
bestChildNode = child.get();
|
||||
}
|
||||
}
|
||||
current = bestChildNode;
|
||||
if (current) {
|
||||
sequenceScore = current->averageReward; // Update to final score
|
||||
}
|
||||
}
|
||||
|
||||
if (!bestSequence.empty()) {
|
||||
printf("MCTS: Best sequence from chosen command (final: %.2f):\n", sequenceScore);
|
||||
for (size_t i = 0; i < bestSequence.size(); ++i) {
|
||||
const auto& [commandIndex, commandType, actorUnitId, targetRow, targetCol, immediateScore, backpropScore] =
|
||||
bestSequence[i];
|
||||
printf(" %zu. cmd:%zu %s", i + 1, commandIndex, commandType.c_str());
|
||||
|
||||
// Add unit and target info if present
|
||||
if (actorUnitId >= 0) { printf(" unit:%d", actorUnitId); }
|
||||
if (targetRow >= 0 && targetCol >= 0) {
|
||||
printf(" target:(%d,%d)", targetRow, targetCol);
|
||||
}
|
||||
|
||||
printf(" (immediate:%.2f, backprop:%.2f)\n", immediateScore, backpropScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto endTime = std::chrono::steady_clock::now();
|
||||
result.timeUsed = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto MCTSAI::BuildMCTSTree(
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
const std::chrono::steady_clock::time_point deadline) const -> std::unique_ptr<MCTSNode> {
|
||||
// Clear transposition registry for this search
|
||||
if (config.enableTranspositionDetection) { stateRegistry.clear(); }
|
||||
|
||||
// Create root node
|
||||
auto root = std::make_unique<MCTSNode>(
|
||||
0,
|
||||
net::eagle0::shardok::common::END_TURN_COMMAND,
|
||||
playerId,
|
||||
0,
|
||||
isDefender);
|
||||
root->resultingGameState = engine.GetCurrentGameState();
|
||||
|
||||
// Register root node in transposition table if enabled
|
||||
if (config.enableTranspositionDetection) {
|
||||
root->stateHash = root->resultingGameState.ComputeFNV1aHash();
|
||||
stateRegistry[root->stateHash] = root.get();
|
||||
}
|
||||
|
||||
// Initialize root with available commands
|
||||
const CommandListSPtr rootCommands = engine.GetAvailableCommandsForAIPlayer(playerId);
|
||||
if (!rootCommands || rootCommands->empty()) { return root; }
|
||||
|
||||
// Filter commands for better performance
|
||||
const std::vector<size_t> filteredIndices = AICommandFilter::FilterCommands(
|
||||
rootCommands,
|
||||
playerId,
|
||||
isDefender,
|
||||
engine.GetCurrentGameState(),
|
||||
settingsGetter,
|
||||
apdCache);
|
||||
|
||||
root->untriedCommands = filteredIndices;
|
||||
root->fullyExpanded = filteredIndices.empty();
|
||||
|
||||
// Main MCTS loop
|
||||
int iterations = 0;
|
||||
|
||||
printf("MCTS: Starting search with %zu filtered commands (budget: %.0fms)\n",
|
||||
filteredIndices.size(),
|
||||
std::chrono::duration<double, std::milli>(deadline - std::chrono::steady_clock::now())
|
||||
.count());
|
||||
|
||||
if (config.useMultithreading) {
|
||||
// Parallel MCTS: Run iterations until time budget expires
|
||||
const int numThreads =
|
||||
std::min(config.numThreads, static_cast<int>(std::thread::hardware_concurrency()));
|
||||
std::vector<std::future<void>> futures;
|
||||
std::mutex treeMutex; // Protect tree updates
|
||||
std::atomic totalIterations{0}; // Track iterations across threads
|
||||
|
||||
for (int t = 0; t < numThreads; ++t) {
|
||||
futures.push_back(std::async(std::launch::async, [&, this] {
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
++totalIterations;
|
||||
|
||||
// Selection and expansion need locking
|
||||
MCTSNode* selected;
|
||||
{
|
||||
std::lock_guard lock(treeMutex);
|
||||
selected = MCTSSelection(root.get());
|
||||
if (selected && !selected->isTerminal && selected->CanExpand()) {
|
||||
selected = MCTSExpansion(
|
||||
selected,
|
||||
engine,
|
||||
settingsGetter,
|
||||
criticalTileCoords);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip simulation if selection failed (all children redundant)
|
||||
if (!selected) continue;
|
||||
|
||||
// Simulation can run in parallel from the selected node's state
|
||||
// Note: Creating engine from node state is correct for MCTS simulation
|
||||
|
||||
// Backpropagation needs locking
|
||||
{
|
||||
ShardokEngine nodeEngine(
|
||||
engine.GetGameSettings(),
|
||||
selected->resultingGameState,
|
||||
criticalTileCoords);
|
||||
const double reward =
|
||||
MCTSSimulation(nodeEngine, selected->playerId, settingsGetter);
|
||||
std::lock_guard lock(treeMutex);
|
||||
MCTSBackpropagation(selected, reward);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
for (auto& future : futures) { future.get(); }
|
||||
iterations = totalIterations.load(); // Get total from all threads
|
||||
} else {
|
||||
// Sequential MCTS - run until time budget expires
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
// Selection
|
||||
MCTSNode* selected = MCTSSelection(root.get());
|
||||
|
||||
// Skip if selection failed (all children redundant)
|
||||
if (!selected) continue;
|
||||
|
||||
// Expansion
|
||||
if (!selected->isTerminal && selected->CanExpand()) {
|
||||
selected = MCTSExpansion(selected, engine, settingsGetter, criticalTileCoords);
|
||||
}
|
||||
|
||||
// Simulation (from selected node's state)
|
||||
ShardokEngine nodeEngine(
|
||||
engine.GetGameSettings(),
|
||||
selected->resultingGameState,
|
||||
criticalTileCoords);
|
||||
double reward = MCTSSimulation(
|
||||
nodeEngine,
|
||||
selected->playerId,
|
||||
settingsGetter); // Use node's player, not original player
|
||||
|
||||
// Backpropagation
|
||||
MCTSBackpropagation(selected, reward);
|
||||
|
||||
iterations++;
|
||||
}
|
||||
}
|
||||
|
||||
printf("MCTS: Completed %d iterations, root has %zu children\n",
|
||||
iterations,
|
||||
root->children.size());
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
auto MCTSAI::MCTSSelection(MCTSNode* root) const -> MCTSNode* {
|
||||
MCTSNode* current = root;
|
||||
|
||||
while (!current->isTerminal && !current->isRedundant) {
|
||||
if (current->CanExpand()) {
|
||||
return current; // Node has untried commands
|
||||
} else if (!current->children.empty()) {
|
||||
current = current->GetBestChild(config.explorationConstant);
|
||||
if (!current) break;
|
||||
} else {
|
||||
break; // Leaf node
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
auto MCTSAI::MCTSExpansion(
|
||||
MCTSNode* node,
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const CoordsSet& criticalTileCoords) const -> MCTSNode* {
|
||||
static int expansionCallCount = 0;
|
||||
if (expansionCallCount < 3) {
|
||||
printf("MCTSExpansion called %d: node depth:%d untried:%zu\n",
|
||||
expansionCallCount++,
|
||||
node->depth,
|
||||
node->untriedCommands.size());
|
||||
}
|
||||
|
||||
if (node->untriedCommands.empty()) return node;
|
||||
|
||||
// Don't expand beyond maximum depth to prevent unbounded tree growth
|
||||
if (node->depth >= config.maxTreeDepth) {
|
||||
node->fullyExpanded = true;
|
||||
node->untriedCommands.clear();
|
||||
return node;
|
||||
}
|
||||
|
||||
// Pick a random untried command
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution dis(0, static_cast<int>(node->untriedCommands.size() - 1));
|
||||
const size_t randomIndex = dis(gen);
|
||||
|
||||
const auto commandIndex = node->untriedCommands[randomIndex];
|
||||
node->untriedCommands.erase(node->untriedCommands.begin() + randomIndex);
|
||||
|
||||
// Create engine from the node's current state (not root state!)
|
||||
const auto nodeEngine = std::make_shared<ShardokEngine>(
|
||||
engine.GetGameSettings(),
|
||||
node->resultingGameState,
|
||||
criticalTileCoords);
|
||||
|
||||
// Get command descriptor from the node's state
|
||||
const CommandListSPtr commands = nodeEngine->GetAvailableCommandsForAIPlayer(node->playerId);
|
||||
if (!commands || commandIndex >= commands->size()) return node;
|
||||
|
||||
const auto& command = commands->at(commandIndex);
|
||||
const auto commandType = command->GetCommandType();
|
||||
const auto descriptor = command->GetCommandProto();
|
||||
|
||||
// Create child node
|
||||
auto child = std::make_unique<MCTSNode>(
|
||||
commandIndex,
|
||||
commandType,
|
||||
node->playerId,
|
||||
node->depth + 1,
|
||||
node->isDefender);
|
||||
child->parent = node;
|
||||
|
||||
// Extract actor unit ID if present
|
||||
if (descriptor.has_actor()) { child->actorUnitId = descriptor.actor().value(); }
|
||||
|
||||
// Extract target coordinates if present
|
||||
// Note: In protobuf3, target is always present but may have default values
|
||||
// We'll always capture the coordinates - commands without targets will have (-1,-1) by default
|
||||
const auto& target = descriptor.target();
|
||||
child->targetRow = target.row();
|
||||
child->targetCol = target.column();
|
||||
|
||||
// Declare variables that will be used later
|
||||
|
||||
// Handle randomness appropriately based on command type
|
||||
if (command->HasOdds()) {
|
||||
// For commands with odds, use average roll for expansion
|
||||
// For expansion, use average roll regardless of success chance
|
||||
const auto generator = std::make_shared<SequenceRandomGenerator>(std::vector{0.5});
|
||||
nodeEngine->PostCommand(node->playerId, commandIndex, generator);
|
||||
} else {
|
||||
// Use average generator for deterministic evaluation
|
||||
nodeEngine->PostCommand(node->playerId, commandIndex, _averageGenerator);
|
||||
}
|
||||
|
||||
child->resultingGameState = nodeEngine->GetCurrentGameState();
|
||||
|
||||
// Check whose turn it is after executing the command
|
||||
PlayerId currentPlayer = nodeEngine->GetCurrentPlayerId();
|
||||
bool isOurTurn = (currentPlayer == playerId);
|
||||
|
||||
// Update child's player ID to reflect whose turn it actually is
|
||||
child->playerId = currentPlayer;
|
||||
|
||||
// Calculate immediate score (always from our perspective)
|
||||
child->immediateScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender, // Use our original role, not node's
|
||||
child->resultingGameState,
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
// Initially, lookahead score equals immediate score
|
||||
child->lookaheadScore = child->immediateScore;
|
||||
|
||||
// Debug: Log first few expansions to see what's happening
|
||||
static int expansionCount = 0;
|
||||
if (expansionCount < 5) {
|
||||
printf("MCTS Expansion %d: cmd:%zu type:%s immediate_score:%.2f\n",
|
||||
expansionCount++,
|
||||
commandIndex,
|
||||
CommandType_Name(commandType).c_str(),
|
||||
child->immediateScore);
|
||||
}
|
||||
|
||||
// Check if terminal
|
||||
child->isTerminal = IsTerminalForPlayer(child->resultingGameState, playerId, settingsGetter);
|
||||
|
||||
// Transposition detection
|
||||
if (config.enableTranspositionDetection) {
|
||||
child->stateHash = child->resultingGameState.ComputeFNV1aHash();
|
||||
|
||||
auto existingIt = stateRegistry.find(child->stateHash);
|
||||
if (existingIt != stateRegistry.end()) {
|
||||
MCTSNode* existingNode = existingIt->second;
|
||||
|
||||
// Apply tie-breaking rules to determine which node to keep
|
||||
bool shouldPruneChild = false;
|
||||
|
||||
if (child->depth > existingNode->depth) {
|
||||
// Rule 1: Prune deeper node (current child is deeper)
|
||||
shouldPruneChild = true;
|
||||
} else if (child->depth == existingNode->depth) {
|
||||
// Rule 2: At same depth, prune node with higher command index
|
||||
if (child->commandIndex > existingNode->commandIndex) {
|
||||
shouldPruneChild = true;
|
||||
} else {
|
||||
// Current child wins - mark existing node as redundant
|
||||
existingNode->isRedundant = true;
|
||||
stateRegistry[child->stateHash] = child.get(); // Update registry
|
||||
}
|
||||
} else {
|
||||
// Child is shallower - mark existing node as redundant
|
||||
existingNode->isRedundant = true;
|
||||
stateRegistry[child->stateHash] = child.get(); // Update registry
|
||||
}
|
||||
|
||||
if (shouldPruneChild) {
|
||||
child->isRedundant = true;
|
||||
// Don't expand redundant nodes
|
||||
}
|
||||
} else {
|
||||
// New state - register it
|
||||
stateRegistry[child->stateHash] = child.get();
|
||||
}
|
||||
}
|
||||
|
||||
// Get available commands for child - only if it's still our turn and not redundant
|
||||
if (!child->isTerminal && !child->isRedundant && isOurTurn) {
|
||||
const CommandListSPtr childCommands =
|
||||
nodeEngine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
if (childCommands) {
|
||||
const std::vector<size_t> childFiltered = AICommandFilter::FilterCommands(
|
||||
childCommands,
|
||||
currentPlayer,
|
||||
isDefender, // Use our original role
|
||||
child->resultingGameState,
|
||||
settingsGetter,
|
||||
apdCache);
|
||||
child->untriedCommands = childFiltered;
|
||||
child->fullyExpanded = childFiltered.empty();
|
||||
}
|
||||
} else if (!isOurTurn) {
|
||||
// Mark as terminal if it's not our turn - we can't expand opponent moves
|
||||
child->isTerminal = true;
|
||||
child->fullyExpanded = true;
|
||||
}
|
||||
|
||||
// Update parent's expansion status
|
||||
if (node->untriedCommands.empty()) { node->fullyExpanded = true; }
|
||||
|
||||
MCTSNode* childPtr = child.get();
|
||||
node->children.push_back(std::move(child));
|
||||
|
||||
return childPtr;
|
||||
}
|
||||
|
||||
auto MCTSAI::MCTSSimulation(
|
||||
const ShardokEngine& engineState,
|
||||
PlayerId startingPlayer,
|
||||
const SettingsGetter& settingsGetter) const -> double {
|
||||
// Create copy for simulation
|
||||
auto simEngine = std::make_shared<ShardokEngine>(engineState, false);
|
||||
|
||||
// Always evaluate from our AI's perspective (not the startingPlayer's perspective)
|
||||
const double initialScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
simEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
// Debug: Note that we're always scoring from our AI's perspective regardless of startingPlayer
|
||||
(void)startingPlayer; // Acknowledge parameter to avoid warning
|
||||
|
||||
// Fast rollout with random/heuristic moves until terminal
|
||||
int simulationSteps = 0;
|
||||
for (int step = 0; step < config.maxSimulationDepth; ++step) {
|
||||
const GameStateW& currentState = simEngine->GetCurrentGameState();
|
||||
|
||||
// Get whose turn it is
|
||||
const PlayerId currentPlayer = simEngine->GetCurrentPlayerId();
|
||||
|
||||
// Check if terminal
|
||||
if (IsTerminalForPlayer(currentState, playerId, settingsGetter)) { break; }
|
||||
|
||||
// Continue as long as it's still our turn (don't stop after each individual command)
|
||||
// In this game, a player can move multiple units before turn switches
|
||||
if (currentPlayer != playerId) {
|
||||
// Turn switched to opponent - stop simulation immediately
|
||||
break;
|
||||
}
|
||||
|
||||
simulationSteps++;
|
||||
|
||||
// Get available commands for current player
|
||||
const CommandListSPtr commands = simEngine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
if (!commands || commands->empty()) break;
|
||||
|
||||
// Select command based on simulation policy
|
||||
const auto commandIndex = static_cast<int>(
|
||||
SelectSimulationCommand(commands, currentPlayer, simEngine, settingsGetter));
|
||||
|
||||
// Execute command
|
||||
simEngine->PostCommand(currentPlayer, commandIndex, _averageGenerator);
|
||||
|
||||
// Debug: Only log if turn changed unexpectedly
|
||||
const PlayerId newPlayer = simEngine->GetCurrentPlayerId();
|
||||
static int debugCount = 0;
|
||||
if (newPlayer != currentPlayer && debugCount < 5) {
|
||||
const auto commandType = commands->at(commandIndex)->GetCommandType();
|
||||
printf("MCTS Sim step %d: cmd_type:%s player_before:%d player_after:%d\n",
|
||||
simulationSteps,
|
||||
CommandType_Name(commandType).c_str(),
|
||||
currentPlayer,
|
||||
newPlayer);
|
||||
printf(" WARNING: Turn changed after command!\n");
|
||||
debugCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate final position
|
||||
const double finalScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
simEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
// Debug: Log first few simulations to see depth and score change
|
||||
static int simCount = 0;
|
||||
if (simCount < 3) {
|
||||
printf("MCTS Simulation %d: steps:%d initial:%.2f final:%.2f delta:%.2f\n",
|
||||
simCount++,
|
||||
simulationSteps,
|
||||
initialScore,
|
||||
finalScore,
|
||||
finalScore - initialScore);
|
||||
}
|
||||
|
||||
return finalScore;
|
||||
}
|
||||
|
||||
auto MCTSAI::MCTSBackpropagation(MCTSNode* node, double reward) -> void {
|
||||
while (node) {
|
||||
node->visitCount++;
|
||||
node->totalReward += reward;
|
||||
node->averageReward = node->totalReward / node->visitCount;
|
||||
|
||||
// Update lookahead score as weighted average
|
||||
if (node->visitCount == 1) {
|
||||
node->lookaheadScore = reward;
|
||||
} else {
|
||||
node->lookaheadScore =
|
||||
(node->lookaheadScore * (node->visitCount - 1) + reward) / node->visitCount;
|
||||
}
|
||||
|
||||
node = node->parent;
|
||||
}
|
||||
}
|
||||
|
||||
auto MCTSAI::IsTerminalForPlayer(
|
||||
const GameStateW& gameState,
|
||||
PlayerId /*currentPlayer*/,
|
||||
const SettingsGetter& settingsGetter) -> bool {
|
||||
// Check if game is over
|
||||
if (gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY ||
|
||||
gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check max rounds
|
||||
if (gameState->current_round() >= settingsGetter.Backing().max_rounds()) { return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto MCTSAI::SelectSimulationCommand(
|
||||
const CommandListSPtr& commands,
|
||||
PlayerId currentPlayer,
|
||||
const std::shared_ptr<ShardokEngine>& simEngine,
|
||||
const SettingsGetter& settingsGetter) const -> size_t {
|
||||
if (commands->size() == 1) {
|
||||
return 0; // Only one choice
|
||||
}
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
|
||||
switch (config.simulationPolicy) {
|
||||
case MCTSSimulationPolicy::RANDOM: {
|
||||
// Pure random selection
|
||||
std::uniform_int_distribution<> dis(0, commands->size() - 1);
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::FILTERED_RANDOM: {
|
||||
// Filter commands first, then random selection
|
||||
const auto filteredIndices = AICommandFilter::FilterCommands(
|
||||
commands,
|
||||
currentPlayer,
|
||||
isDefender,
|
||||
simEngine->GetCurrentGameState(),
|
||||
settingsGetter,
|
||||
apdCache);
|
||||
|
||||
if (filteredIndices.empty()) {
|
||||
// Fallback to random if no commands pass filter
|
||||
std::uniform_int_distribution<> dis(0, commands->size() - 1);
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
std::uniform_int_distribution<> dis(0, filteredIndices.size() - 1);
|
||||
return filteredIndices[dis(gen)];
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::BEST_IMMEDIATE: {
|
||||
// Evaluate all commands and pick the best
|
||||
double bestScore = -std::numeric_limits<double>::max();
|
||||
size_t bestIndex = 0;
|
||||
|
||||
for (size_t i = 0; i < commands->size(); ++i) {
|
||||
// Create a temporary engine to evaluate this command
|
||||
const auto testEngine = std::make_shared<ShardokEngine>(*simEngine, false);
|
||||
|
||||
// Get score BEFORE executing command (for potential player flip comparison)
|
||||
const double preScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
testEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
const PlayerId playerBefore = testEngine->GetCurrentPlayerId();
|
||||
testEngine->PostCommand(currentPlayer, i, _averageGenerator);
|
||||
const PlayerId playerAfter = testEngine->GetCurrentPlayerId();
|
||||
|
||||
double score;
|
||||
if (playerAfter != playerBefore) {
|
||||
// Player flipped - use pre-execution score to avoid opponent turn effects
|
||||
score = preScore;
|
||||
} else {
|
||||
// Normal command - use post-execution score
|
||||
score = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
testEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
}
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::WEIGHTED_BEST_IMMEDIATE: {
|
||||
// Evaluate all commands and weight by ranking
|
||||
struct CommandScore {
|
||||
size_t index;
|
||||
double score;
|
||||
};
|
||||
|
||||
std::vector<CommandScore> commandScores;
|
||||
commandScores.reserve(commands->size());
|
||||
|
||||
for (size_t i = 0; i < commands->size(); ++i) {
|
||||
// Create a temporary engine to evaluate this command
|
||||
const auto testEngine = std::make_shared<ShardokEngine>(*simEngine, false);
|
||||
|
||||
// Get score BEFORE executing command (for potential player flip comparison)
|
||||
const double preScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
testEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
const PlayerId playerBefore = testEngine->GetCurrentPlayerId();
|
||||
testEngine->PostCommand(currentPlayer, i, _averageGenerator);
|
||||
const PlayerId playerAfter = testEngine->GetCurrentPlayerId();
|
||||
|
||||
double score;
|
||||
if (playerAfter != playerBefore) {
|
||||
// Player flipped - use pre-execution score to avoid opponent turn effects
|
||||
score = preScore;
|
||||
} else {
|
||||
// Normal command - use post-execution score
|
||||
score = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
testEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
}
|
||||
|
||||
commandScores.push_back({i, score});
|
||||
}
|
||||
|
||||
// Sort by score (best first)
|
||||
std::sort(
|
||||
commandScores.begin(),
|
||||
commandScores.end(),
|
||||
[](const CommandScore& a, const CommandScore& b) { return a.score > b.score; });
|
||||
|
||||
// Assign weights: 1.0 for best, 0.5 for second, 0.33 for third, etc.
|
||||
std::vector<double> weights;
|
||||
weights.reserve(commandScores.size());
|
||||
double totalWeight = 0.0;
|
||||
|
||||
for (size_t i = 0; i < commandScores.size(); ++i) {
|
||||
double weight = 1.0 / (i + 1); // 1/1, 1/2, 1/3, ...
|
||||
weights.push_back(weight);
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
// Random selection based on weights
|
||||
std::uniform_real_distribution dis(0.0, totalWeight);
|
||||
const double target = dis(gen);
|
||||
double cumulative = 0.0;
|
||||
|
||||
for (size_t i = 0; i < weights.size(); ++i) {
|
||||
cumulative += weights[i];
|
||||
if (cumulative >= target) { return commandScores[i].index; }
|
||||
}
|
||||
|
||||
// Fallback (shouldn't happen)
|
||||
return commandScores[0].index;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to random (shouldn't reach here)
|
||||
std::uniform_int_distribution dis(0, static_cast<int>(commands->size() - 1));
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,128 +0,0 @@
|
||||
//
|
||||
// MCTS-based AI system for Shardok
|
||||
// Alternative to IterativeDeepeningAI using Monte Carlo Tree Search
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_MCTSAI_HPP
|
||||
#define EAGLE0_MCTSAI_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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/ai/IterativeDeepeningAI.hpp" // For SearchResult compatibility
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.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/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class ShardokEngine;
|
||||
|
||||
// MCTSNode is defined in internal/MCTSNode.hpp
|
||||
namespace internal {
|
||||
struct MCTSNode;
|
||||
}
|
||||
|
||||
// Simulation policy for MCTS rollouts
|
||||
enum class MCTSSimulationPolicy {
|
||||
RANDOM, // Pure random selection
|
||||
FILTERED_RANDOM, // Random from filtered commands
|
||||
BEST_IMMEDIATE, // Choose best immediate score
|
||||
WEIGHTED_BEST_IMMEDIATE // Random weighted by score ranking
|
||||
};
|
||||
|
||||
// Configuration for MCTS algorithm
|
||||
struct MCTSConfig {
|
||||
double explorationConstant = 1.414; // UCB1 constant (sqrt(2) by default)
|
||||
int maxSimulationDepth = 1000; // Maximum depth for rollout
|
||||
int maxTreeDepth = 2000; // Maximum tree depth to prevent stack overflow
|
||||
bool useMultithreading = true; // Enable parallel MCTS
|
||||
int numThreads = 16; // Number of threads for parallel MCTS (when enabled)
|
||||
MCTSSimulationPolicy simulationPolicy = MCTSSimulationPolicy::BEST_IMMEDIATE;
|
||||
bool enableTranspositionDetection = true; // Enable pruning of duplicate states
|
||||
};
|
||||
|
||||
class MCTSAI {
|
||||
public:
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
using SearchResult = IterativeDeepeningAI::SearchResult;
|
||||
|
||||
MCTSAI(PlayerId playerId,
|
||||
bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
MCTSConfig config = MCTSConfig{});
|
||||
|
||||
// Main search interface - compatible with IterativeDeepeningAI
|
||||
[[nodiscard]] auto Search(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
const std::vector<CommandProto>& commands,
|
||||
const AITimeBudget& budget) const -> SearchResult;
|
||||
|
||||
// Get/set configuration
|
||||
[[nodiscard]] auto GetConfig() const -> const MCTSConfig& { return config; }
|
||||
void SetConfig(const MCTSConfig& newConfig) { config = newConfig; }
|
||||
|
||||
private:
|
||||
PlayerId playerId;
|
||||
bool isDefender;
|
||||
AIStrategy strategy;
|
||||
const CoordsSet& castleCoords;
|
||||
const APDCache& apdCache;
|
||||
const ALCache& alCache;
|
||||
MCTSConfig config;
|
||||
|
||||
// Transposition detection infrastructure
|
||||
mutable std::unordered_map<uint64_t, internal::MCTSNode*>
|
||||
stateRegistry; // Hash -> first node mapping
|
||||
|
||||
// Internal MCTS tree building
|
||||
[[nodiscard]] auto BuildMCTSTree(
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const
|
||||
-> std::unique_ptr<internal::MCTSNode>;
|
||||
|
||||
// MCTS algorithm phases
|
||||
auto MCTSSelection(internal::MCTSNode* root) const -> internal::MCTSNode*;
|
||||
auto MCTSExpansion(
|
||||
internal::MCTSNode* node,
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const CoordsSet& criticalTileCoords) const -> internal::MCTSNode*;
|
||||
auto MCTSSimulation(
|
||||
const ShardokEngine& engineState,
|
||||
PlayerId currentPlayer,
|
||||
const SettingsGetter& settingsGetter) const -> double;
|
||||
static auto MCTSBackpropagation(internal::MCTSNode* node, double reward) -> void;
|
||||
|
||||
// Helper functions
|
||||
[[nodiscard]] static auto IsTerminalForPlayer(
|
||||
const GameStateW& gameState,
|
||||
PlayerId currentPlayer,
|
||||
const SettingsGetter& settingsGetter) -> bool;
|
||||
|
||||
// Simulation command selection based on policy
|
||||
[[nodiscard]] auto SelectSimulationCommand(
|
||||
const CommandListSPtr& commands,
|
||||
PlayerId currentPlayer,
|
||||
const std::shared_ptr<ShardokEngine>& simEngine,
|
||||
const SettingsGetter& settingsGetter) const -> size_t;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_MCTSAI_HPP
|
||||
@@ -1,16 +0,0 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "mcts_node",
|
||||
hdrs = ["MCTSNode.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -1,210 +0,0 @@
|
||||
//
|
||||
// Internal MCTS Node structure for Shardok AI
|
||||
// This is an implementation detail and should not be used by external code
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_INTERNAL_MCTSNODE_HPP
|
||||
#define EAGLE0_INTERNAL_MCTSNODE_HPP
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
|
||||
// Suppress the protobuf deprecation warning temporarily
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace internal {
|
||||
|
||||
// Import CommandType for use within the internal namespace
|
||||
using CommandType = net::eagle0::shardok::common::CommandType;
|
||||
|
||||
// MCTS Node structure
|
||||
struct MCTSNode {
|
||||
// Command information
|
||||
size_t commandIndex;
|
||||
CommandType commandType;
|
||||
int actorUnitId = -1; // Unit performing the command (-1 if not applicable)
|
||||
int targetRow = -1; // Target coordinate row (-1 if not applicable)
|
||||
int targetCol = -1; // Target coordinate column (-1 if not applicable)
|
||||
|
||||
// Score information
|
||||
double immediateScore;
|
||||
double lookaheadScore;
|
||||
|
||||
// Game state after this command
|
||||
GameStateW resultingGameState;
|
||||
|
||||
// MCTS statistics
|
||||
int visitCount = 0;
|
||||
double totalReward = 0.0;
|
||||
double averageReward = 0.0;
|
||||
double ucb1Value = 0.0;
|
||||
|
||||
// Tree structure
|
||||
std::vector<std::unique_ptr<MCTSNode>> children;
|
||||
std::vector<size_t> untriedCommands;
|
||||
bool fullyExpanded = false;
|
||||
MCTSNode* parent = nullptr;
|
||||
|
||||
// Game context
|
||||
PlayerId playerId;
|
||||
int depth = 0;
|
||||
bool isDefender = false;
|
||||
bool isTerminal = false;
|
||||
|
||||
// Transposition detection
|
||||
uint64_t stateHash = 0;
|
||||
bool isRedundant = false; // True if this node represents a duplicate state
|
||||
|
||||
MCTSNode(
|
||||
const size_t cmdIndex,
|
||||
const CommandType cmdType,
|
||||
const PlayerId pid,
|
||||
const int d,
|
||||
const bool defender)
|
||||
: commandIndex(cmdIndex),
|
||||
commandType(cmdType),
|
||||
immediateScore(0.0),
|
||||
lookaheadScore(0.0),
|
||||
playerId(pid),
|
||||
depth(d),
|
||||
isDefender(defender) {}
|
||||
|
||||
// Iterative destructor to avoid stack overflow with deep trees
|
||||
~MCTSNode() {
|
||||
// Use iterative approach to destroy children
|
||||
std::vector<std::unique_ptr<MCTSNode>> nodesToDestroy;
|
||||
nodesToDestroy.swap(children);
|
||||
|
||||
while (!nodesToDestroy.empty()) {
|
||||
// Take ownership of all children from the current batch
|
||||
std::vector<std::unique_ptr<MCTSNode>> currentBatch;
|
||||
currentBatch.swap(nodesToDestroy);
|
||||
|
||||
// Collect grandchildren for next iteration
|
||||
for (const auto& node : currentBatch) {
|
||||
if (node && !node->children.empty()) {
|
||||
for (auto& child : node->children) {
|
||||
nodesToDestroy.push_back(std::move(child));
|
||||
}
|
||||
node->children.clear();
|
||||
}
|
||||
}
|
||||
// currentBatch goes out of scope here, destroying nodes with no children
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate UCB1 value for this node
|
||||
void CalculateUCB1(const double explorationConstant) {
|
||||
if (visitCount == 0) {
|
||||
ucb1Value = std::numeric_limits<double>::max();
|
||||
} else if (parent && parent->visitCount > 0) {
|
||||
ucb1Value = averageReward +
|
||||
explorationConstant * std::sqrt(std::log(parent->visitCount) / visitCount);
|
||||
} else {
|
||||
ucb1Value = averageReward;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this node can be expanded
|
||||
[[nodiscard]] bool CanExpand() const { return !fullyExpanded && !untriedCommands.empty(); }
|
||||
|
||||
// Get best child based on UCB1
|
||||
[[nodiscard]] MCTSNode* GetBestChild(const double explorationConstant) const {
|
||||
if (children.empty()) return nullptr;
|
||||
|
||||
MCTSNode* bestChild = nullptr;
|
||||
double bestValue = -std::numeric_limits<double>::max();
|
||||
|
||||
static int selectionCallCount = 0;
|
||||
const bool shouldDebug = selectionCallCount < 5;
|
||||
|
||||
for (auto& child : children) {
|
||||
// Skip redundant nodes
|
||||
if (child->isRedundant) continue;
|
||||
|
||||
child->CalculateUCB1(explorationConstant);
|
||||
|
||||
if (child->ucb1Value > bestValue) {
|
||||
bestValue = child->ucb1Value;
|
||||
bestChild = child.get();
|
||||
}
|
||||
|
||||
if (shouldDebug && child->visitCount > 0) {
|
||||
printf("UCB1 Debug: cmd:%zu visits:%d reward:%.2f ucb1:%.2f%s\n",
|
||||
child->commandIndex,
|
||||
child->visitCount,
|
||||
child->averageReward,
|
||||
child->ucb1Value,
|
||||
child->isRedundant ? " [REDUNDANT]" : "");
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDebug) {
|
||||
if (bestChild) {
|
||||
printf("UCB1 Selected: cmd:%zu ucb1:%.2f\n",
|
||||
bestChild->commandIndex,
|
||||
bestChild->ucb1Value);
|
||||
} else {
|
||||
printf("UCB1 Selected: nullptr (all children redundant)\n");
|
||||
}
|
||||
selectionCallCount++;
|
||||
}
|
||||
return bestChild;
|
||||
}
|
||||
|
||||
// Get best child based on average reward (for final selection)
|
||||
[[nodiscard]] MCTSNode* GetBestFinalChild() const {
|
||||
if (children.empty()) return nullptr;
|
||||
|
||||
MCTSNode* bestChild = nullptr;
|
||||
double bestScore = -std::numeric_limits<double>::max();
|
||||
int bestVisits = 0;
|
||||
|
||||
for (const auto& child : children) {
|
||||
// Skip redundant nodes
|
||||
if (child->isRedundant) continue;
|
||||
|
||||
// For final selection, prefer most-visited node (robust child selection)
|
||||
// Only consider nodes that have been visited
|
||||
if (child->visitCount > bestVisits) {
|
||||
bestVisits = child->visitCount;
|
||||
bestScore = child->averageReward;
|
||||
bestChild = child.get();
|
||||
} else if (child->visitCount == bestVisits && child->averageReward > bestScore) {
|
||||
// Tie-break on average reward
|
||||
bestScore = child->averageReward;
|
||||
bestChild = child.get();
|
||||
}
|
||||
}
|
||||
|
||||
// If no child was visited (shouldn't happen), fall back to lookahead score
|
||||
if (!bestChild && !children.empty()) {
|
||||
for (const auto& child : children) {
|
||||
// Skip redundant nodes
|
||||
if (child->isRedundant) continue;
|
||||
|
||||
if (child->lookaheadScore > bestScore) {
|
||||
bestScore = child->lookaheadScore;
|
||||
bestChild = child.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestChild;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_INTERNAL_MCTSNODE_HPP
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
#include "PerformanceTestGameStateBuilder.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIConfig.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"
|
||||
@@ -120,37 +119,21 @@ int main(int argc, char* argv[]) {
|
||||
const auto* hexMap = currentState->hex_map();
|
||||
const auto settingsGetter = settings->GetGetter();
|
||||
|
||||
ShardokAIClient aiClient(
|
||||
aiPlayerId,
|
||||
isDefender,
|
||||
hexMap,
|
||||
settingsGetter,
|
||||
AIAlgorithmType::MCTS_CLEAN);
|
||||
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,
|
||||
AIAlgorithmType::MCTS_CLEAN);
|
||||
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) {
|
||||
std::cout << "Setup phase detected. Completing setup...\n";
|
||||
int setupMoves = 0;
|
||||
while (currentState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
|
||||
PlayerId currentPlayer = currentState->current_player();
|
||||
auto availableCommands = engine.GetAvailableCommandProtos(currentPlayer, false);
|
||||
|
||||
std::cout << "Setup move " << setupMoves++ << ": player "
|
||||
<< static_cast<int>(currentPlayer) << " has " << availableCommands.size()
|
||||
<< " commands\n";
|
||||
|
||||
if (availableCommands.empty()) {
|
||||
std::cout << "No commands available for player "
|
||||
<< static_cast<int>(currentPlayer) << "\n";
|
||||
@@ -160,19 +143,15 @@ int main(int argc, char* argv[]) {
|
||||
if (currentPlayer == aiPlayerId) {
|
||||
// Let AI make intelligent placement decisions
|
||||
auto choiceResults = aiClient.ChooseCommandIndex(engine);
|
||||
std::cout << "AI player chose command " << choiceResults.chosenIndex << "\n";
|
||||
engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
|
||||
} else {
|
||||
// Human player: use AI for setup to ensure consistent state handling
|
||||
auto choiceResults = humanSetupAI.ChooseCommandIndex(engine);
|
||||
std::cout << "Human player (AI) chose command " << choiceResults.chosenIndex
|
||||
<< "\n";
|
||||
engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
|
||||
}
|
||||
|
||||
currentState = engine.GetCurrentGameState();
|
||||
}
|
||||
std::cout << "Setup complete! Game state: " << currentState->status()->state() << "\n";
|
||||
}
|
||||
|
||||
// Test AI performance for configured number of turns
|
||||
@@ -181,25 +160,6 @@ int main(int argc, char* argv[]) {
|
||||
std::vector<AIPerformanceMetrics> metrics;
|
||||
|
||||
for (int turn = 0; turn < config.numTurns; ++turn) {
|
||||
// Update current state and check whose turn it is
|
||||
currentState = engine.GetCurrentGameState();
|
||||
PlayerId currentPlayer = currentState->current_player();
|
||||
|
||||
// If it's not the AI's turn, skip to next iteration
|
||||
if (currentPlayer != aiPlayerId) {
|
||||
std::cout << " Turn " << turn << ": It's player "
|
||||
<< static_cast<int>(currentPlayer) << "'s turn (not AI). Skipping...\n";
|
||||
|
||||
// Have the opponent make a simple move to advance the game
|
||||
const auto opponentCommands =
|
||||
engine.GetAvailableCommandProtos(currentPlayer, false);
|
||||
if (!opponentCommands.empty()) {
|
||||
// For now, opponent chooses first available command
|
||||
engine.PostCommand(currentPlayer, 0);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if AI can make a move
|
||||
const auto availableCommands = engine.GetAvailableCommandProtos(aiPlayerId, false);
|
||||
if (availableCommands.empty()) {
|
||||
|
||||
@@ -74,8 +74,7 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
|
||||
pi.player_id(),
|
||||
pi.is_defender(),
|
||||
e->GetCurrentGameState()->hex_map(),
|
||||
e->GetGameSettings()->GetGetter(),
|
||||
AIAlgorithmType::MCTS_CLEAN);
|
||||
e->GetGameSettings()->GetGetter());
|
||||
|
||||
aic.push_back(newClient);
|
||||
}
|
||||
|
||||
@@ -58,22 +58,6 @@ ShardokEngine::ShardokEngine(
|
||||
startingHistoryState(gsBuffer),
|
||||
criticalTileCoords(GetCriticalTileLocations(GetCurrentGameState()->hex_map())) {}
|
||||
|
||||
ShardokEngine::ShardokEngine(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &gsBuffer,
|
||||
const CoordsSet &criticalTileCoords,
|
||||
const int32_t startingHistoryCount,
|
||||
const bool trackHistory)
|
||||
: gameSettings(settings),
|
||||
settingsGetter(settings->GetGetter()),
|
||||
availableCommandsFactory(
|
||||
AvailableCommandsFactory::MakeAvailableCommandsFactory(settingsGetter)),
|
||||
gameState(gsBuffer),
|
||||
trackHistory(trackHistory),
|
||||
startingHistoryCount(startingHistoryCount),
|
||||
startingHistoryState(gsBuffer),
|
||||
criticalTileCoords(criticalTileCoords) {}
|
||||
|
||||
ShardokEngine::ShardokEngine(
|
||||
const GameSettingsSPtr &settings,
|
||||
vector<Unit> &&unplacedUnorderedUnits,
|
||||
|
||||
@@ -88,14 +88,6 @@ public:
|
||||
int32_t startingHistoryCount = 0,
|
||||
bool trackHistory = true);
|
||||
|
||||
// Constructor using existing game state with pre-computed critical tile coords
|
||||
ShardokEngine(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &gsBuffer,
|
||||
const CoordsSet &criticalTileCoords,
|
||||
int32_t startingHistoryCount = 0,
|
||||
bool trackHistory = true);
|
||||
|
||||
// Constructor for a brand new game
|
||||
ShardokEngine(
|
||||
const GameSettingsSPtr &settings,
|
||||
|
||||
+16
-9
@@ -135,27 +135,27 @@ auto ActionPointDistancesCache::GetRaw(
|
||||
MakeCacheKey(mapId, battalionType, includeBravingWater, braveWaterActionPointCost);
|
||||
|
||||
// Check the persistent map first
|
||||
if (auto it = persistentCache.find(cacheKey); it != persistentCache.end()) {
|
||||
if (persistentCache.contains(cacheKey)) {
|
||||
#if CACHE_STATS_LOGGING_
|
||||
cacheStats.persistentHits++;
|
||||
MaybePrintCacheStats();
|
||||
#endif
|
||||
// Return directly from persistent cache without TLS insertion
|
||||
// This avoids the overhead of thread-local storage operations on hot path
|
||||
return it->second.sharedPtr.get();
|
||||
return persistentCache.at(cacheKey).rawPtr;
|
||||
}
|
||||
|
||||
#if CACHE_STATS_LOGGING_
|
||||
cacheStats.persistentMisses++;
|
||||
#endif
|
||||
|
||||
// Check thread-local cache (no locks needed!)
|
||||
if (auto it = tlsCache.find(cacheKey); it != tlsCache.end()) {
|
||||
// Check thread-local cache first (no locks needed!)
|
||||
if (tlsCache.contains(cacheKey)) {
|
||||
#if CACHE_STATS_LOGGING_
|
||||
cacheStats.localHits++;
|
||||
MaybePrintCacheStats();
|
||||
#endif
|
||||
return it->second.sharedPtr.get(); // Compute raw pointer on demand
|
||||
return tlsCache.at(cacheKey).rawPtr; // Raw pointer - zero overhead access!
|
||||
}
|
||||
|
||||
#if CACHE_STATS_LOGGING_
|
||||
@@ -224,10 +224,17 @@ auto ActionPointDistancesCache::GetRaw(
|
||||
// Store both shared_ptr and raw pointer for hybrid access
|
||||
tlsCache.emplace(cacheKey, CacheEntry(result));
|
||||
|
||||
// Note: No eviction logic needed here - ConsolidateThreadLocalCache_Racy()
|
||||
// is called after each AI decision to clear the cache and prevent unbounded growth.
|
||||
// Previous eviction logic was unsafe as it could free cache entries while raw
|
||||
// pointers to those entries were still in use, causing use-after-free crashes.
|
||||
// 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();
|
||||
}
|
||||
|
||||
+4
-1
@@ -59,8 +59,11 @@ class ActionPointDistancesCache {
|
||||
private:
|
||||
struct CacheEntry {
|
||||
shared_ptr<ActionPointDistances> sharedPtr;
|
||||
const ActionPointDistances* rawPtr;
|
||||
|
||||
explicit CacheEntry(shared_ptr<ActionPointDistances> ptr) : sharedPtr(std::move(ptr)) {}
|
||||
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.
|
||||
|
||||
@@ -24,7 +24,6 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/library/action_point_distances:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
|
||||
+6
-17
@@ -48,7 +48,6 @@ namespace eagle {
|
||||
private PlayerId _playerId;
|
||||
private int _recommendedFood;
|
||||
private int _recommendedGold;
|
||||
private List<AvailableDestinationProvince> _sortedAvailableDestinations;
|
||||
|
||||
public override AvailableCommand.SealedValueOneofCase CommandType =>
|
||||
AvailableCommand.SealedValueOneofCase.MarchCommand;
|
||||
@@ -121,13 +120,9 @@ namespace eagle {
|
||||
DestinationProvince is { RulingFactionId : not null } &&
|
||||
DestinationProvince.RulingFactionId.Value != _playerId;
|
||||
|
||||
private bool RequiresRiverCrossing => SelectedDestinationProvinceIndex is {}
|
||||
idx && _sortedAvailableDestinations != null && idx < _sortedAvailableDestinations.Count &&
|
||||
_sortedAvailableDestinations[idx].RequiresRiverCrossing;
|
||||
|
||||
public override bool WarnOnCommitButton =>
|
||||
WouldAbandonProvince || NotEnoughFood || MarchingOnTrucePartner ||
|
||||
BadlyConstructedUnits || NoBattalionsIntoEnemy || RequiresRiverCrossing;
|
||||
public override bool WarnOnCommitButton => WouldAbandonProvince || NotEnoughFood ||
|
||||
MarchingOnTrucePartner ||
|
||||
BadlyConstructedUnits || NoBattalionsIntoEnemy;
|
||||
public override string CommitWarningText {
|
||||
get {
|
||||
if (!(WarnOnCommitButton)) return null;
|
||||
@@ -152,9 +147,6 @@ namespace eagle {
|
||||
txt += "Some of your heroes are leading battalion types that would restrict their abilities\n\n";
|
||||
}
|
||||
}
|
||||
if (RequiresRiverCrossing) {
|
||||
txt += $"Assaulting {DestinationProvince.Name} may require crossing a river\n\n";
|
||||
}
|
||||
|
||||
return txt;
|
||||
}
|
||||
@@ -218,12 +210,9 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void SetUpDestinationProvinceDropdown() {
|
||||
_sortedAvailableDestinations =
|
||||
SelectedOneProvinceCommand.AvailableDestinationProvinces
|
||||
.OrderBy(adp => _model.Provinces[adp.ProvinceId].Name)
|
||||
.ToList();
|
||||
_sortedDestinationProvinceIds =
|
||||
_sortedAvailableDestinations.Select(adp => adp.ProvinceId).ToList();
|
||||
_sortedDestinationProvinceIds = SelectedOneProvinceCommand.AvailableDestinationProvinces
|
||||
.OrderBy(pid => _model.Provinces[pid].Name)
|
||||
.ToList();
|
||||
var destinationProvinceNames = _sortedDestinationProvinceIds
|
||||
.Select(id => GUIUtils.DarkColoredProvinceName(
|
||||
_model.Provinces[id],
|
||||
|
||||
+1
-1
@@ -862,7 +862,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private int? TroopCount() {
|
||||
return existingBattalions.Where(x => !x.dismissed).Select(eb => eb.Count).Sum() +
|
||||
return existingBattalions.Select(eb => eb.Count).Sum() +
|
||||
newBattalions.Select(nb => nb.Count).Sum();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-10
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
@@ -23,9 +21,6 @@ namespace eagle {
|
||||
|
||||
private TradeAvailableCommand TradeCommand => _availableCommand.TradeCommand;
|
||||
private ProvinceId ActingProvinceId => TradeCommand.ActingProvinceId;
|
||||
private ProvinceView ActingProvince => _model.Provinces[ActingProvinceId];
|
||||
private int ProvinceGoldCap => ActingProvince.FullInfo.GoldCap;
|
||||
private int ProvinceFoodCap => ActingProvince.FullInfo.FoodCap;
|
||||
|
||||
private int MaxFood =>
|
||||
TradeCommand.FoodAvailable +
|
||||
@@ -53,8 +48,7 @@ namespace eagle {
|
||||
SetTextFields();
|
||||
}
|
||||
|
||||
public override bool EnableCommitButton =>
|
||||
!Mathf.Approximately(slider.value, TradeCommand.FoodAvailable);
|
||||
public override bool EnableCommitButton => slider.value != TradeCommand.FoodAvailable;
|
||||
public override string DisabledCommitButtonReason => "No change";
|
||||
|
||||
private bool IsBuy => FoodAmount > TradeCommand.FoodAvailable;
|
||||
@@ -74,10 +68,7 @@ namespace eagle {
|
||||
|
||||
public void SetTextFields() {
|
||||
goldText.text = GoldAmount.ToString();
|
||||
goldText.color = GoldAmount > ProvinceGoldCap ? Color.red : Color.black;
|
||||
|
||||
foodText.text = FoodAmount.ToString();
|
||||
foodText.color = FoodAmount > ProvinceFoodCap ? Color.red : Color.black;
|
||||
|
||||
description.text = IsBuy ? $"Buying {NetFood} food for {-NetGold} gold"
|
||||
: $"Selling {-NetFood} food for {NetGold} gold";
|
||||
|
||||
@@ -244,11 +244,6 @@ message ManagePrisonersAvailableCommand {
|
||||
repeated PrisonerToManage prisoners = 2;
|
||||
}
|
||||
|
||||
message AvailableDestinationProvince {
|
||||
int32 province_id = 1;
|
||||
bool requires_river_crossing = 2;
|
||||
}
|
||||
|
||||
message MarchCommandFromOneProvince {
|
||||
map<int32, .net.eagle0.eagle.api.command.util.SuitableBattalions> suitable_battalions_for_heroes = 1;
|
||||
|
||||
@@ -259,7 +254,7 @@ message MarchCommandFromOneProvince {
|
||||
|
||||
int32 food_available = 6;
|
||||
|
||||
repeated AvailableDestinationProvince available_destination_provinces = 7;
|
||||
repeated int32 available_destination_provinces = 7;
|
||||
repeated int32 available_hero_ids = 8;
|
||||
repeated .net.eagle0.eagle.api.command.util.BattalionWithFoodCost available_battalions = 10;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ case class RandomState[+T](
|
||||
newValue: T,
|
||||
nextRandom: FunctionalRandom
|
||||
) {
|
||||
def map[U](f: T => U): RandomState[U] = RandomState(f(newValue), nextRandom)
|
||||
def map[U](f: T => U): RandomState[U] = RandomState(f(newValue), nextRandom)
|
||||
def continue[U](f: (T, FunctionalRandom) => RandomState[U]): RandomState[U] =
|
||||
f(newValue, nextRandom)
|
||||
|
||||
@@ -62,9 +62,10 @@ abstract class FunctionalRandom {
|
||||
else ni.nextRandom.nextInt(atLeast = atLeast, lessThan = lessThan)
|
||||
} else {
|
||||
val diff = lessThan - atLeast
|
||||
val max = (Int.MaxValue / diff) * diff
|
||||
val np = nextPositiveInt
|
||||
if np.newValue > max then np.nextRandom.nextInt(atLeast = atLeast, lessThan = lessThan)
|
||||
val max = (Int.MaxValue / diff) * diff
|
||||
val np = nextPositiveInt
|
||||
if np.newValue > max then
|
||||
np.nextRandom.nextInt(atLeast = atLeast, lessThan = lessThan)
|
||||
else RandomState(np.newValue % diff + atLeast, np.nextRandom)
|
||||
}
|
||||
}
|
||||
@@ -105,7 +106,7 @@ abstract class FunctionalRandom {
|
||||
nextItems[Double](count, _.nextDouble)
|
||||
|
||||
def nextPositiveInt: RandomState[Int] = {
|
||||
val ni = nextInt
|
||||
val ni = nextInt
|
||||
val nonNegative =
|
||||
if ni.newValue < 0 then -(ni.newValue + 1)
|
||||
else ni.newValue
|
||||
@@ -126,11 +127,12 @@ abstract class FunctionalRandom {
|
||||
shuffledHead: Vector[T],
|
||||
unshuffledTail: Vector[T],
|
||||
fr: FunctionalRandom
|
||||
): RandomState[Vector[T]] =
|
||||
): RandomState[Vector[T]] = {
|
||||
if unshuffledTail.isEmpty then RandomState(shuffledHead, fr)
|
||||
else if unshuffledTail.length == 1 then RandomState(shuffledHead ++ unshuffledTail, fr)
|
||||
else if unshuffledTail.length == 1 then
|
||||
RandomState(shuffledHead ++ unshuffledTail, fr)
|
||||
else {
|
||||
val ni = fr.nextInt(0, unshuffledTail.length)
|
||||
val ni = fr.nextInt(0, unshuffledTail.length)
|
||||
val index = ni.newValue
|
||||
|
||||
if index == 0 then {
|
||||
@@ -141,19 +143,20 @@ abstract class FunctionalRandom {
|
||||
)
|
||||
} else {
|
||||
val nextHead = shuffledHead :+ unshuffledTail(index)
|
||||
val newTail = unshuffledTail.splitAt(index) match {
|
||||
val newTail = unshuffledTail.splitAt(index) match {
|
||||
case (l, r) =>
|
||||
(l.drop(1) :+ unshuffledTail.head) ++ r.drop(1)
|
||||
}
|
||||
go(nextHead, newTail, ni.nextRandom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go(Vector(), seq, this)
|
||||
}
|
||||
|
||||
def nextRandomSubset[T](size: Int, seq: Vector[T]): RandomState[Vector[T]] =
|
||||
nextShuffled(seq).map(_.take(size))
|
||||
nextShuffled(seq).map { _.take(size) }
|
||||
|
||||
private def nextFlatMapImpl[T, U, S[X] <: Seq[X], FACT <: SeqFactory[S]](
|
||||
seq: S[T]
|
||||
@@ -161,9 +164,8 @@ abstract class FunctionalRandom {
|
||||
f: (T, FunctionalRandom) => RandomState[S[U]]
|
||||
)(using xFactory: FACT): RandomState[S[U]] = {
|
||||
@nowarn def foldGen(rsU: RandomState[S[U]], t: T): RandomState[S[U]] =
|
||||
rsU.continue {
|
||||
case (us: S[U], fr: FunctionalRandom) =>
|
||||
f(t, fr).map(x => (us ++ x).asInstanceOf[S[U]])
|
||||
rsU.continue { case (us: S[U], fr: FunctionalRandom) =>
|
||||
f(t, fr).map(x => (us ++ x).asInstanceOf[S[U]])
|
||||
}
|
||||
FunctionalRandom.nextFoldLeft(
|
||||
seq,
|
||||
@@ -187,9 +189,8 @@ abstract class FunctionalRandom {
|
||||
rsU: RandomState[S[U]],
|
||||
t: T
|
||||
): RandomState[S[U]] =
|
||||
rsU.continue {
|
||||
case (us: S[U], fr: FunctionalRandom) =>
|
||||
f(t, fr).map(x => (us :+ x).asInstanceOf[S[U]])
|
||||
rsU.continue { case (us: S[U], fr: FunctionalRandom) =>
|
||||
f(t, fr).map(x => (us :+ x).asInstanceOf[S[U]])
|
||||
}
|
||||
|
||||
FunctionalRandom.nextFoldLeft(seq, RandomState(xFactory.empty[U], this))(
|
||||
@@ -212,10 +213,11 @@ abstract class FunctionalRandom {
|
||||
): RandomState[Vector[U]] = remVec match {
|
||||
case head +: tail =>
|
||||
val newAccRS =
|
||||
if pf.isDefinedAt(head) then pf(head)(accRS.nextRandom).map(accRS.newValue :+ _)
|
||||
if pf.isDefinedAt(head) then
|
||||
pf(head)(accRS.nextRandom).map(accRS.newValue :+ _)
|
||||
else accRS
|
||||
go(tail, newAccRS)
|
||||
case _ => accRS
|
||||
case _ => accRS
|
||||
}
|
||||
|
||||
go(vec, RandomState(Vector(), this))
|
||||
@@ -230,7 +232,7 @@ abstract class FunctionalRandom {
|
||||
case head +: tail =>
|
||||
if pf.isDefinedAt(head) then pf(head)(this).map(Option(_))
|
||||
else go(tail)
|
||||
case _ => ??? // above cases should cover
|
||||
case _ => ??? // above cases should cover
|
||||
}
|
||||
|
||||
go(seq)
|
||||
@@ -247,10 +249,11 @@ abstract class FunctionalRandom {
|
||||
case items if items.isEmpty => accRS
|
||||
case head +: tail =>
|
||||
val newAccRS =
|
||||
if pf.isDefinedAt(head) then pf(head)(accRS.nextRandom).map(accRS.newValue ++ _)
|
||||
if pf.isDefinedAt(head) then
|
||||
pf(head)(accRS.nextRandom).map(accRS.newValue ++ _)
|
||||
else accRS
|
||||
go(tail, newAccRS)
|
||||
case _ => ??? // above cases should cover
|
||||
case _ => ??? // above cases should cover
|
||||
}
|
||||
|
||||
go(vec, RandomState(Vector(), this))
|
||||
@@ -278,13 +281,13 @@ abstract class FunctionalRandom {
|
||||
|
||||
case class SeededRandom(override val seed: Long) extends FunctionalRandom {
|
||||
private val magicMultiplier = 0x5deece66dL
|
||||
private val magicAdder = 0xbL
|
||||
private val mask = 0xffffffffffffffffL
|
||||
private val magicAdder = 0xbL
|
||||
private val mask = 0xffffffffffffffffL
|
||||
|
||||
override def nextInt: RandomState[Int] = {
|
||||
// borrowed from Functional Programming in Scala
|
||||
val newSeed = (seed * magicMultiplier + magicAdder) & mask
|
||||
val next = SeededRandom(newSeed)
|
||||
val newSeed = (seed * magicMultiplier + magicAdder) & mask
|
||||
val next = SeededRandom(newSeed)
|
||||
val newValue = (newSeed >>> 16).toInt
|
||||
RandomState(newValue, next)
|
||||
}
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
package net.eagle0.common
|
||||
|
||||
import java.util.concurrent.{Executors, TimeUnit}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
import io.grpc.{Status, StatusRuntimeException}
|
||||
import io.grpc.stub.AbstractStub
|
||||
import io.grpc.{Status, StatusRuntimeException}
|
||||
|
||||
import java.util.concurrent.{Executors, TimeUnit}
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
final case class GrpcRetrier[Stub <: AbstractStub[Stub]](grpcClient: Stub) {
|
||||
implicit val ec: ExecutionContext =
|
||||
ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
|
||||
|
||||
private val firstDelayMillis = TimeUnit.SECONDS.toMillis(1)
|
||||
private val maxDelayMillis = TimeUnit.SECONDS.toMillis(10)
|
||||
private val timeoutSeconds = 1200
|
||||
private val maxDelayMillis = TimeUnit.SECONDS.toMillis(10)
|
||||
private val timeoutSeconds = 1200
|
||||
|
||||
private def go[Response](
|
||||
clientOp: Stub => Future[Response],
|
||||
delayMillis: Long
|
||||
): Future[Response] =
|
||||
clientOp(grpcClient.withDeadlineAfter(timeoutSeconds, TimeUnit.SECONDS)).recoverWith {
|
||||
case sre: StatusRuntimeException if sre.getStatus.getCode == Status.Code.UNAVAILABLE =>
|
||||
println(
|
||||
s"Unavailable, retrying in ${TimeUnit.MILLISECONDS.toSeconds(delayMillis)}s..."
|
||||
)
|
||||
Thread.sleep(delayMillis)
|
||||
go(
|
||||
clientOp = clientOp,
|
||||
delayMillis = Math.min(delayMillis * 2, maxDelayMillis)
|
||||
)
|
||||
case sre: StatusRuntimeException if sre.getStatus.getCode == Status.Code.CANCELLED =>
|
||||
println("Cancelled, reconnecting...")
|
||||
go(
|
||||
clientOp = clientOp,
|
||||
delayMillis = firstDelayMillis
|
||||
)
|
||||
case fr: Throwable => throw fr
|
||||
}
|
||||
): Future[Response] = {
|
||||
clientOp(grpcClient.withDeadlineAfter(timeoutSeconds, TimeUnit.SECONDS))
|
||||
.recoverWith {
|
||||
case sre: StatusRuntimeException
|
||||
if sre.getStatus.getCode == Status.Code.UNAVAILABLE =>
|
||||
println(
|
||||
s"Unavailable, retrying in ${TimeUnit.MILLISECONDS.toSeconds(delayMillis)}s..."
|
||||
)
|
||||
Thread.sleep(delayMillis)
|
||||
go(
|
||||
clientOp = clientOp,
|
||||
delayMillis = Math.min(delayMillis * 2, maxDelayMillis)
|
||||
)
|
||||
case sre: StatusRuntimeException
|
||||
if sre.getStatus.getCode == Status.Code.CANCELLED =>
|
||||
println("Cancelled, reconnecting...")
|
||||
go(
|
||||
clientOp = clientOp,
|
||||
delayMillis = firstDelayMillis
|
||||
)
|
||||
case fr: Throwable => throw fr
|
||||
}
|
||||
}
|
||||
|
||||
def withRetries[Response](
|
||||
clientOp: Stub => Future[Response]
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package net.eagle0.common
|
||||
|
||||
import java.net.URL
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.native.Json
|
||||
import org.json4s.jvalue2extractable
|
||||
|
||||
import java.net.URL
|
||||
import scala.io.Source
|
||||
import scala.util.Using
|
||||
|
||||
import org.json4s.jvalue2extractable
|
||||
import org.json4s.native.Json
|
||||
import org.json4s.DefaultFormats
|
||||
|
||||
object JsonUtils {
|
||||
implicit val jsonFormats: DefaultFormats.type = DefaultFormats
|
||||
private val json = new Json(jsonFormats)
|
||||
private val json = new Json(jsonFormats)
|
||||
|
||||
// This will just crash if the source url is not available, or if the
|
||||
// json is not a String:Vector[String] map
|
||||
|
||||
@@ -3,9 +3,9 @@ package net.eagle0.common
|
||||
import scala.collection.mutable
|
||||
|
||||
case class Metrics[T](logFrequency: Int) {
|
||||
private var counters = mutable.Map[T, Int]()
|
||||
private var counters = mutable.Map[T, Int]()
|
||||
private var totalCount = 0
|
||||
private var lastLog = 0
|
||||
private var lastLog = 0
|
||||
|
||||
def increment(counter: T): Unit =
|
||||
this.synchronized {
|
||||
@@ -19,15 +19,15 @@ case class Metrics[T](logFrequency: Int) {
|
||||
}
|
||||
|
||||
def logCounters: Unit = this.synchronized {
|
||||
counters.toVector.sortBy(-_._2).foreach {
|
||||
case (t, count) =>
|
||||
println(s"$count $t")
|
||||
counters.toVector.sortBy(-_._2).foreach { case (t, count) =>
|
||||
println(s"$count $t")
|
||||
}
|
||||
}
|
||||
|
||||
def maybeLogCounters: Unit =
|
||||
def maybeLogCounters: Unit = {
|
||||
if totalCount - lastLog > logFrequency then {
|
||||
logCounters
|
||||
lastLog = totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package net.eagle0.common
|
||||
|
||||
import scala.collection.generic.IsIterable
|
||||
import scala.collection.Factory
|
||||
import scala.collection.generic.IsIterable
|
||||
|
||||
object MoreSeq {
|
||||
extension [A, Repr[_]](coll: Repr[A])(using itr: IsIterable[Repr[A]])
|
||||
@@ -17,13 +17,11 @@ object MoreSeq {
|
||||
|
||||
def irregularTranspose[A](coll: Iterable[Seq[A]]): Vector[Vector[A]] = {
|
||||
val maxLength = coll.map(_.length).max
|
||||
coll.foldLeft(Vector.fill(maxLength)(Vector[A]())) {
|
||||
case (acc, nextLine) =>
|
||||
acc.zipWithIndex.map {
|
||||
case (vec, idx) =>
|
||||
if idx < nextLine.length then vec :+ nextLine(idx)
|
||||
else vec
|
||||
}
|
||||
coll.foldLeft(Vector.fill(maxLength)(Vector[A]())) { case (acc, nextLine) =>
|
||||
acc.zipWithIndex.map { case (vec, idx) =>
|
||||
if idx < nextLine.length then vec :+ nextLine(idx)
|
||||
else vec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package net.eagle0.common
|
||||
|
||||
import java.io.{BufferedInputStream, InputStream}
|
||||
|
||||
import scala.util.Using
|
||||
|
||||
import scalapb.{GeneratedMessage, GeneratedMessageCompanion}
|
||||
|
||||
import java.io.{BufferedInputStream, InputStream}
|
||||
import scala.util.Using
|
||||
|
||||
object ProtoParser {
|
||||
def parseZipped[T <: GeneratedMessage](inputStream: InputStream)(using
|
||||
obj: GeneratedMessageCompanion[T]
|
||||
|
||||
@@ -3,9 +3,8 @@ package net.eagle0.common
|
||||
import java.io.{FileOutputStream, OutputStream, OutputStreamWriter}
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Paths
|
||||
import java.time.{Instant, ZoneOffset}
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
import java.time.{Instant, ZoneOffset}
|
||||
import scala.util.{Failure, Success, Using}
|
||||
|
||||
object SimpleTimedLogger {
|
||||
@@ -35,7 +34,7 @@ object SimpleTimedLogger {
|
||||
private def format(string: String): String =
|
||||
s"${formatter.format(Instant.now())} $string"
|
||||
|
||||
private def formatLine(string: String): String = s"${format(string)}\n"
|
||||
private def formatLine(string: String): String = s"${format(string)}\n"
|
||||
private def formatNoLine(string: String): String = s"${format(string)} "
|
||||
|
||||
private class PrintLogger extends SimpleTimedLoggerT {
|
||||
@@ -44,14 +43,15 @@ object SimpleTimedLogger {
|
||||
override def logLine(str: String): Unit = print(formatLine(str))
|
||||
}
|
||||
|
||||
private class SimpleTimedLogger(osConstructor: () => OutputStream) extends SimpleTimedLoggerT {
|
||||
private class SimpleTimedLogger(osConstructor: () => OutputStream)
|
||||
extends SimpleTimedLoggerT {
|
||||
def log(str: String): Unit = write(formatNoLine(str))
|
||||
|
||||
def logLine(str: String): Unit = write(formatLine(str))
|
||||
|
||||
private def write(str: String): Unit =
|
||||
Using(new OutputStreamWriter(osConstructor(), StandardCharsets.UTF_8)) { fw =>
|
||||
fw.write(str)
|
||||
Using(new OutputStreamWriter(osConstructor(), StandardCharsets.UTF_8)) {
|
||||
fw => fw.write(str)
|
||||
} match {
|
||||
case Success(_) => ()
|
||||
case Failure(exception) =>
|
||||
@@ -68,7 +68,7 @@ trait SimpleTimedLoggerT {
|
||||
def logLine(str: String): Unit
|
||||
|
||||
def withStopwatch[T](str: String)(f: => T): T = {
|
||||
val start = System.currentTimeMillis()
|
||||
val start = System.currentTimeMillis()
|
||||
val result = f
|
||||
logLine(s"Finished $str in ${System.currentTimeMillis() - start} ms")
|
||||
result
|
||||
|
||||
@@ -2,7 +2,6 @@ package net.eagle0.common
|
||||
|
||||
import java.io.{File, PrintWriter}
|
||||
import java.net.URL
|
||||
|
||||
import scala.collection.immutable.ListMap
|
||||
import scala.io.Source
|
||||
import scala.util.{Try, Using}
|
||||
@@ -34,11 +33,11 @@ object TsvUtils {
|
||||
}
|
||||
|
||||
def loadLines(url: URL): Try[Vector[Vector[String]]] =
|
||||
Using(Source.fromURL(url, "UTF-8"))(loadLines)
|
||||
Using(Source.fromURL(url, "UTF-8")) { loadLines }
|
||||
|
||||
// Loads a tsv and returns a Vector of arrays of strings.
|
||||
def loadLines(path: String): Try[Vector[Vector[String]]] =
|
||||
Using(Source.fromFile(path, "UTF-8"))(loadLines)
|
||||
Using(Source.fromFile(path, "UTF-8")) { loadLines }
|
||||
|
||||
def loadMaps(url: URL): Try[Vector[ListMap[String, String]]] =
|
||||
loadLines(url).map(linesToMaps)
|
||||
@@ -80,7 +79,7 @@ object TsvUtils {
|
||||
private def mapsToLines(
|
||||
maps: Vector[Map[String, String]]
|
||||
): Vector[Vector[String]] = {
|
||||
val headers = maps.head.keys.toVector
|
||||
val headers = maps.head.keys.toVector
|
||||
val otherLines = maps.map(oneMap => headers.map(oneMap))
|
||||
|
||||
headers +: otherLines
|
||||
@@ -88,11 +87,12 @@ object TsvUtils {
|
||||
|
||||
private def saveLines(lines: Vector[Vector[String]], url: URL): Unit = {
|
||||
val file = new File(url.getPath)
|
||||
val pw = new PrintWriter(file)
|
||||
val pw = new PrintWriter(file)
|
||||
|
||||
try
|
||||
try {
|
||||
lines.foreach(line => pw.write(line.mkString("\t") + "\n"))
|
||||
finally
|
||||
} finally {
|
||||
pw.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ object ZipUtils {
|
||||
val os = new ByteArrayOutputStream()
|
||||
|
||||
val buf = new Array[Byte](1024)
|
||||
var n = gz.read(buf)
|
||||
var n = gz.read(buf)
|
||||
while n > 0 do {
|
||||
os.write(buf, 0, n)
|
||||
n = gz.read(buf)
|
||||
|
||||
@@ -8,10 +8,10 @@ object ApiKeys {
|
||||
"src/main/scala/net/eagle0/common/llm_integration/api_keys.txt"
|
||||
|
||||
// These must have valid values set in api_keys.txt
|
||||
private val openAIKeyName = "openai_api_key"
|
||||
private val openAIKeyName = "openai_api_key"
|
||||
private val anthropicKeyName = "anthropic_api_key"
|
||||
|
||||
private def readDictionary(): Map[String, String] =
|
||||
private def readDictionary(): Map[String, String] = {
|
||||
Using(scala.io.Source.fromFile(apiKeyFilePath)) { source =>
|
||||
val lines = source.getLines().filterNot(_.startsWith("#")).toList
|
||||
lines.map { line =>
|
||||
@@ -27,19 +27,21 @@ object ApiKeys {
|
||||
"Failed to read api_keys.txt. Make sure you have copied api_keys_template.txt to api_keys.txt and set the keys."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val dictionary = readDictionary()
|
||||
|
||||
private def getKey(key: String): String =
|
||||
private def getKey(key: String): String = {
|
||||
dictionary.get(key) match {
|
||||
case None =>
|
||||
case None =>
|
||||
throw new ApiKeyException(s"$key not found")
|
||||
case Some(value) if value.startsWith("your_") =>
|
||||
throw new ApiKeyException(
|
||||
s"API key for $key is not set. Please set it in $apiKeyFilePath."
|
||||
)
|
||||
case Some(value) => value
|
||||
case Some(value) => value
|
||||
}
|
||||
}
|
||||
|
||||
lazy val openAI: String = getKey(openAIKeyName)
|
||||
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
package net.eagle0.common.llm_integration
|
||||
|
||||
import java.net.{URI, URL}
|
||||
import net.eagle0.common.llm_integration.ClaudeServiceImpl.{
|
||||
dictionaryFor,
|
||||
messageVector
|
||||
}
|
||||
import org.json4s.native.{Json, Serialization}
|
||||
import org.json4s.{DefaultFormats, JString}
|
||||
import org.json4s.jvalue2extractable
|
||||
import org.json4s.jvalue2monadic
|
||||
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpRequest.BodyPublishers
|
||||
import java.net.{URI, URL}
|
||||
import java.time.{Duration, ZonedDateTime}
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.function.Consumer
|
||||
|
||||
import net.eagle0.common.llm_integration.ClaudeServiceImpl.{dictionaryFor, messageVector}
|
||||
import org.json4s.{DefaultFormats, JString}
|
||||
import org.json4s.jvalue2extractable
|
||||
import org.json4s.jvalue2monadic
|
||||
import org.json4s.native.{Json, Serialization}
|
||||
|
||||
object ClaudeServiceImpl {
|
||||
val model: String = "claude-sonnet-4-20250514"
|
||||
val maxTokens = 2048
|
||||
val maxTokens = 2048
|
||||
|
||||
private val apiKey = ApiKeys.anthropic
|
||||
private val apiKey = ApiKeys.anthropic
|
||||
private val baseURL = new URL("https://api.anthropic.com/v1/messages")
|
||||
private def dictionaryFor(
|
||||
modelName: String
|
||||
): Map[String, Any] = Map(
|
||||
"model" -> modelName,
|
||||
): Map[String, Any] = Map(
|
||||
"model" -> modelName,
|
||||
"max_tokens" -> maxTokens,
|
||||
"stream" -> true
|
||||
"stream" -> true
|
||||
)
|
||||
|
||||
private def messageVector(inputText: String): Vector[Map[String, String]] =
|
||||
Vector(
|
||||
Map(
|
||||
"role" -> "user",
|
||||
"role" -> "user",
|
||||
"content" -> inputText
|
||||
)
|
||||
)
|
||||
@@ -80,14 +83,14 @@ class ClaudeServiceImpl(
|
||||
private class ClaudeStringConsumer(
|
||||
streamingConsumer: Consumer[StreamingTextResults]
|
||||
) extends Consumer[String] {
|
||||
private val json = new Json(DefaultFormats)
|
||||
private val json = new Json(DefaultFormats)
|
||||
private var streamId = ""
|
||||
|
||||
override def accept(t: String): Unit = {
|
||||
val parsedJson = json.parse(t)
|
||||
|
||||
parsedJson \ "type" match {
|
||||
case JString("message_start") =>
|
||||
case JString("message_start") =>
|
||||
streamId = (parsedJson \ "message" \ "id").extract[String]
|
||||
case JString("content_block_delta") =>
|
||||
(parsedJson \ "delta" \ "text") match {
|
||||
@@ -99,7 +102,7 @@ class ClaudeServiceImpl(
|
||||
completed = false
|
||||
)
|
||||
)
|
||||
case _ => ???
|
||||
case _ => ???
|
||||
}
|
||||
case JString("content_block_start") => ()
|
||||
case JString("content_block_stop") => ()
|
||||
@@ -113,12 +116,12 @@ class ClaudeServiceImpl(
|
||||
completed = true
|
||||
)
|
||||
)
|
||||
case JString("error") =>
|
||||
case JString("error") =>
|
||||
val errorDetails = parsedJson \ "error"
|
||||
val errorType = (errorDetails \ "type").extract[String]
|
||||
val errorType = (errorDetails \ "type").extract[String]
|
||||
val errorMessage = (errorDetails \ "message").extract[String]
|
||||
println(s"Got an \"($errorType)\" error from Claude: $errorMessage")
|
||||
case _ => ???
|
||||
case _ => ???
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,24 +136,26 @@ class ClaudeServiceImpl(
|
||||
headers: Map[String, Vector[String]]
|
||||
): Option[RateLimits] =
|
||||
for {
|
||||
requestLimit <- headers.get("anthropic-ratelimit-requests-limit")
|
||||
tokenLimit <- headers.get("anthropic-ratelimit-tokens-limit")
|
||||
requestLimit <- headers.get("anthropic-ratelimit-requests-limit")
|
||||
tokenLimit <- headers.get("anthropic-ratelimit-tokens-limit")
|
||||
requestsRemaining <- headers.get("anthropic-ratelimit-requests-remaining")
|
||||
tokensRemaining <- headers.get("anthropic-ratelimit-tokens-remaining")
|
||||
requestResetTime <- headers.get("anthropic-ratelimit-requests-reset")
|
||||
tokenResetTime <- headers.get("anthropic-ratelimit-tokens-reset")
|
||||
} yield RateLimits(
|
||||
requestLimit = requestLimit.head.toInt,
|
||||
tokenLimit = tokenLimit.head.toInt,
|
||||
requestsRemaining = requestsRemaining.head.toInt,
|
||||
tokensRemaining = tokensRemaining.head.toInt,
|
||||
requestResetTime = ZonedDateTime.parse(
|
||||
requestResetTime.head,
|
||||
rfc3339formatter
|
||||
),
|
||||
tokenResetTime = ZonedDateTime.parse(
|
||||
tokenResetTime.head,
|
||||
rfc3339formatter
|
||||
tokensRemaining <- headers.get("anthropic-ratelimit-tokens-remaining")
|
||||
requestResetTime <- headers.get("anthropic-ratelimit-requests-reset")
|
||||
tokenResetTime <- headers.get("anthropic-ratelimit-tokens-reset")
|
||||
} yield {
|
||||
RateLimits(
|
||||
requestLimit = requestLimit.head.toInt,
|
||||
tokenLimit = tokenLimit.head.toInt,
|
||||
requestsRemaining = requestsRemaining.head.toInt,
|
||||
tokensRemaining = tokensRemaining.head.toInt,
|
||||
requestResetTime = ZonedDateTime.parse(
|
||||
requestResetTime.head,
|
||||
rfc3339formatter
|
||||
),
|
||||
tokenResetTime = ZonedDateTime.parse(
|
||||
tokenResetTime.head,
|
||||
rfc3339formatter
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+27
-20
@@ -1,22 +1,25 @@
|
||||
package net.eagle0.common.llm_integration
|
||||
|
||||
import java.net.http.{HttpClient, HttpRequest, HttpResponse, HttpTimeoutException}
|
||||
import net.eagle0.common.sse.SseSubscriber
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.http.HttpClient.{Redirect, Version}
|
||||
import java.net.http.HttpResponse.ResponseInfo
|
||||
import java.net.HttpURLConnection
|
||||
import java.time.{Duration, ZonedDateTime}
|
||||
import java.net.http.{
|
||||
HttpClient,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
HttpTimeoutException
|
||||
}
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.{Timer, TimerTask}
|
||||
import java.time.{Duration, ZonedDateTime}
|
||||
import java.util.concurrent.CompletionException
|
||||
import java.util.function.Consumer
|
||||
|
||||
import java.util.{Timer, TimerTask}
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise}
|
||||
import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala}
|
||||
import scala.jdk.FutureConverters.CompletionStageOps
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
import net.eagle0.common.sse.SseSubscriber
|
||||
|
||||
enum ExternalTextGenerationError extends Error:
|
||||
case RateLimit(code: Int, msg: String)
|
||||
case Http(code: Int, msg: String)
|
||||
@@ -44,10 +47,10 @@ final class ExternalTextGenerationCaller(
|
||||
private var inProgressCount = 0
|
||||
|
||||
def maxConcurrentStreams: Int = serviceImpl.maxConcurrentStreams
|
||||
def getInProgressCount: Int = inProgressCount
|
||||
def getInProgressCount: Int = inProgressCount
|
||||
|
||||
private var currentRateLimits: Option[RateLimits] = None
|
||||
private var currentRateLimitTime: ZonedDateTime = ZonedDateTime.now()
|
||||
private var currentRateLimitTime: ZonedDateTime = ZonedDateTime.now()
|
||||
|
||||
def millisUntilReset: Long =
|
||||
ChronoUnit.MILLIS.between(
|
||||
@@ -71,8 +74,9 @@ final class ExternalTextGenerationCaller(
|
||||
inputText: String,
|
||||
partialCompletion: Option[String],
|
||||
streamingConsumer: Consumer[StreamingTextResults],
|
||||
backoffSeconds: Double = ExternalTextGenerationCaller.initialBackoffSeconds
|
||||
): Future[HttpResponse[Unit]] =
|
||||
backoffSeconds: Double =
|
||||
ExternalTextGenerationCaller.initialBackoffSeconds
|
||||
): Future[HttpResponse[Unit]] = {
|
||||
streamCompletion(
|
||||
inputText = inputText,
|
||||
request = serviceImpl.makeRequest(
|
||||
@@ -82,6 +86,7 @@ final class ExternalTextGenerationCaller(
|
||||
backoffSeconds = backoffSeconds,
|
||||
streamingConsumer = streamingConsumer
|
||||
)
|
||||
}
|
||||
|
||||
private def streamCompletion(
|
||||
inputText: String,
|
||||
@@ -95,18 +100,20 @@ final class ExternalTextGenerationCaller(
|
||||
println(s"Trying again after $backoffSeconds seconds...")
|
||||
|
||||
val promise = Promise[HttpResponse[Unit]]()
|
||||
val t = new Timer()
|
||||
val t = new Timer()
|
||||
t.schedule(
|
||||
new TimerTask {
|
||||
override def run(): Unit =
|
||||
override def run(): Unit = {
|
||||
promise.completeWith(
|
||||
streamCompletion(
|
||||
inputText = inputText,
|
||||
request = request,
|
||||
backoffSeconds = ExternalTextGenerationCaller.nextBackoff(backoffSeconds),
|
||||
backoffSeconds =
|
||||
ExternalTextGenerationCaller.nextBackoff(backoffSeconds),
|
||||
streamingConsumer = streamingConsumer
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
(backoffSeconds * 1000).toLong
|
||||
)
|
||||
@@ -117,19 +124,19 @@ final class ExternalTextGenerationCaller(
|
||||
httpClient
|
||||
.sendAsync(
|
||||
request,
|
||||
(respInfo: ResponseInfo) =>
|
||||
(respInfo: ResponseInfo) => {
|
||||
if respInfo.statusCode() == HttpURLConnection.HTTP_OK then
|
||||
new SseSubscriber(serviceImpl.stringConsumer(streamingConsumer))
|
||||
else
|
||||
throw new RuntimeException(
|
||||
s"Unexpected response code: ${respInfo.statusCode()}"
|
||||
)
|
||||
}
|
||||
)
|
||||
.asScala
|
||||
.andThen {
|
||||
case x =>
|
||||
inProgressCount -= 1
|
||||
x
|
||||
.andThen { case x =>
|
||||
inProgressCount -= 1
|
||||
x
|
||||
}
|
||||
.transform {
|
||||
case Success(httpResponse) =>
|
||||
@@ -187,7 +194,7 @@ final class ExternalTextGenerationCaller(
|
||||
case e: CompletionException =>
|
||||
println(s"CompletionException error $e")
|
||||
tryAgain()
|
||||
case e: Throwable =>
|
||||
case e: Throwable =>
|
||||
println(s"error $e")
|
||||
tryAgain()
|
||||
}
|
||||
|
||||
+6
-4
@@ -1,7 +1,7 @@
|
||||
package net.eagle0.common.llm_integration
|
||||
|
||||
import scala.concurrent.duration.{Duration, SECONDS}
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration.{Duration, SECONDS}
|
||||
//import scala.util.Random
|
||||
|
||||
object ExternalTextGenerationCallerApp {
|
||||
@@ -66,11 +66,13 @@ object ExternalTextGenerationCallerApp {
|
||||
|Alakanda's faction controls Atfordia.
|
||||
|Roger the Shrubber's faction controls Chapellia.""".stripMargin
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
val completion = caller.streamCompletion(
|
||||
val startTime = System.currentTimeMillis()
|
||||
val completion = caller.streamCompletion(
|
||||
inputText = inputText,
|
||||
partialCompletion = None,
|
||||
streamingConsumer = results => print(results.value)
|
||||
streamingConsumer = results => {
|
||||
print(results.value)
|
||||
}
|
||||
)
|
||||
Await.result(completion, Duration(500, SECONDS))
|
||||
println("")
|
||||
|
||||
+52
-47
@@ -1,30 +1,33 @@
|
||||
package net.eagle0.common.llm_integration
|
||||
|
||||
import java.net.{URI, URL}
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpRequest.BodyPublishers
|
||||
import java.time.{Duration, ZonedDateTime}
|
||||
import java.util.function.Consumer
|
||||
|
||||
import net.eagle0.common.llm_integration.OpenAIChatCompletionsServiceImpl.{dictionaryFor, messageVector}
|
||||
import net.eagle0.common.llm_integration.OpenAIChatCompletionsServiceImpl.{
|
||||
dictionaryFor,
|
||||
messageVector
|
||||
}
|
||||
import org.json4s.native.{Json, Serialization}
|
||||
import org.json4s.{DefaultFormats, JArray, JObject, JString}
|
||||
import org.json4s.jvalue2extractable
|
||||
import org.json4s.jvalue2monadic
|
||||
import org.json4s.native.{Json, Serialization}
|
||||
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpRequest.BodyPublishers
|
||||
import java.net.{URI, URL}
|
||||
import java.time.{Duration, ZonedDateTime}
|
||||
import java.util.function.Consumer
|
||||
|
||||
object OpenAIChatCompletionsServiceImpl {
|
||||
val gpt5: String = "gpt-5"
|
||||
|
||||
private val apiKey = ApiKeys.openAI
|
||||
private val baseURL = new URL("https://api.openai.com/v1/chat/completions")
|
||||
private val apiKey = ApiKeys.openAI
|
||||
private val baseURL = new URL("https://api.openai.com/v1/chat/completions")
|
||||
private val temperature: Double = 1.0
|
||||
|
||||
private def dictionaryFor(
|
||||
modelName: String
|
||||
): Map[String, Any] = Map(
|
||||
"model" -> modelName,
|
||||
"temperature" -> temperature,
|
||||
"stream" -> true,
|
||||
): Map[String, Any] = Map(
|
||||
"model" -> modelName,
|
||||
"temperature" -> temperature,
|
||||
"stream" -> true,
|
||||
"reasoning_effort" -> "minimal"
|
||||
)
|
||||
|
||||
@@ -34,12 +37,12 @@ object OpenAIChatCompletionsServiceImpl {
|
||||
): Vector[Map[String, String]] =
|
||||
Vector(
|
||||
Map(
|
||||
"role" -> "user",
|
||||
"role" -> "user",
|
||||
"content" -> inputText
|
||||
)
|
||||
) ++ partialCompletion.map(pc =>
|
||||
Map(
|
||||
"role" -> "assistant",
|
||||
"role" -> "assistant",
|
||||
"content" -> pc
|
||||
)
|
||||
)
|
||||
@@ -96,18 +99,18 @@ class OpenAIChatCompletionsServiceImpl(
|
||||
override def stringConsumer(
|
||||
streamingConsumer: Consumer[StreamingTextResults]
|
||||
): Consumer[String] = (t: String) => {
|
||||
val json = new Json(DefaultFormats)
|
||||
val json = new Json(DefaultFormats)
|
||||
val parsedJson =
|
||||
try
|
||||
try {
|
||||
json.parse(t)
|
||||
catch {
|
||||
} catch {
|
||||
case pe: org.json4s.ParserUtil.ParseException =>
|
||||
println(s"Failed to parse JSON: $t")
|
||||
throw pe
|
||||
}
|
||||
|
||||
val streamId = (parsedJson \ "id").extract[String]
|
||||
val choice = (parsedJson \ "choices")
|
||||
val choice = (parsedJson \ "choices")
|
||||
.asInstanceOf[JArray]
|
||||
.arr
|
||||
.head
|
||||
@@ -123,7 +126,7 @@ class OpenAIChatCompletionsServiceImpl(
|
||||
case JString(str) =>
|
||||
println(s"Unexpected finish reason $str")
|
||||
true
|
||||
case _ => false
|
||||
case _ => false
|
||||
}
|
||||
|
||||
(choice \ "delta" \ "content") match {
|
||||
@@ -135,7 +138,7 @@ class OpenAIChatCompletionsServiceImpl(
|
||||
completed = completed
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
case _ =>
|
||||
if completed then
|
||||
streamingConsumer.accept(
|
||||
StreamingTextResults(
|
||||
@@ -151,30 +154,32 @@ class OpenAIChatCompletionsServiceImpl(
|
||||
headers: Map[String, Vector[String]]
|
||||
): Option[RateLimits] =
|
||||
for {
|
||||
requestLimit <- headers.get("x-ratelimit-limit-requests")
|
||||
tokenLimit <- headers.get("x-ratelimit-limit-tokens")
|
||||
requestLimit <- headers.get("x-ratelimit-limit-requests")
|
||||
tokenLimit <- headers.get("x-ratelimit-limit-tokens")
|
||||
requestsRemaining <- headers.get("x-ratelimit-remaining-requests")
|
||||
tokensRemaining <- headers.get("x-ratelimit-remaining-tokens")
|
||||
requestResetTime <- headers.get("x-ratelimit-reset-requests")
|
||||
tokenResetTime <- headers.get("x-ratelimit-reset-tokens")
|
||||
} yield RateLimits(
|
||||
requestLimit = requestLimit.head.toInt,
|
||||
tokenLimit = tokenLimit.head.toInt,
|
||||
requestsRemaining = requestsRemaining.head.toInt,
|
||||
tokensRemaining = tokensRemaining.head.toInt,
|
||||
requestResetTime = ZonedDateTime
|
||||
.now()
|
||||
.plus(
|
||||
OpenAiDurationParser
|
||||
.parseDuration(requestResetTime.head)
|
||||
.getOrElse(Duration.ZERO)
|
||||
),
|
||||
tokenResetTime = ZonedDateTime
|
||||
.now()
|
||||
.plus(
|
||||
OpenAiDurationParser
|
||||
.parseDuration(tokenResetTime.head)
|
||||
.getOrElse(Duration.ZERO)
|
||||
)
|
||||
)
|
||||
tokensRemaining <- headers.get("x-ratelimit-remaining-tokens")
|
||||
requestResetTime <- headers.get("x-ratelimit-reset-requests")
|
||||
tokenResetTime <- headers.get("x-ratelimit-reset-tokens")
|
||||
} yield {
|
||||
RateLimits(
|
||||
requestLimit = requestLimit.head.toInt,
|
||||
tokenLimit = tokenLimit.head.toInt,
|
||||
requestsRemaining = requestsRemaining.head.toInt,
|
||||
tokensRemaining = tokensRemaining.head.toInt,
|
||||
requestResetTime = ZonedDateTime
|
||||
.now()
|
||||
.plus(
|
||||
OpenAiDurationParser
|
||||
.parseDuration(requestResetTime.head)
|
||||
.getOrElse(Duration.ZERO)
|
||||
),
|
||||
tokenResetTime = ZonedDateTime
|
||||
.now()
|
||||
.plus(
|
||||
OpenAiDurationParser
|
||||
.parseDuration(tokenResetTime.head)
|
||||
.getOrElse(Duration.ZERO)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import java.time.Duration
|
||||
|
||||
object OpenAiDurationParser {
|
||||
private val durationRegex = """(\d+)((ms)|[smhdy])""".r
|
||||
private val durationMap = Map(
|
||||
private val durationMap = Map(
|
||||
"ms" -> Duration.ofMillis(1),
|
||||
"s" -> Duration.ofSeconds(1),
|
||||
"m" -> Duration.ofMinutes(1),
|
||||
"h" -> Duration.ofHours(1),
|
||||
"d" -> Duration.ofDays(1),
|
||||
"y" -> Duration.ofDays(365)
|
||||
"s" -> Duration.ofSeconds(1),
|
||||
"m" -> Duration.ofMinutes(1),
|
||||
"h" -> Duration.ofHours(1),
|
||||
"d" -> Duration.ofDays(1),
|
||||
"y" -> Duration.ofDays(365)
|
||||
)
|
||||
|
||||
def parseDuration(duration: String): Option[Duration] =
|
||||
@@ -18,7 +18,7 @@ object OpenAiDurationParser {
|
||||
.findAllMatchIn(duration)
|
||||
.map { m =>
|
||||
val amount = m.group(1).toInt
|
||||
val unit = m.group(2)
|
||||
val unit = m.group(2)
|
||||
durationMap(unit).multipliedBy(amount)
|
||||
}
|
||||
.toSeq match {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package net.eagle0.common.name_generation
|
||||
|
||||
import java.io.File
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
import net.eagle0.common.{SeededRandom, TsvUtils}
|
||||
|
||||
import java.io.File
|
||||
import scala.util.Random
|
||||
|
||||
class NameGenerator {
|
||||
private val random = SeededRandom(Random.nextLong())
|
||||
private val emptySubstitutions = Map[String, String]()
|
||||
private val random = SeededRandom(Random.nextLong())
|
||||
private val emptySubstitutions = Map[String, String]()
|
||||
private val nameConstructionUrl = new File(
|
||||
"/opt/nameConstruction.txt"
|
||||
).toURI.toURL
|
||||
private val namesFileUrl = new File("/opt/names.tsv").toURI.toURL
|
||||
private val namesFileUrl = new File("/opt/names.tsv").toURI.toURL
|
||||
|
||||
private val namesMap = TsvUtils.loadColumnMaps(namesFileUrl).get
|
||||
private val namesMap = TsvUtils.loadColumnMaps(namesFileUrl).get
|
||||
private val formatString =
|
||||
StringConstructionTokenGenerator.formatStringFromUrl(
|
||||
nameConstructionUrl
|
||||
@@ -41,17 +40,17 @@ class NameGenerator {
|
||||
atSpecializations = Vector("male")
|
||||
)
|
||||
|
||||
def nextFemaleName(): String =
|
||||
def nextFemaleName(): String =
|
||||
femaleTok.nextString(random, emptySubstitutions).newValue
|
||||
def nextMaleName(): String =
|
||||
def nextMaleName(): String =
|
||||
maleTok.nextString(random, emptySubstitutions).newValue
|
||||
def nextNeutralName(): String =
|
||||
allTok.nextString(random, emptySubstitutions).newValue
|
||||
|
||||
def nextName(): String = {
|
||||
val intR = random.nextInt(0, 20)
|
||||
val intR = random.nextInt(0, 20)
|
||||
val newInt = intR.newValue
|
||||
val tok =
|
||||
val tok =
|
||||
if newInt == 0 then allTok
|
||||
else if newInt <= 10 then femaleTok
|
||||
else maleTok
|
||||
|
||||
@@ -10,9 +10,8 @@ class StringConstructionParser(
|
||||
import ParseSequence.*
|
||||
|
||||
private def charString(seq: Seq[CharacterOrToken]): String =
|
||||
seq.collect {
|
||||
case C(char) =>
|
||||
char
|
||||
seq.collect { case C(char) =>
|
||||
char
|
||||
}.mkString
|
||||
|
||||
def parseLiteral(fmt: String): Option[(String, StringConstructionToken)] = {
|
||||
@@ -26,11 +25,12 @@ class StringConstructionParser(
|
||||
def parseRemaining(str: String, acc: String): ParseStatus =
|
||||
str.head match {
|
||||
case '\\' =>
|
||||
if str.length < 2 then throw new IllegalArgumentException("Found \\ at end of literal")
|
||||
if str.length < 2 then
|
||||
throw new IllegalArgumentException("Found \\ at end of literal")
|
||||
else parseRemaining(str.drop(2), acc + str.charAt(1))
|
||||
case '\"' =>
|
||||
ParseStatus(str.drop(1), acc)
|
||||
case _ =>
|
||||
case _ =>
|
||||
parseRemaining(str.drop(1), acc + str.head)
|
||||
}
|
||||
|
||||
@@ -41,44 +41,44 @@ class StringConstructionParser(
|
||||
|
||||
private def parseOptional(
|
||||
ts: ParseSequence
|
||||
): Try[(ParseSequence, StringConstructionToken)] =
|
||||
): Try[(ParseSequence, StringConstructionToken)] = {
|
||||
insideBalanced(ts, open = '{', closed = '}').flatMap {
|
||||
case (remainAfter, inWithOdds) =>
|
||||
val DecimalParseResults(inAfterOdds, odds) = readDecimal(inWithOdds)
|
||||
|
||||
parsePossiblyParsed(inAfterOdds).flatMap {
|
||||
case (rem, parsed) =>
|
||||
if rem.nonEmpty then
|
||||
Failure(
|
||||
new IllegalArgumentException(
|
||||
"Found extra characters inside optional"
|
||||
)
|
||||
parsePossiblyParsed(inAfterOdds).flatMap { case (rem, parsed) =>
|
||||
if rem.nonEmpty then
|
||||
Failure(
|
||||
new IllegalArgumentException(
|
||||
"Found extra characters inside optional"
|
||||
)
|
||||
else Success((remainAfter, OptionalToken(parsed, odds)))
|
||||
)
|
||||
else Success((remainAfter, OptionalToken(parsed, odds)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def parseOneof(
|
||||
ts: ParseSequence
|
||||
): Try[(ParseSequence, StringConstructionToken)] =
|
||||
): Try[(ParseSequence, StringConstructionToken)] = {
|
||||
insideBalanced(ts, open = '[', closed = ']').map {
|
||||
case (remainAfter, inside) =>
|
||||
var entries = Vector.empty[OneofListEntry]
|
||||
var pos = inside
|
||||
var pos = inside
|
||||
while pos.nonEmpty do {
|
||||
val t = pos.head
|
||||
if t == ',' then pos = pos.drop(1)
|
||||
else {
|
||||
val DecimalParseResults(remIn, weight) = readDecimal(pos)
|
||||
parsePossiblyParsed(remIn).map {
|
||||
case (rem, parsed) =>
|
||||
entries = entries :+ OneofListEntry(parsed, weight)
|
||||
pos = rem
|
||||
parsePossiblyParsed(remIn).map { case (rem, parsed) =>
|
||||
entries = entries :+ OneofListEntry(parsed, weight)
|
||||
pos = rem
|
||||
}
|
||||
}
|
||||
}
|
||||
(remainAfter, OneofListToken(entries))
|
||||
}
|
||||
}
|
||||
|
||||
def parseListSelector(
|
||||
ts: ParseSequence
|
||||
@@ -112,11 +112,12 @@ class StringConstructionParser(
|
||||
|
||||
def parseCapitalized(
|
||||
ts: ParseSequence
|
||||
): Try[(ParseSequence, StringConstructionToken)] =
|
||||
): Try[(ParseSequence, StringConstructionToken)] = {
|
||||
for {
|
||||
(remainAfter, inside) <- insideBalanced(ts, open = '-', closed = '+')
|
||||
(_, parsed) <- parsePossiblyParsed(inside)
|
||||
(_, parsed) <- parsePossiblyParsed(inside)
|
||||
} yield (remainAfter, TitleCaseToken(parsed))
|
||||
}
|
||||
|
||||
def parseOrdinal(
|
||||
ts: ParseSequence
|
||||
@@ -145,7 +146,7 @@ class StringConstructionParser(
|
||||
def go(
|
||||
tokens: ParseSequence,
|
||||
acc: Seq[StringConstructionToken]
|
||||
): Try[(ParseSequence, Seq[StringConstructionToken])] =
|
||||
): Try[(ParseSequence, Seq[StringConstructionToken])] = {
|
||||
if tokens.isEmpty then Success((Vector.empty, acc))
|
||||
else {
|
||||
tokens.head match {
|
||||
@@ -167,10 +168,10 @@ class StringConstructionParser(
|
||||
case T(t) => go(tokens.drop(1), acc :+ t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go(tokens, Vector.empty).map {
|
||||
case (rem, ts) =>
|
||||
(rem, if ts.size == 1 then ts.head else SequenceToken(ts))
|
||||
go(tokens, Vector.empty).map { case (rem, ts) =>
|
||||
(rem, if ts.size == 1 then ts.head else SequenceToken(ts))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +180,7 @@ class StringConstructionParser(
|
||||
def parseNext(
|
||||
remainingString: String,
|
||||
acc: ParseSequence
|
||||
): ParseSequence =
|
||||
): ParseSequence = {
|
||||
if remainingString.isEmpty then acc
|
||||
else
|
||||
remainingString.head match {
|
||||
@@ -191,18 +192,19 @@ class StringConstructionParser(
|
||||
throw new IllegalArgumentException(
|
||||
s"Illegal literal in $remainingString"
|
||||
)
|
||||
case _ =>
|
||||
case _ =>
|
||||
parseNext(
|
||||
remainingString.drop(1),
|
||||
acc :+ C(remainingString.head)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val tokens = parseNext(formatString, Vector.empty)
|
||||
parsePossiblyParsed(tokens).flatMap {
|
||||
case (rem, tok) =>
|
||||
if rem.nonEmpty then Failure(new IllegalArgumentException("Failed to parse"))
|
||||
else Success(tok)
|
||||
parsePossiblyParsed(tokens).flatMap { case (rem, tok) =>
|
||||
if rem.nonEmpty then
|
||||
Failure(new IllegalArgumentException("Failed to parse"))
|
||||
else Success(tok)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,8 +213,8 @@ class StringConstructionParser(
|
||||
open: Char,
|
||||
closed: Char
|
||||
): Try[(ParseSequence, ParseSequence)] = {
|
||||
var pos = ts.drop(1)
|
||||
var level = 1
|
||||
var pos = ts.drop(1)
|
||||
var level = 1
|
||||
var inside = Vector.empty[CharacterOrToken]
|
||||
|
||||
while level > 0 && pos.nonEmpty do {
|
||||
@@ -224,7 +226,8 @@ class StringConstructionParser(
|
||||
pos = pos.drop(1)
|
||||
}
|
||||
|
||||
if level > 0 then Failure(new IllegalArgumentException("Did not find closing character"))
|
||||
if level > 0 then
|
||||
Failure(new IllegalArgumentException("Did not find closing character"))
|
||||
|
||||
Success((pos, inside))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package net.eagle0.common.name_generation
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
import net.eagle0.common.JankyRandom
|
||||
|
||||
import java.nio.file.Path
|
||||
import scala.util.Random
|
||||
|
||||
object StringConstructionTester {
|
||||
def main(args: Array[String]): Unit = {
|
||||
val arraysPath = args(0)
|
||||
@@ -26,7 +25,7 @@ object StringConstructionTester {
|
||||
"longbowmen"
|
||||
)
|
||||
types.foreach { typeName =>
|
||||
val adj = s"${typeName}_adj"
|
||||
val adj = s"${typeName}_adj"
|
||||
val newSubs =
|
||||
substitutions + ("BATTALION_NAME" -> typeName) + ("BATTALION_ADJECTIVE" -> adj)
|
||||
|
||||
@@ -41,7 +40,7 @@ object StringConstructionTester {
|
||||
val random = new JankyRandom(new Random())
|
||||
|
||||
print(s"---\n$typeName\n---\n")
|
||||
(1 to 20).toVector.foreach { _ =>
|
||||
((1 to 20).toVector).foreach { _ =>
|
||||
println(token.nextString(random, Map("PLACE" -> "Pieska")).newValue)
|
||||
}
|
||||
print("\n\n\n")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.eagle0.common.name_generation
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
sealed trait StringConstructionToken {
|
||||
def nextString(
|
||||
random: FunctionalRandom,
|
||||
@@ -19,7 +19,8 @@ case class LiteralToken(literal: String) extends StringConstructionToken {
|
||||
RandomState(literal, random)
|
||||
}
|
||||
|
||||
class MissingLiteralKeyException(key: String) extends Exception(s"Missing literal key $key") {}
|
||||
class MissingLiteralKeyException(key: String)
|
||||
extends Exception(s"Missing literal key $key") {}
|
||||
|
||||
case class SubstitutionToken(key: String) extends StringConstructionToken {
|
||||
override def nextString(
|
||||
@@ -33,25 +34,27 @@ case class SubstitutionToken(key: String) extends StringConstructionToken {
|
||||
)
|
||||
}
|
||||
|
||||
case class SequenceToken(tokens: Seq[StringConstructionToken]) extends StringConstructionToken {
|
||||
case class SequenceToken(tokens: Seq[StringConstructionToken])
|
||||
extends StringConstructionToken {
|
||||
override def nextString(
|
||||
random: FunctionalRandom,
|
||||
literalSubstitutions: Map[String, String]
|
||||
): RandomState[String] =
|
||||
tokens.foldLeft(RandomState[String]("", random)) {
|
||||
case (state, tok) =>
|
||||
val nextStr = tok.nextString(state.nextRandom, literalSubstitutions)
|
||||
RandomState(state.newValue + nextStr.newValue, nextStr.nextRandom)
|
||||
tokens.foldLeft(RandomState[String]("", random)) { case (state, tok) =>
|
||||
val nextStr = tok.nextString(state.nextRandom, literalSubstitutions)
|
||||
RandomState(state.newValue + nextStr.newValue, nextStr.nextRandom)
|
||||
}
|
||||
}
|
||||
|
||||
case class OptionalToken(token: StringConstructionToken, odds: Double) extends StringConstructionToken {
|
||||
case class OptionalToken(token: StringConstructionToken, odds: Double)
|
||||
extends StringConstructionToken {
|
||||
override def nextString(
|
||||
random: FunctionalRandom,
|
||||
literalSubstitutions: Map[String, String]
|
||||
): RandomState[String] = {
|
||||
val state = random.nextDouble
|
||||
if state.newValue < odds then token.nextString(state.nextRandom, literalSubstitutions)
|
||||
if state.newValue < odds then
|
||||
token.nextString(state.nextRandom, literalSubstitutions)
|
||||
else RandomState("", state.nextRandom)
|
||||
}
|
||||
}
|
||||
@@ -64,32 +67,33 @@ case class OneofListEntry(token: StringConstructionToken, weight: Double) {
|
||||
token.nextString(random, literalSubstitutions)
|
||||
}
|
||||
|
||||
case class OneofListToken(entries: Seq[OneofListEntry]) extends StringConstructionToken {
|
||||
case class OneofListToken(entries: Seq[OneofListEntry])
|
||||
extends StringConstructionToken {
|
||||
override def nextString(
|
||||
random: FunctionalRandom,
|
||||
literalSubstitutions: Map[String, String]
|
||||
): RandomState[String] = {
|
||||
val totalWeight = entries.map(_.weight).sum
|
||||
random.nextDouble.continue {
|
||||
case (double, fr) =>
|
||||
@tailrec
|
||||
def go(
|
||||
remainingEntries: Seq[OneofListEntry],
|
||||
remainingD: Double
|
||||
): RandomState[String] = remainingEntries match {
|
||||
case Nil =>
|
||||
throw new IllegalStateException("Failed to find a valid oneof")
|
||||
case w +: tail =>
|
||||
if remainingD < w.weight then w.toString(fr, literalSubstitutions)
|
||||
else go(tail, remainingD - w.weight)
|
||||
}
|
||||
random.nextDouble.continue { case (double, fr) =>
|
||||
@tailrec
|
||||
def go(
|
||||
remainingEntries: Seq[OneofListEntry],
|
||||
remainingD: Double
|
||||
): RandomState[String] = remainingEntries match {
|
||||
case Nil =>
|
||||
throw new IllegalStateException("Failed to find a valid oneof")
|
||||
case w +: tail =>
|
||||
if remainingD < w.weight then w.toString(fr, literalSubstitutions)
|
||||
else go(tail, remainingD - w.weight)
|
||||
}
|
||||
|
||||
go(entries, double * totalWeight)
|
||||
go(entries, double * totalWeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class ListSelectionToken(choices: Seq[String]) extends StringConstructionToken {
|
||||
case class ListSelectionToken(choices: Seq[String])
|
||||
extends StringConstructionToken {
|
||||
override def nextString(
|
||||
random: FunctionalRandom,
|
||||
literalSubstitutions: Map[String, String]
|
||||
@@ -100,7 +104,7 @@ case class OrdinalSelectionToken(max: Int) extends StringConstructionToken {
|
||||
override def nextString(
|
||||
random: FunctionalRandom,
|
||||
literalSubstitutions: Map[String, String]
|
||||
): RandomState[String] =
|
||||
): RandomState[String] = {
|
||||
random.nextInt(1, max).map { value =>
|
||||
if value == 11 then "11th"
|
||||
else {
|
||||
@@ -112,9 +116,11 @@ case class OrdinalSelectionToken(max: Int) extends StringConstructionToken {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class TitleCaseToken(base: StringConstructionToken) extends StringConstructionToken {
|
||||
case class TitleCaseToken(base: StringConstructionToken)
|
||||
extends StringConstructionToken {
|
||||
val uncapitalizedWords: Set[String] =
|
||||
Set("and", "but", "for", "or", "nor", "the", "a", "an", "to", "as", "of")
|
||||
|
||||
@@ -122,8 +128,8 @@ case class TitleCaseToken(base: StringConstructionToken) extends StringConstruct
|
||||
words match {
|
||||
case first +: middle :+ last =>
|
||||
first.capitalize +: middle :+ last.capitalize
|
||||
case Vector(only: String) => Vector(only.capitalize)
|
||||
case Vector() => Vector()
|
||||
case Vector(only: String) => Vector(only.capitalize)
|
||||
case Vector() => Vector()
|
||||
}
|
||||
|
||||
private def byWordCapitalized(string: String): Vector[String] =
|
||||
@@ -135,8 +141,9 @@ case class TitleCaseToken(base: StringConstructionToken) extends StringConstruct
|
||||
override def nextString(
|
||||
random: FunctionalRandom,
|
||||
literalSubstitutions: Map[String, String]
|
||||
): RandomState[String] =
|
||||
): RandomState[String] = {
|
||||
base.nextString(random, literalSubstitutions).map { cap =>
|
||||
firstAndLastCapitalized(byWordCapitalized(cap)).mkString(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -1,7 +1,6 @@
|
||||
package net.eagle0.common.name_generation
|
||||
|
||||
import java.net.URL
|
||||
|
||||
import scala.io.Source
|
||||
import scala.util.Using
|
||||
|
||||
@@ -23,8 +22,8 @@ object StringConstructionTokenGenerator {
|
||||
}
|
||||
|
||||
val (stringsMap, unfilteredStringsMap) = {
|
||||
val allSpecializations = namesMap.map {
|
||||
case (key, words) =>
|
||||
val allSpecializations = namesMap
|
||||
.map { case (key, words) =>
|
||||
key.split("@", 2) match {
|
||||
case Array(realKey) => (realKey, "", words)
|
||||
case Array(realKey, spec) => (realKey, spec, words)
|
||||
@@ -33,14 +32,17 @@ object StringConstructionTokenGenerator {
|
||||
"Too many @s in column name"
|
||||
)
|
||||
}
|
||||
}.toVector.groupBy(_._1)
|
||||
}
|
||||
.toVector
|
||||
.groupBy { _._1 }
|
||||
|
||||
val stringsMap = allSpecializations.map {
|
||||
case (realWord, specializations) =>
|
||||
val includedSpecializations =
|
||||
specializations.filter(spec => spec._2.isEmpty || atSpecializations.contains(spec._2))
|
||||
val stringsMap = allSpecializations
|
||||
.map { case (realWord, specializations) =>
|
||||
val includedSpecializations = specializations.filter(spec =>
|
||||
spec._2.isEmpty || atSpecializations.contains(spec._2)
|
||||
)
|
||||
(realWord, includedSpecializations.flatMap(_._3))
|
||||
}
|
||||
}
|
||||
|
||||
val unfilteredStringsMap = allSpecializations.map {
|
||||
case (realWord, specializations) =>
|
||||
|
||||
@@ -5,10 +5,10 @@ import scala.annotation.tailrec
|
||||
// Functions for parsing events from an SSE stream. Uses spec as defined in
|
||||
// https://html.spec.whatwg.org/multipage/server-sent-events.html
|
||||
object SseEventReader {
|
||||
private val DataKey = "data"
|
||||
private val EventKey = "event"
|
||||
private val IdKey = "id"
|
||||
private val RetryKey = "retry"
|
||||
private val DataKey = "data"
|
||||
private val EventKey = "event"
|
||||
private val IdKey = "id"
|
||||
private val RetryKey = "retry"
|
||||
private val DefaultEventValue = "message"
|
||||
|
||||
sealed trait ReadResult
|
||||
@@ -36,7 +36,8 @@ object SseEventReader {
|
||||
else
|
||||
readLines(
|
||||
IncompleteEvent(
|
||||
eventType = carryoverEvent.map(_.eventType).getOrElse(DefaultEventValue),
|
||||
eventType =
|
||||
carryoverEvent.map(_.eventType).getOrElse(DefaultEventValue),
|
||||
metadata = carryoverEvent.map(_.metadata).getOrElse(Map.empty),
|
||||
data = carryoverEvent.map(_.data).getOrElse(""),
|
||||
carryover = carryoverEvent.map(_.carryover).getOrElse("") + text
|
||||
@@ -52,14 +53,14 @@ object SseEventReader {
|
||||
if value.startsWith(" ") then value.drop(1) else value
|
||||
|
||||
key match {
|
||||
case DataKey =>
|
||||
case DataKey =>
|
||||
IncompleteEvent(
|
||||
eventType = acc.eventType,
|
||||
metadata = acc.metadata,
|
||||
data = acc.data + trimmedSpaceValue,
|
||||
carryover = acc.carryover
|
||||
)
|
||||
case EventKey =>
|
||||
case EventKey =>
|
||||
IncompleteEvent(
|
||||
eventType = trimmedSpaceValue,
|
||||
metadata = acc.metadata,
|
||||
@@ -73,23 +74,23 @@ object SseEventReader {
|
||||
data = acc.data,
|
||||
carryover = acc.carryover
|
||||
)
|
||||
case _ =>
|
||||
case _ =>
|
||||
// The spec actually says to ignore fields except if the key is "event", "data", "id", or "retry"
|
||||
acc
|
||||
}
|
||||
}
|
||||
|
||||
@tailrec
|
||||
private def readLines(acc: IncompleteEvent): ReadResult =
|
||||
private def readLines(acc: IncompleteEvent): ReadResult = {
|
||||
acc.carryover.split("\n", 2) match {
|
||||
case Array("", moreText) => // blank line indicates end of event
|
||||
case Array("", moreText) => // blank line indicates end of event
|
||||
CompleteEvent(
|
||||
eventType = acc.eventType,
|
||||
metadata = acc.metadata,
|
||||
data = acc.data,
|
||||
remainingText = moreText
|
||||
)
|
||||
case Array(singleLine) => // did not end in a newline
|
||||
case Array(singleLine) => // did not end in a newline
|
||||
IncompleteEvent(
|
||||
eventType = acc.eventType,
|
||||
metadata = acc.metadata,
|
||||
@@ -100,7 +101,7 @@ object SseEventReader {
|
||||
case Array(kv, newCarryover) =>
|
||||
val accWithNewCarryover = acc.copy(carryover = newCarryover)
|
||||
kv.split(":", 2) match {
|
||||
case Array("", _) =>
|
||||
case Array("", _) =>
|
||||
// Line started with a colon; ignore line
|
||||
readLines(accWithNewCarryover)
|
||||
case Array(key, value) =>
|
||||
@@ -108,13 +109,14 @@ object SseEventReader {
|
||||
readLines(
|
||||
withSingleKeyValuePair(accWithNewCarryover, key, value)
|
||||
)
|
||||
case Array(key) =>
|
||||
case Array(key) =>
|
||||
// Line contained a key with no value, use empty string
|
||||
readLines(
|
||||
withSingleKeyValuePair(accWithNewCarryover, key, "")
|
||||
)
|
||||
case _ => ???
|
||||
case _ => ???
|
||||
}
|
||||
case _ => ???
|
||||
case _ => ???
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
package net.eagle0.common.sse
|
||||
|
||||
import java.net.http.HttpResponse.BodySubscriber
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.ByteBuffer
|
||||
import java.util
|
||||
import java.util.concurrent.{CompletableFuture, CompletionStage, Flow}
|
||||
import java.util.function.Consumer
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.jdk.CollectionConverters.CollectionHasAsScala
|
||||
|
||||
import org.json4s.ParserUtil
|
||||
|
||||
class SseException(message: String, cause: Throwable = null) extends Exception(message, cause)
|
||||
import java.net.http.HttpResponse.BodySubscriber
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util
|
||||
import java.util.concurrent.{CompletableFuture, CompletionStage, Flow}
|
||||
import java.util.function.Consumer
|
||||
import scala.annotation.tailrec
|
||||
import scala.jdk.CollectionConverters.CollectionHasAsScala
|
||||
|
||||
class SseSubscriber(messageDataConsumer: Consumer[String]) extends BodySubscriber[Unit] {
|
||||
class SseException(message: String, cause: Throwable = null)
|
||||
extends Exception(message, cause)
|
||||
|
||||
class SseSubscriber(messageDataConsumer: Consumer[String])
|
||||
extends BodySubscriber[Unit] {
|
||||
@volatile private var subscription: Flow.Subscription = _
|
||||
private val future: CompletableFuture[Unit] = new CompletableFuture[Unit]
|
||||
private val future: CompletableFuture[Unit] = new CompletableFuture[Unit]
|
||||
|
||||
private val DoneToken = "[DONE]"
|
||||
private val DoneToken = "[DONE]"
|
||||
private val MessageType = "message"
|
||||
|
||||
override def getBody: CompletionStage[Unit] = future
|
||||
@@ -40,7 +41,7 @@ class SseSubscriber(messageDataConsumer: Consumer[String]) extends BodySubscribe
|
||||
|
||||
@volatile private var incompleteText: Option[SseEventReader.IncompleteEvent] =
|
||||
None
|
||||
override def onNext(buffers: util.List[ByteBuffer]): Unit = try {
|
||||
override def onNext(buffers: util.List[ByteBuffer]): Unit = try {
|
||||
val newText = buffers.asScala
|
||||
.map(StandardCharsets.UTF_8.decode)
|
||||
.mkString
|
||||
@@ -49,7 +50,7 @@ class SseSubscriber(messageDataConsumer: Consumer[String]) extends BodySubscribe
|
||||
def consumeEvents(
|
||||
incompleteEvent: Option[SseEventReader.IncompleteEvent],
|
||||
remainingText: String
|
||||
): Option[SseEventReader.IncompleteEvent] =
|
||||
): Option[SseEventReader.IncompleteEvent] = {
|
||||
SseEventReader.readOneEvent(incompleteEvent, remainingText) match {
|
||||
case SseEventReader.EndOfStream =>
|
||||
None
|
||||
@@ -90,6 +91,7 @@ class SseSubscriber(messageDataConsumer: Consumer[String]) extends BodySubscribe
|
||||
case incomplete: SseEventReader.IncompleteEvent =>
|
||||
Some(incomplete)
|
||||
}
|
||||
}
|
||||
|
||||
this.incompleteText = consumeEvents(this.incompleteText, newText)
|
||||
|
||||
@@ -104,7 +106,7 @@ class SseSubscriber(messageDataConsumer: Consumer[String]) extends BodySubscribe
|
||||
println(fullBuffer)
|
||||
future.completeExceptionally(pe)
|
||||
subscription.cancel()
|
||||
case e: Exception =>
|
||||
case e: Exception =>
|
||||
future.completeExceptionally(e)
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package net.eagle0.eagle
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.language.existentials
|
||||
import scala.util.Random
|
||||
|
||||
import io.grpc.{Server, ServerBuilder}
|
||||
import net.eagle0.common.{FunctionalRandom, SeededRandom, SimpleTimedLogger}
|
||||
import net.eagle0.eagle.api.eagle.EagleGrpc
|
||||
import net.eagle0.eagle.service.*
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.language.existentials
|
||||
import scala.util.Random
|
||||
|
||||
object Main {
|
||||
import ServerSetupHelpers.*
|
||||
|
||||
private val eagleGrpcPortKey = Symbol("eagleGrpcPort")
|
||||
private val eagleGrpcPortKey = Symbol("eagleGrpcPort")
|
||||
private val defaultEagleGrpcPort = "40032"
|
||||
|
||||
private val shardokInterfaceRemoteAddressKey = Symbol(
|
||||
private val shardokInterfaceRemoteAddressKey = Symbol(
|
||||
"shardokInterfaceRemoteAddress"
|
||||
)
|
||||
private val defaultShardokInterfaceRemoteAddress = "eagle0.net:443"
|
||||
@@ -26,25 +26,26 @@ object Main {
|
||||
def nextOption(
|
||||
map: Map[Symbol, String],
|
||||
list: Vector[String]
|
||||
): Map[Symbol, String] =
|
||||
): Map[Symbol, String] = {
|
||||
list match {
|
||||
case "--eagle-grpc-port" +: value +: tail =>
|
||||
case "--eagle-grpc-port" +: value +: tail =>
|
||||
nextOption(map ++ Map(eagleGrpcPortKey -> value), tail)
|
||||
case "--shardok-interface-remote-address" +: value +: tail =>
|
||||
nextOption(
|
||||
map ++ Map(shardokInterfaceRemoteAddressKey -> value),
|
||||
tail
|
||||
)
|
||||
case "--gpt-model-name" +: value +: tail =>
|
||||
case "--gpt-model-name" +: value +: tail =>
|
||||
nextOption(
|
||||
map ++ Map(gptModelNameKey -> value),
|
||||
tail
|
||||
)
|
||||
case option +: _ =>
|
||||
case option +: _ =>
|
||||
throw new IllegalArgumentException("Unknown option " + option)
|
||||
|
||||
case _ => map
|
||||
}
|
||||
}
|
||||
nextOption(Map(), args.toVector)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package net.eagle0.eagle
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
//import net.eagle0.eagle.common.action_result_type.ActionResultType.PROVINCE_CONQUERED
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.INVITATION_ACCEPTED
|
||||
import net.eagle0.eagle.service.persistence.{eagleExtension, LocalFilePersister}
|
||||
|
||||
import net.eagle0.eagle.service.PersistedHistory
|
||||
import net.eagle0.eagle.service.persistence.{LocalFilePersister, eagleExtension}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
// USAGE
|
||||
// bazel run //src/main/scala/net/eagle0/eagle:remove_actions -- /Users/dancrosby/eagle0/eagle/save/ f0fef63f600f31f8 11
|
||||
@@ -23,7 +24,7 @@ object RemoveActions {
|
||||
val persister = new LocalFilePersister(
|
||||
path + gameId.toHexString
|
||||
)
|
||||
val history =
|
||||
val history =
|
||||
PersistedHistory.gameHistoryFromPersister(gameId).get
|
||||
|
||||
val found = history.recentHistory.indexWhere(
|
||||
@@ -53,8 +54,8 @@ object RemoveActions {
|
||||
}
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
val path = args(0)
|
||||
val gameId = BigInt(args(1), 16).toLong
|
||||
val path = args(0)
|
||||
val gameId = BigInt(args(1), 16).toLong
|
||||
val countToTruncate = args(2).toInt
|
||||
|
||||
truncate(path = path, gameId = gameId, countToTruncate = countToTruncate)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package net.eagle0.eagle
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.service.persistence.LocalFilePersister
|
||||
import net.eagle0.eagle.service.PersistedHistory
|
||||
import scalapb.{GeneratedMessage, GeneratedMessageCompanion}
|
||||
import net.eagle0.eagle.service.persistence.LocalFilePersister
|
||||
import scalapb.descriptors.*
|
||||
import scalapb.{GeneratedMessage, GeneratedMessageCompanion}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
object MessageComparators {
|
||||
@tailrec
|
||||
@@ -18,13 +18,14 @@ object MessageComparators {
|
||||
acc: Vector[FieldDescriptor] = Vector()
|
||||
): Vector[FieldDescriptor] = fieldNames match {
|
||||
case name +: tail =>
|
||||
val field =
|
||||
val field = {
|
||||
comp.scalaDescriptor.fields
|
||||
.find(_.name == name)
|
||||
.getOrElse {
|
||||
println(s"No field $name in ${comp.scalaDescriptor.name}")
|
||||
sys.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if tail.isEmpty then acc :+ field
|
||||
else
|
||||
@@ -33,7 +34,7 @@ object MessageComparators {
|
||||
tail,
|
||||
acc :+ field
|
||||
)
|
||||
case _ => acc
|
||||
case _ => acc
|
||||
}
|
||||
|
||||
def nestedPValue(
|
||||
@@ -49,7 +50,7 @@ object MessageComparators {
|
||||
gm.getFieldByNumber(fd.number).asInstanceOf[GeneratedMessage],
|
||||
tail
|
||||
)
|
||||
case _ => gm.toPMessage
|
||||
case _ => gm.toPMessage
|
||||
}
|
||||
|
||||
def typedComparison[T](
|
||||
@@ -58,17 +59,19 @@ object MessageComparators {
|
||||
value: String
|
||||
): (GeneratedMessage => Boolean) = comparator match {
|
||||
case "=" =>
|
||||
gm =>
|
||||
nestedPValue(gm, fieldDescriptors) match {
|
||||
case PInt(i) => i == value.toInt
|
||||
case PDouble(d) => d == value.toDouble
|
||||
case PString(s) => s == value
|
||||
case PBoolean(b) => b == value.toBoolean
|
||||
case PEmpty => value.isEmpty
|
||||
case x =>
|
||||
throw new NotImplementedError(s"$x pvalue not implemented")
|
||||
}
|
||||
case x => throw new NotImplementedError(s"$x comparator not implemented")
|
||||
(
|
||||
gm =>
|
||||
nestedPValue(gm, fieldDescriptors) match {
|
||||
case PInt(i) => i == value.toInt
|
||||
case PDouble(d) => d == value.toDouble
|
||||
case PString(s) => s == value
|
||||
case PBoolean(b) => b == value.toBoolean
|
||||
case PEmpty => value.isEmpty
|
||||
case x =>
|
||||
throw new NotImplementedError(s"$x pvalue not implemented")
|
||||
}
|
||||
)
|
||||
case x => throw new NotImplementedError(s"$x comparator not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +80,9 @@ object GameStateFilters {
|
||||
|
||||
def filter(filter: String): ActionWithResultingState => Boolean = {
|
||||
val (left, rightWithComparator) = filter.span(!comparisonChars.contains(_))
|
||||
val (comparator, right) = rightWithComparator.splitAt(1)
|
||||
val (comparator, right) = rightWithComparator.splitAt(1)
|
||||
|
||||
val fieldNames = left.split('.').toVector
|
||||
val fieldNames = left.split('.').toVector
|
||||
val (prefix, remaining) = fieldNames.splitAt(1)
|
||||
prefix.head match {
|
||||
case "gs" =>
|
||||
@@ -118,11 +121,11 @@ object SavedGameUtils {
|
||||
def filterOne(
|
||||
results: Vector[ActionWithResultingState],
|
||||
filter: String
|
||||
): Vector[ActionWithResultingState] =
|
||||
results.filter {
|
||||
case awrs =>
|
||||
GameStateFilters.filter(filter)(awrs)
|
||||
): Vector[ActionWithResultingState] = {
|
||||
results.filter { case awrs =>
|
||||
GameStateFilters.filter(filter)(awrs)
|
||||
}
|
||||
}
|
||||
|
||||
def filteredResults(
|
||||
results: Vector[ActionWithResultingState],
|
||||
@@ -138,9 +141,9 @@ object SavedGameUtils {
|
||||
def fieldValues(
|
||||
results: Vector[ActionWithResultingState],
|
||||
fields: Vector[String]
|
||||
): Vector[FieldWithValue] =
|
||||
): Vector[FieldWithValue] = {
|
||||
for {
|
||||
result <- results
|
||||
result <- results
|
||||
qualifiedFieldName <- fields
|
||||
} yield {
|
||||
val (ident, subfieldNames) = qualifiedFieldName.split('.').splitAt(1)
|
||||
@@ -169,6 +172,7 @@ object SavedGameUtils {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
val argList = args.toList
|
||||
@@ -203,7 +207,8 @@ object SavedGameUtils {
|
||||
)
|
||||
|
||||
def withoutFilterIndices(indices: Vector[Int]): FilteredResultsState = {
|
||||
val newFilters = filters.zipWithIndex.filterNot { case (_, idx) => indices.contains(idx) }
|
||||
val newFilters = filters.zipWithIndex
|
||||
.filterNot { case (_, idx) => indices.contains(idx) }
|
||||
.map(_._1)
|
||||
this.copy(
|
||||
filters = newFilters,
|
||||
@@ -213,7 +218,8 @@ object SavedGameUtils {
|
||||
|
||||
def withoutPrintIndices(indices: Vector[Int]): FilteredResultsState =
|
||||
this.copy(
|
||||
prints = prints.zipWithIndex.filterNot { case (_, idx) => indices.contains(idx) }
|
||||
prints = prints.zipWithIndex
|
||||
.filterNot { case (_, idx) => indices.contains(idx) }
|
||||
.map(_._1)
|
||||
)
|
||||
}
|
||||
@@ -222,14 +228,12 @@ object SavedGameUtils {
|
||||
state: FilteredResultsState
|
||||
): FilteredResultsState = {
|
||||
println("Your filters:")
|
||||
state.filters.zipWithIndex.foreach {
|
||||
case (filt, idx) =>
|
||||
println(s" ($idx) $filt")
|
||||
state.filters.zipWithIndex.foreach { case (filt, idx) =>
|
||||
println(s" ($idx) $filt")
|
||||
}
|
||||
println("Your prints:")
|
||||
state.prints.zipWithIndex.foreach {
|
||||
case (pf, idx) =>
|
||||
println(s" ($idx) $pf")
|
||||
state.prints.zipWithIndex.foreach { case (pf, idx) =>
|
||||
println(s" ($idx) $pf")
|
||||
}
|
||||
println(s"${state.partialResults.size} filtered results")
|
||||
|
||||
@@ -244,8 +248,8 @@ object SavedGameUtils {
|
||||
state.withoutFilterIndices(idxStrings.map(_.toInt).toVector)
|
||||
case "dp" :: idxStrings =>
|
||||
state.withoutPrintIndices(idxStrings.map(_.toInt).toVector)
|
||||
case "q" :: _ => sys.exit(0)
|
||||
case x :: _ =>
|
||||
case "q" :: _ => sys.exit(0)
|
||||
case x :: _ =>
|
||||
println(s"Unknown command $x, stopping")
|
||||
state
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.eagle0.eagle
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
import net.eagle0.eagle.service.new_game_creation.GameParametersUtils
|
||||
import net.eagle0.eagle.service.ServerSetupHelpers.newGamesManager
|
||||
import net.eagle0.eagle.service.new_game_creation.GameParametersUtils
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
object TestAIGame {
|
||||
def main(args: Array[String]): Unit = {
|
||||
@@ -18,7 +18,8 @@ object TestAIGame {
|
||||
|
||||
(1 to gameCount).foreach { _ =>
|
||||
gamesManager.synchronizedCreateGame(
|
||||
expandedGameParameters = GameParametersUtils.defaultExpandedGameParameters,
|
||||
expandedGameParameters =
|
||||
GameParametersUtils.defaultExpandedGameParameters,
|
||||
gameId = Random.nextLong(),
|
||||
humanPlayers = Vector.empty,
|
||||
aiPlayerCount = 5
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import scala.collection.Map
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand
|
||||
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.Engine
|
||||
import net.eagle0.eagle.library.util.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
AlmsCommandSelector,
|
||||
AttackDecisionCommandChooser,
|
||||
CommandChooser
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.library.Engine
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
|
||||
import scala.collection.Map
|
||||
|
||||
case class AIClientWithSelectedCommand(
|
||||
client: AIClient,
|
||||
@@ -118,7 +118,7 @@ case class AIClient(
|
||||
gs: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[AIClientWithSelectedCommand] = {
|
||||
val opac = acs.head._2
|
||||
val opac = acs.head._2
|
||||
val oneProvinceAcs = acs.head._2.commands
|
||||
|
||||
CommandChooser.choose(
|
||||
@@ -215,7 +215,7 @@ case class AIClient(
|
||||
) match {
|
||||
case RandomState(Some(cs), fr) =>
|
||||
RandomState(AIClientWithSelectedCommand(this, Some(cs)), fr)
|
||||
case RandomState(None, fr) =>
|
||||
case RandomState(None, fr) =>
|
||||
if EarlyGameAIClient.isEarlyGame(gs, factionId) then
|
||||
EarlyGameAIClient
|
||||
.chooseEarlyGameCommand(
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.eagle.{FactionId, GameId}
|
||||
|
||||
import java.io.FileWriter
|
||||
import java.nio.file.Paths
|
||||
|
||||
import scala.util.Using
|
||||
|
||||
import net.eagle0.eagle.{FactionId, GameId}
|
||||
|
||||
class AIClientLogger(gameId: GameId, factionId: FactionId) {
|
||||
private val logFile = Paths
|
||||
.get(
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
|
||||
object AIClientUtils {
|
||||
def extraHeroCount(pid: ProvinceId, fid: FactionId, gs: GameState): Int =
|
||||
(gs.provinces(pid).rulingFactionHeroIds.size - desiredHeroCount(
|
||||
(gs.provinces(pid.value).rulingFactionHeroIds.size - desiredHeroCount(
|
||||
pid,
|
||||
fid,
|
||||
gs
|
||||
)).max(0)
|
||||
|
||||
def desiredHeroCount(pid: ProvinceId, fid: FactionId, gs: GameState): Int =
|
||||
def desiredHeroCount(pid: ProvinceId, fid: FactionId, gs: GameState): Int = {
|
||||
if LegacyFactionUtils.hostileNeighbors(pid, fid, gs).nonEmpty then 3
|
||||
else if gs.provinces(pid).support < 65 then 2
|
||||
else if gs.provinces(pid.value).support < 65 then 2
|
||||
else 1
|
||||
}
|
||||
|
||||
def desiredCountForMarchTowardFocus(
|
||||
originProvince: Province,
|
||||
@@ -54,7 +55,11 @@ object AIClientUtils {
|
||||
originProvince = originProvince,
|
||||
destinationProvince = destinationProvince,
|
||||
availableHeroIds = availableHeroIds,
|
||||
factionLeaders = gs.factions(originProvince.rulingFactionId.get).leaders.toVector,
|
||||
factionLeaders = gs
|
||||
.factions(originProvince.rulingFactionId.get)
|
||||
.leaders
|
||||
.toVector
|
||||
.map(HeroId(_)),
|
||||
favorLeaders = favorLeaders
|
||||
),
|
||||
gs,
|
||||
@@ -69,19 +74,21 @@ object AIClientUtils {
|
||||
favorLeaders: Boolean
|
||||
): Vector[HeroId] = {
|
||||
val availableHeroes = availableHeroIds
|
||||
.map(gs.heroes)
|
||||
.map(hid => gs.heroes(hid.value))
|
||||
|
||||
if availableHeroes.size <= desiredCount then availableHeroes.map(_.id)
|
||||
if availableHeroes.size <= desiredCount then
|
||||
availableHeroes.map(h => HeroId(h.id))
|
||||
else
|
||||
availableHeroes
|
||||
.sortBy(hero =>
|
||||
(
|
||||
if favorLeaders then !LegacyFactionUtils.isFactionLeader(hero.id, gs)
|
||||
else LegacyFactionUtils.isFactionLeader(hero.id, gs),
|
||||
if favorLeaders then
|
||||
!LegacyFactionUtils.isFactionLeader(HeroId(hero.id), gs)
|
||||
else LegacyFactionUtils.isFactionLeader(HeroId(hero.id), gs),
|
||||
-LegacyHeroUtils.power(hero)
|
||||
)
|
||||
)
|
||||
.map(_.id)
|
||||
.map(h => HeroId(h.id))
|
||||
.take(desiredCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.FactionId
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.{
|
||||
@@ -11,9 +12,8 @@ import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooser
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.DeterministicCommandChooser
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.FactionId
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
|
||||
object EarlyGameAIClient {
|
||||
def isEarlyGame(gs: GameState, factionId: FactionId): Boolean =
|
||||
@@ -26,7 +26,7 @@ object EarlyGameAIClient {
|
||||
gs: GameState,
|
||||
acs: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[CommandSelection] =
|
||||
): RandomState[CommandSelection] = {
|
||||
CommandChooser
|
||||
.choose(
|
||||
actingFactionId = actingFactionId,
|
||||
@@ -40,4 +40,5 @@ object EarlyGameAIClient {
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.map(_.get)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.ProvinceDistances
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
|
||||
object FactionLeaderProvinceRanker {
|
||||
private def factionLeaderProvinceOrdering(
|
||||
@@ -25,7 +25,7 @@ object FactionLeaderProvinceRanker {
|
||||
): Ordering[Province] = Ordering.by((p: Province) =>
|
||||
ProvinceDistances.distanceThroughFriendliesOption(
|
||||
fromProvinceId,
|
||||
p.id,
|
||||
ProvinceId(p.id),
|
||||
factionId,
|
||||
gameState
|
||||
)
|
||||
@@ -36,7 +36,9 @@ object FactionLeaderProvinceRanker {
|
||||
factionId: FactionId,
|
||||
gameState: GameState
|
||||
): Ordering[Province] = Ordering
|
||||
.by((p: Province) => LegacyFactionUtils.hostileNeighbors(p, factionId, gameState).size)
|
||||
.by((p: Province) =>
|
||||
LegacyFactionUtils.hostileNeighbors(p, factionId, gameState).size
|
||||
)
|
||||
.reverse
|
||||
|
||||
// Orders by most-neutral-neighbors-first
|
||||
@@ -74,7 +76,7 @@ object FactionLeaderProvinceRanker {
|
||||
LegacyFactionUtils
|
||||
.provinces(factionId, gameState)
|
||||
.sorted(ordering)
|
||||
.map(_.id)
|
||||
.map(p => ProvinceId(p.id))
|
||||
}
|
||||
|
||||
def bestProvinceId(
|
||||
@@ -88,7 +90,7 @@ object FactionLeaderProvinceRanker {
|
||||
LegacyFactionUtils
|
||||
.provinces(factionId, gameState)
|
||||
.minOption(ordering)
|
||||
.map(_.id)
|
||||
.map(p => ProvinceId(p.id))
|
||||
}
|
||||
|
||||
def bestProvinceId(
|
||||
@@ -104,7 +106,7 @@ object FactionLeaderProvinceRanker {
|
||||
LegacyFactionUtils
|
||||
.provinces(factionId, gameState)
|
||||
.minOption(ordering)
|
||||
.map(_.id)
|
||||
.map(p => ProvinceId(p.id))
|
||||
}
|
||||
|
||||
def rankedProvinceIds(
|
||||
@@ -120,7 +122,7 @@ object FactionLeaderProvinceRanker {
|
||||
LegacyFactionUtils
|
||||
.provinces(factionId, gameState)
|
||||
.sorted(ordering)
|
||||
.map(_.id)
|
||||
.map(p => ProvinceId(p.id))
|
||||
}
|
||||
|
||||
def bestSettlementPidForFactionLeader(
|
||||
@@ -131,7 +133,7 @@ object FactionLeaderProvinceRanker {
|
||||
val ordering = factionLeaderProvinceOrdering(
|
||||
factionId = factionId,
|
||||
gameState = gameState,
|
||||
fromProvinceId = currentProvince.id
|
||||
fromProvinceId = ProvinceId(currentProvince.id)
|
||||
)
|
||||
|
||||
LegacyFactionUtils
|
||||
@@ -139,7 +141,7 @@ object FactionLeaderProvinceRanker {
|
||||
.filter(ordering.lt(_, currentProvince))
|
||||
.filterNot(LegacyProvinceUtils.ruledByFactionLeader(_, gameState))
|
||||
.minOption(ordering)
|
||||
.map(_.id)
|
||||
.map(p => ProvinceId(p.id))
|
||||
}
|
||||
|
||||
def bestSettlementPidForFactionLeader(
|
||||
@@ -149,7 +151,7 @@ object FactionLeaderProvinceRanker {
|
||||
): Option[ProvinceId] =
|
||||
bestSettlementPidForFactionLeader(
|
||||
factionId = factionId,
|
||||
currentProvince = gameState.provinces(currentProvinceId),
|
||||
currentProvince = gameState.provinces(currentProvinceId.value),
|
||||
gameState = gameState
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, MarchAvailableCommand}
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{
|
||||
AvailableCommand,
|
||||
MarchAvailableCommand
|
||||
}
|
||||
import net.eagle0.eagle.api.selected_command.MarchSelectedCommand
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.randomSelectionForType
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.FactionId
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
|
||||
object FixLeaderAloneCommandSelector {
|
||||
def maybeFixLeaderAlone(
|
||||
@@ -20,53 +23,60 @@ object FixLeaderAloneCommandSelector {
|
||||
randomSelectionForType[MarchAvailableCommand](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
case (ac, frOuter) =>
|
||||
frOuter.nextFlatCollectFirst(
|
||||
ac.oneProvinceCommands
|
||||
.filter(opmc =>
|
||||
gameState
|
||||
.provinces(opmc.originProvinceId)
|
||||
.rulingFactionHeroIds
|
||||
.filterNot(LegacyFactionUtils.isFactionLeader(_, gameState))
|
||||
.size > 1
|
||||
)
|
||||
) { opmc => fr =>
|
||||
opmc.availableDestinationProvinces
|
||||
.map(dest => gameState.provinces(dest.provinceId))
|
||||
.filter(_.rulingFactionId.contains(actingFactionId))
|
||||
.filter(_.rulingFactionHeroIds.size == 1)
|
||||
.filter(dest =>
|
||||
LegacyFactionUtils
|
||||
.isFactionLeader(dest.rulingFactionHeroIds.head, gameState)
|
||||
)
|
||||
.map { destination =>
|
||||
fr.nextRandomElement(opmc.availableHeroIds)
|
||||
.map { hid =>
|
||||
CombatUnit(factionId = actingFactionId, heroId = hid)
|
||||
}
|
||||
.map { combatUnit =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = ac.actingProvinceId,
|
||||
available = ac,
|
||||
selected = MarchSelectedCommand(
|
||||
gold = 0,
|
||||
food = 0,
|
||||
originProvince = opmc.originProvinceId,
|
||||
destinationProvinceId = destination.id,
|
||||
marchingUnits = Vector(
|
||||
combatUnit
|
||||
)
|
||||
),
|
||||
reason = "Moving a hero so that a faction leader won't be alone"
|
||||
)
|
||||
) { case (ac, frOuter) =>
|
||||
frOuter.nextFlatCollectFirst(
|
||||
ac.oneProvinceCommands
|
||||
.filter(opmc =>
|
||||
gameState
|
||||
.provinces(opmc.originProvinceId)
|
||||
.rulingFactionHeroIds
|
||||
.filterNot(hid =>
|
||||
LegacyFactionUtils.isFactionLeader(HeroId(hid), gameState)
|
||||
)
|
||||
.size > 1
|
||||
)
|
||||
) { opmc => fr =>
|
||||
opmc.availableDestinationProvinces
|
||||
.map(
|
||||
gameState.provinces
|
||||
)
|
||||
.filter(_.rulingFactionId.contains(actingFactionId))
|
||||
.filter(_.rulingFactionHeroIds.size == 1)
|
||||
.filter(dest =>
|
||||
LegacyFactionUtils
|
||||
.isFactionLeader(
|
||||
HeroId(dest.rulingFactionHeroIds.head),
|
||||
gameState
|
||||
)
|
||||
)
|
||||
.map { destination =>
|
||||
fr.nextRandomElement(opmc.availableHeroIds)
|
||||
.map { hid =>
|
||||
CombatUnit(factionId = actingFactionId.value, heroId = hid)
|
||||
}
|
||||
.map { combatUnit =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = ProvinceId(ac.actingProvinceId),
|
||||
available = ac,
|
||||
selected = MarchSelectedCommand(
|
||||
gold = 0,
|
||||
food = 0,
|
||||
originProvince = opmc.originProvinceId,
|
||||
destinationProvinceId = destination.id,
|
||||
marchingUnits = Vector(
|
||||
combatUnit
|
||||
)
|
||||
),
|
||||
reason =
|
||||
"Moving a hero so that a faction leader won't be alone"
|
||||
)
|
||||
}
|
||||
}
|
||||
.headOption
|
||||
.getOrElse(RandomState(None, fr))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.headOption
|
||||
.getOrElse(RandomState(None, fr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, DiplomacyAvailableCommand}
|
||||
import net.eagle0.eagle.api.available_command.{
|
||||
AvailableCommand,
|
||||
DiplomacyAvailableCommand
|
||||
}
|
||||
import net.eagle0.eagle.api.command.util.diplomacy_option.InvitationOption
|
||||
import net.eagle0.eagle.api.selected_command.DiplomacySelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.settings.MinChanceForAIInvite
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{ProvinceGoldSurplusCalculator, TrustForDiplomacy}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.FactionId
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
ProvinceGoldSurplusCalculator,
|
||||
TrustForDiplomacy
|
||||
}
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.library.settings.MinChanceForAIInvite
|
||||
|
||||
object InvitationCommandSelector {
|
||||
private case class InvitationOptionWithChance(
|
||||
@@ -21,41 +27,49 @@ object InvitationCommandSelector {
|
||||
gameState: GameState,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[DiplomacyAvailableCommand](acs = availableCommands) { diplomacyAvailableCommand =>
|
||||
diplomacyAvailableCommand.options.collect { case io: InvitationOption => io }.filter { invitationOption =>
|
||||
TrustForDiplomacy.meetsConditionsForInvitation(
|
||||
actingFactionId = actingFactionId,
|
||||
targetFactionId = invitationOption.targetFactionId,
|
||||
gameState = gameState
|
||||
)
|
||||
}.filter { invitationOption =>
|
||||
ProvinceGoldSurplusCalculator.provinceGoldSurplus(
|
||||
diplomacyAvailableCommand.actingProvinceId,
|
||||
gameState
|
||||
) >= invitationOption.goldCost
|
||||
}.map { invitationOption =>
|
||||
InvitationOptionWithChance(
|
||||
invitationOption = invitationOption,
|
||||
acceptanceChance = ResolveDiplomacyCommandSelector.invitationAcceptanceChance(
|
||||
actingFactionId,
|
||||
invitationOption.targetFactionId,
|
||||
gameState
|
||||
)
|
||||
)
|
||||
}.maxByOption(_.acceptanceChance)
|
||||
.filter(_.acceptanceChance >= MinChanceForAIInvite.intValue)
|
||||
.map { iowc =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = diplomacyAvailableCommand.actingProvinceId,
|
||||
available = diplomacyAvailableCommand,
|
||||
selected = DiplomacySelectedCommand(
|
||||
selectedOption = iowc.invitationOption,
|
||||
targetFactionId = iowc.invitationOption.targetFactionId,
|
||||
sentHeroId = diplomacyAvailableCommand.recommendedHeroId
|
||||
),
|
||||
reason = "maybeInviteOtherFaction"
|
||||
)
|
||||
}
|
||||
flatSelectionForType[DiplomacyAvailableCommand](acs = availableCommands) {
|
||||
diplomacyAvailableCommand =>
|
||||
diplomacyAvailableCommand.options
|
||||
.collect { case io: InvitationOption => io }
|
||||
.filter { invitationOption =>
|
||||
TrustForDiplomacy.meetsConditionsForInvitation(
|
||||
actingFactionId = actingFactionId,
|
||||
targetFactionId = FactionId(invitationOption.targetFactionId),
|
||||
gameState = gameState
|
||||
)
|
||||
}
|
||||
.filter { invitationOption =>
|
||||
ProvinceGoldSurplusCalculator.provinceGoldSurplus(
|
||||
ProvinceId(diplomacyAvailableCommand.actingProvinceId),
|
||||
gameState
|
||||
) >= invitationOption.goldCost
|
||||
}
|
||||
.map { invitationOption =>
|
||||
InvitationOptionWithChance(
|
||||
invitationOption = invitationOption,
|
||||
acceptanceChance =
|
||||
ResolveDiplomacyCommandSelector.invitationAcceptanceChance(
|
||||
actingFactionId,
|
||||
FactionId(invitationOption.targetFactionId),
|
||||
gameState
|
||||
)
|
||||
)
|
||||
}
|
||||
.maxByOption { _.acceptanceChance }
|
||||
.filter(_.acceptanceChance >= MinChanceForAIInvite.intValue)
|
||||
.map { iowc =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId =
|
||||
ProvinceId(diplomacyAvailableCommand.actingProvinceId),
|
||||
available = diplomacyAvailableCommand,
|
||||
selected = DiplomacySelectedCommand(
|
||||
selectedOption = iowc.invitationOption,
|
||||
targetFactionId = iowc.invitationOption.targetFactionId,
|
||||
sentHeroId = diplomacyAvailableCommand.recommendedHeroId
|
||||
),
|
||||
reason = "maybeInviteOtherFaction"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{MarchAvailableCommand, MarchCommandFromOneProvince}
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{
|
||||
MarchAvailableCommand,
|
||||
MarchCommandFromOneProvince
|
||||
}
|
||||
import net.eagle0.eagle.api.selected_command.MarchSelectedCommand
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.{CommandSelection, LegacyBattalionUtils, ProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.provinceGoldSurplus
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.provinceGoldSurplus
|
||||
import net.eagle0.eagle.library.util.{
|
||||
LegacyBattalionUtils,
|
||||
CommandSelection,
|
||||
ProvinceDistances
|
||||
}
|
||||
|
||||
object MarchTowardProvinceCommandChooser {
|
||||
def marchTowardProvinceCommand(
|
||||
@@ -20,81 +27,85 @@ object MarchTowardProvinceCommandChooser {
|
||||
): Option[CommandSelection] = {
|
||||
val bestCommand =
|
||||
opmc.availableDestinationProvinces
|
||||
.map(dest => (opmc, gs.provinces(dest.provinceId)))
|
||||
.flatMap {
|
||||
case (opmc, p) =>
|
||||
ProvinceDistances
|
||||
.distanceThroughFriendliesOption(
|
||||
destinationProvinceId,
|
||||
p.id,
|
||||
fid,
|
||||
gs
|
||||
)
|
||||
.map(distance => (opmc, p, distance))
|
||||
.map(dest => (opmc, gs.provinces(dest)))
|
||||
.flatMap { case (opmc, p) =>
|
||||
ProvinceDistances
|
||||
.distanceThroughFriendliesOption(
|
||||
destinationProvinceId,
|
||||
ProvinceId(p.id),
|
||||
fid,
|
||||
gs
|
||||
)
|
||||
.map(distance => (opmc, p, distance))
|
||||
}
|
||||
.minByOption(_._3)
|
||||
|
||||
val actingProvinceId = ac.actingProvinceId
|
||||
|
||||
bestCommand.flatMap {
|
||||
case (opmc, dest, _) =>
|
||||
internalRequire(
|
||||
opmc.originProvinceId != destinationProvinceId,
|
||||
s"Marching away from $destinationProvinceId in an effort to get to that province"
|
||||
bestCommand.flatMap { case (opmc, dest, _) =>
|
||||
internalRequire(
|
||||
ProvinceId(opmc.originProvinceId) != destinationProvinceId,
|
||||
s"Marching away from $destinationProvinceId in an effort to get to that province"
|
||||
)
|
||||
|
||||
val originProvince = gs.provinces(opmc.originProvinceId)
|
||||
|
||||
val takenHeroIds =
|
||||
AIClientUtils.takenHeroIdsForMarchTowardFocus(
|
||||
originProvince = originProvince,
|
||||
destinationProvince = dest,
|
||||
availableHeroIds = opmc.availableHeroIds.toVector.map(HeroId(_)),
|
||||
favorLeaders = favorLeaders,
|
||||
gs = gs
|
||||
)
|
||||
|
||||
val originProvince = gs.provinces(opmc.originProvinceId)
|
||||
Option.when(takenHeroIds.nonEmpty) {
|
||||
val takenBattalions =
|
||||
if originProvince.battalionIds.size <= takenHeroIds.size then
|
||||
originProvince.battalionIds.map(gs.battalions)
|
||||
else
|
||||
originProvince.battalionIds
|
||||
.map(gs.battalions)
|
||||
.sortBy(_.size)
|
||||
.take(takenHeroIds.size)
|
||||
|
||||
val takenHeroIds =
|
||||
AIClientUtils.takenHeroIdsForMarchTowardFocus(
|
||||
originProvince = originProvince,
|
||||
destinationProvince = dest,
|
||||
availableHeroIds = opmc.availableHeroIds.toVector,
|
||||
favorLeaders = favorLeaders,
|
||||
gs = gs
|
||||
val takenUnits = takenHeroIds
|
||||
.zipAll(
|
||||
takenBattalions.map(batt => Some(batt.id)),
|
||||
HeroId(0),
|
||||
None
|
||||
)
|
||||
|
||||
Option.when(takenHeroIds.nonEmpty) {
|
||||
val takenBattalions =
|
||||
if originProvince.battalionIds.size <= takenHeroIds.size then originProvince.battalionIds.map(gs.battalions)
|
||||
else
|
||||
originProvince.battalionIds
|
||||
.map(gs.battalions)
|
||||
.sortBy(_.size)
|
||||
.take(takenHeroIds.size)
|
||||
|
||||
val takenUnits = takenHeroIds
|
||||
.zipAll(
|
||||
takenBattalions.map(batt => Some(batt.id)),
|
||||
0,
|
||||
None
|
||||
.map { case (hid, bid) =>
|
||||
CombatUnit(
|
||||
factionId = fid.value,
|
||||
heroId = hid.value,
|
||||
battalionId = bid
|
||||
)
|
||||
.map {
|
||||
case (hid, bid) =>
|
||||
CombatUnit(factionId = fid, heroId = hid, battalionId = bid)
|
||||
}
|
||||
}
|
||||
|
||||
val foodToTake = 2 * LegacyBattalionUtils.monthlyConsumedFood(
|
||||
takenBattalions,
|
||||
gs.battalionTypes.toVector
|
||||
)
|
||||
val goldToTake = provinceGoldSurplus(originProvince.id, gs)
|
||||
CommandSelection(
|
||||
actingFactionId = fid,
|
||||
actingProvinceId = actingProvinceId,
|
||||
available = ac,
|
||||
selected = MarchSelectedCommand(
|
||||
marchingUnits = takenUnits, // Check the food cost here?
|
||||
destinationProvinceId = dest.id,
|
||||
originProvince = opmc.originProvinceId,
|
||||
gold = Math.min(goldToTake, opmc.goldAvailable),
|
||||
food = Math.min(foodToTake, opmc.foodAvailable)
|
||||
),
|
||||
reason =
|
||||
if favorLeaders then s"marching leaders toward ${gs.provinces(destinationProvinceId).name}"
|
||||
else s"marching heroes toward ${gs.provinces(destinationProvinceId).name}"
|
||||
)
|
||||
}
|
||||
val foodToTake = 2 * LegacyBattalionUtils.monthlyConsumedFood(
|
||||
takenBattalions,
|
||||
gs.battalionTypes.toVector
|
||||
)
|
||||
val goldToTake = provinceGoldSurplus(ProvinceId(originProvince.id), gs)
|
||||
CommandSelection(
|
||||
actingFactionId = fid,
|
||||
actingProvinceId = ProvinceId(actingProvinceId),
|
||||
available = ac,
|
||||
selected = MarchSelectedCommand(
|
||||
marchingUnits = takenUnits, // Check the food cost here?
|
||||
destinationProvinceId = dest.id,
|
||||
originProvince = opmc.originProvinceId,
|
||||
gold = Math.min(goldToTake, opmc.goldAvailable),
|
||||
food = Math.min(foodToTake, opmc.foodAvailable),
|
||||
unknownFields = scalapb.UnknownFieldSet.empty
|
||||
),
|
||||
reason = if favorLeaders then
|
||||
s"marching leaders toward ${gs.provinces(destinationProvinceId.value).name}"
|
||||
else
|
||||
s"marching heroes toward ${gs.provinces(destinationProvinceId.value).name}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, MoreOption, RandomState}
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.ai.AIClientUtils.extraHeroCount
|
||||
import net.eagle0.eagle.api.available_command.*
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.{MarchSelectedCommand, ReconSelectedCommand}
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.common.improvement_type.ImprovementType
|
||||
import net.eagle0.eagle.views.province_view.FullProvinceInfo
|
||||
import net.eagle0.eagle.api.selected_command.{
|
||||
MarchSelectedCommand,
|
||||
ReconSelectedCommand
|
||||
}
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.{MinSupportForTaxes, MonthsReconConsideredRecent}
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
MinSupportForTaxes,
|
||||
MonthsReconConsideredRecent
|
||||
}
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
AlmsCommandSelector,
|
||||
AttackCommandChooser,
|
||||
@@ -25,15 +36,10 @@ import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
OrganizeCommandSelector,
|
||||
ProvinceGoldSurplusCalculator
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
import net.eagle0.eagle.views.province_view.FullProvinceInfo
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
object MidGameAIClient {
|
||||
private def validateNoFactionLeaderAlone(
|
||||
@@ -135,20 +141,20 @@ object MidGameAIClient {
|
||||
_ /* availableHeroIds */,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
val actingProvince = gameState.provinces(actingProvinceId)
|
||||
val actingProvince = gameState.provinces(actingProvinceId)
|
||||
val foodMonthsToHoldBack =
|
||||
FoodConsumptionUtils.foodConsumptionMonthsToHold(
|
||||
actingProvinceId,
|
||||
gameState
|
||||
)
|
||||
val foodToHoldBack =
|
||||
val foodToHoldBack =
|
||||
foodMonthsToHoldBack * LegacyProvinceUtils.monthlyFoodConsumption(
|
||||
actingProvinceId,
|
||||
gameState
|
||||
)
|
||||
val maxFoodToSend =
|
||||
val maxFoodToSend =
|
||||
Math.min(actingProvince.food - foodToHoldBack, foodAvailable)
|
||||
val maxGoldToSend = Math.min(
|
||||
val maxGoldToSend = Math.min(
|
||||
ProvinceGoldSurplusCalculator
|
||||
.provinceGoldSurplus(actingProvinceId, gameState),
|
||||
goldAvailable
|
||||
@@ -166,12 +172,12 @@ object MidGameAIClient {
|
||||
.map(gameState.provinces)
|
||||
.filter(_.rulingFactionId.contains(factionId))
|
||||
.map { province =>
|
||||
val consumption = LegacyProvinceUtils
|
||||
val consumption = LegacyProvinceUtils
|
||||
.monthlyFoodConsumption(province.id, gameState)
|
||||
val incomingFood =
|
||||
val incomingFood =
|
||||
province.incomingShipments.flatMap(_.supplies).map(_.food).sum
|
||||
val availableFood = province.food + incomingFood
|
||||
val monthsOfFood =
|
||||
val monthsOfFood =
|
||||
if consumption == 0 then 120
|
||||
else availableFood / consumption
|
||||
DestinationFoodStatus(
|
||||
@@ -183,28 +189,29 @@ object MidGameAIClient {
|
||||
.filter(_.monthsOfFood < 4)
|
||||
.minByOption(_.monthsOfFood)
|
||||
|
||||
bestDestination.map { destinationFoodStatus =>
|
||||
val destinationProvince = destinationFoodStatus.province
|
||||
val desiredFood = LegacyProvinceUtils.monthlyFoodConsumption(
|
||||
destinationProvince.id,
|
||||
gameState
|
||||
) * 6 - destinationProvince.food
|
||||
val foodToSend =
|
||||
if desiredFood > maxFoodToSend then maxFoodToSend
|
||||
else (desiredFood + maxFoodToSend) / 2
|
||||
bestDestination
|
||||
.map { destinationFoodStatus =>
|
||||
val destinationProvince = destinationFoodStatus.province
|
||||
val desiredFood = LegacyProvinceUtils.monthlyFoodConsumption(
|
||||
destinationProvince.id,
|
||||
gameState
|
||||
) * 6 - destinationProvince.food
|
||||
val foodToSend =
|
||||
if desiredFood > maxFoodToSend then maxFoodToSend
|
||||
else (desiredFood + maxFoodToSend) / 2
|
||||
|
||||
CommandChoiceHelpers
|
||||
.chosenSendSuppliesCommandWithSuppliesAndDestination(
|
||||
actingFactionId = factionId,
|
||||
gameState = gameState,
|
||||
ac = available,
|
||||
supplies = Supplies(
|
||||
gold = maxGoldToSend / 2,
|
||||
food = foodToSend
|
||||
),
|
||||
destination = destinationFoodStatus.province.id
|
||||
)
|
||||
}
|
||||
CommandChoiceHelpers
|
||||
.chosenSendSuppliesCommandWithSuppliesAndDestination(
|
||||
actingFactionId = factionId,
|
||||
gameState = gameState,
|
||||
ac = available,
|
||||
supplies = Supplies(
|
||||
gold = maxGoldToSend / 2,
|
||||
food = foodToSend
|
||||
),
|
||||
destination = destinationFoodStatus.province.id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.map(_.withReason("relay supplies"))
|
||||
@@ -328,70 +335,71 @@ object MidGameAIClient {
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
provincesWithFactionLeader(actingFactionId, gameState).map { province =>
|
||||
MoreOption
|
||||
.flatWhen(province.rulingFactionHeroIds.size < 4)(
|
||||
maybeMoveHeroesTowardFactionLeaderCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gameState,
|
||||
acs = availableCommands
|
||||
)
|
||||
) match {
|
||||
case Some(cs) => RandomState(Some(cs), functionalRandom)
|
||||
case None =>
|
||||
chosenGenericCommandFromRankedOptions(
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gameState,
|
||||
acs = availableCommands,
|
||||
choosersAndReasons = Vector(
|
||||
(
|
||||
ExpandCommandSelector.maybeChosenExpandCommand,
|
||||
"no hostile neighbors, expand"
|
||||
),
|
||||
(
|
||||
maybeMoveToRecruitCommand,
|
||||
"no hostile neighbors, travel to recruit"
|
||||
),
|
||||
(
|
||||
provincesWithFactionLeader(actingFactionId, gameState)
|
||||
.map { province =>
|
||||
MoreOption
|
||||
.flatWhen(province.rulingFactionHeroIds.size < 4)(
|
||||
maybeMoveHeroesTowardFactionLeaderCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gameState,
|
||||
acs = availableCommands
|
||||
)
|
||||
) match {
|
||||
case Some(cs) => RandomState(Some(cs), functionalRandom)
|
||||
case None =>
|
||||
chosenGenericCommandFromRankedOptions(
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gameState,
|
||||
acs = availableCommands,
|
||||
choosersAndReasons = Vector(
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
"chosen neutral neighbors: Improve b/c nothing better"
|
||||
),
|
||||
(
|
||||
ExpandCommandSelector.maybeChosenExpandCommand,
|
||||
"no hostile neighbors, expand"
|
||||
),
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
chosenRestCommand(
|
||||
maybeMoveToRecruitCommand,
|
||||
"no hostile neighbors, travel to recruit"
|
||||
),
|
||||
(
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
"chosen neutral neighbors: rest"
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
"chosen neutral neighbors: rest"
|
||||
)
|
||||
),
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
"chosen neutral neighbors: Improve b/c nothing better"
|
||||
),
|
||||
(
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
chosenRestCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
"chosen neutral neighbors: rest"
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
"chosen neutral neighbors: rest"
|
||||
)
|
||||
),
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.find(_.newValue.isDefined)
|
||||
.get
|
||||
|
||||
@@ -402,84 +410,85 @@ object MidGameAIClient {
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.DeterministicCommandChooser
|
||||
provincesWithFactionLeader(actingFactionId, gameState).map { province =>
|
||||
MoreOption
|
||||
.flatWhen(province.rulingFactionHeroIds.size < 4)(
|
||||
maybeMoveHeroesTowardFactionLeaderCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gameState,
|
||||
acs = availableCommands
|
||||
)
|
||||
) match {
|
||||
case Some(cs) => RandomState(Some(cs), functionalRandom)
|
||||
case None =>
|
||||
chosenGenericCommandFromRankedOptions(
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gameState,
|
||||
acs = availableCommands,
|
||||
choosersAndReasons = Vector(
|
||||
(
|
||||
maybeMoveToRecruitCommand,
|
||||
"chosen no neutral neighbors, travel to recruit"
|
||||
),
|
||||
(
|
||||
provincesWithFactionLeader(actingFactionId, gameState)
|
||||
.map { province =>
|
||||
MoreOption
|
||||
.flatWhen(province.rulingFactionHeroIds.size < 4)(
|
||||
maybeMoveHeroesTowardFactionLeaderCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gameState,
|
||||
acs = availableCommands
|
||||
)
|
||||
) match {
|
||||
case Some(cs) => RandomState(Some(cs), functionalRandom)
|
||||
case None =>
|
||||
chosenGenericCommandFromRankedOptions(
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gameState,
|
||||
acs = availableCommands,
|
||||
choosersAndReasons = Vector(
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
MoveLeaderToBetterProvinceCommandChooser
|
||||
.maybeMoveLeaderToBetterProvinceCommand(
|
||||
maybeMoveToRecruitCommand,
|
||||
"chosen no neutral neighbors, travel to recruit"
|
||||
),
|
||||
(
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
MoveLeaderToBetterProvinceCommandChooser
|
||||
.maybeMoveLeaderToBetterProvinceCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
"chosen no neutral neighbors, travel to front"
|
||||
),
|
||||
(
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
"chosen no neutral neighbors, travel to front"
|
||||
),
|
||||
(
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands
|
||||
functionalRandom
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
"chosen no neutral neighbors: Improve b/c nothing better"
|
||||
),
|
||||
(
|
||||
"chosen no neutral neighbors: Improve b/c nothing better"
|
||||
),
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
chosenRestCommand(
|
||||
(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
"chosen no neutral neighbors: rest"
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
chosenRestCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands,
|
||||
"chosen no neutral neighbors: rest"
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
"chosen neutral neighbors: rest"
|
||||
)
|
||||
),
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
"chosen neutral neighbors: rest"
|
||||
)
|
||||
),
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.find(_.newValue.isDefined)
|
||||
.get
|
||||
}
|
||||
@@ -494,11 +503,11 @@ object MidGameAIClient {
|
||||
.reconnedProvinces
|
||||
.find(_.id == provinceId)
|
||||
.flatMap(_.fullInfo)
|
||||
def reconnedHeroCountFor(provinceId: ProvinceId): Int =
|
||||
def reconnedHeroCountFor(provinceId: ProvinceId): Int =
|
||||
reconnedFullInfoFor(provinceId)
|
||||
.map(_.rulingFactionHeroIds.size)
|
||||
.getOrElse(0)
|
||||
def reconnedBattalionsFor(provinceId: ProvinceId): Vector[BattalionView] =
|
||||
def reconnedBattalionsFor(provinceId: ProvinceId): Vector[BattalionView] =
|
||||
reconnedFullInfoFor(provinceId)
|
||||
.map(_.battalions.toVector)
|
||||
.getOrElse(Vector())
|
||||
@@ -664,7 +673,8 @@ object MidGameAIClient {
|
||||
}
|
||||
}
|
||||
),
|
||||
(fid, gs, acs, fr) => chosenNoNeutralNeighborsCommand(fid, gs, acs, fr)
|
||||
(fid, gs, acs, fr) =>
|
||||
chosenNoNeutralNeighborsCommand(fid, gs, acs, fr)
|
||||
),
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
@@ -686,7 +696,7 @@ object MidGameAIClient {
|
||||
factionId: FactionId
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[ImproveAvailableCommand](acs) { availableCommand =>
|
||||
val province = gs.provinces(availableCommand.actingProvinceId)
|
||||
val province = gs.provinces(availableCommand.actingProvinceId)
|
||||
val existingBattalions = province.battalionIds
|
||||
.map(gs.battalions)
|
||||
|
||||
@@ -715,7 +725,7 @@ object MidGameAIClient {
|
||||
.map(_.minimumAgriculture - province.agriculture)
|
||||
.filter(_ > 0)
|
||||
.min
|
||||
val neededEconomy = notAvailable
|
||||
val neededEconomy = notAvailable
|
||||
.map(_.minimumEconomy - province.economy)
|
||||
.filter(_ > 0)
|
||||
.min
|
||||
@@ -860,7 +870,7 @@ object MidGameAIClient {
|
||||
case (None, fr2) => go(RandomState(tail, fr2))
|
||||
}
|
||||
}
|
||||
case (_, fr) => RandomState(None, fr)
|
||||
case (_, fr) => RandomState(None, fr)
|
||||
}
|
||||
|
||||
go(
|
||||
@@ -879,34 +889,35 @@ object MidGameAIClient {
|
||||
val factionLeaderProvincesNeedingHeroes =
|
||||
provincesWithFactionLeader(actingFactionId, gs)
|
||||
.filter(p => p.rulingFactionHeroIds.size < p.heroCap)
|
||||
val cs = for {
|
||||
val cs = for {
|
||||
(marchCommand, idx) <- acs.zipWithIndex.collectFirst {
|
||||
case (ac: MarchAvailableCommand, idx) => (ac, idx)
|
||||
}
|
||||
case (ac: MarchAvailableCommand, idx) => (ac, idx)
|
||||
}
|
||||
} yield {
|
||||
// Find the march command origin provinces that contain extra heroes
|
||||
val candidateCommands = marchCommand.oneProvinceCommands.filter { opmc =>
|
||||
extraHeroCount(opmc.originProvinceId, actingFactionId, gs) > 0
|
||||
}
|
||||
|
||||
val furthestOrigin = candidateCommands.flatMap { opmc =>
|
||||
// distance to the faction leader province we're closest to
|
||||
factionLeaderProvincesNeedingHeroes.flatMap { flp =>
|
||||
ProvinceDistances
|
||||
.distanceThroughFriendliesOption(
|
||||
p1 = flp.id,
|
||||
p2 = opmc.originProvinceId,
|
||||
fid = actingFactionId,
|
||||
gs = gs
|
||||
)
|
||||
.map {
|
||||
(_: Int, flp.id: ProvinceId)
|
||||
val furthestOrigin = candidateCommands
|
||||
.flatMap { opmc =>
|
||||
// distance to the faction leader province we're closest to
|
||||
factionLeaderProvincesNeedingHeroes
|
||||
.flatMap { flp =>
|
||||
ProvinceDistances
|
||||
.distanceThroughFriendliesOption(
|
||||
p1 = flp.id,
|
||||
p2 = opmc.originProvinceId,
|
||||
fid = actingFactionId,
|
||||
gs = gs
|
||||
)
|
||||
.map {
|
||||
(_: Int, flp.id: ProvinceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
.minByOption(_._1)
|
||||
.filterNot(_._1 == 0) // If distance is zero, we'd be moving away from the province we're trying to get to
|
||||
.map {
|
||||
case (distance: Int, destinationProvinceId: ProvinceId) =>
|
||||
.minByOption(_._1)
|
||||
.filterNot(_._1 == 0) // If distance is zero, we'd be moving away from the province we're trying to get to
|
||||
.map { case (distance: Int, destinationProvinceId: ProvinceId) =>
|
||||
(
|
||||
// distance to the faction leader province we're closest to
|
||||
distance,
|
||||
@@ -914,22 +925,21 @@ object MidGameAIClient {
|
||||
extraHeroCount(opmc.originProvinceId, actingFactionId, gs),
|
||||
opmc
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.maxByOption(tup => (tup._1, tup._3))
|
||||
.map(tup => (tup._2, tup._4))
|
||||
.map { tup => (tup._2, tup._4) }
|
||||
|
||||
furthestOrigin.flatMap {
|
||||
case (destinationProvinceId, opmc) =>
|
||||
MarchTowardProvinceCommandChooser
|
||||
.marchTowardProvinceCommand(
|
||||
ac = marchCommand,
|
||||
fid = actingFactionId,
|
||||
opmc = opmc,
|
||||
destinationProvinceId = destinationProvinceId,
|
||||
gs = gs,
|
||||
favorLeaders = false
|
||||
)
|
||||
furthestOrigin.flatMap { case (destinationProvinceId, opmc) =>
|
||||
MarchTowardProvinceCommandChooser
|
||||
.marchTowardProvinceCommand(
|
||||
ac = marchCommand,
|
||||
fid = actingFactionId,
|
||||
opmc = opmc,
|
||||
destinationProvinceId = destinationProvinceId,
|
||||
gs = gs,
|
||||
favorLeaders = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -947,7 +957,8 @@ object MidGameAIClient {
|
||||
actingProvinceId = ac.actingProvinceId,
|
||||
available = ac,
|
||||
selected = ReconSelectedCommand(
|
||||
actingHeroId = ac.availableHeroIds.maxBy(hid => gameState.heroes(hid).vigor),
|
||||
actingHeroId =
|
||||
ac.availableHeroIds.maxBy(hid => gameState.heroes(hid).vigor),
|
||||
targetProvinceId = targetProvinceId
|
||||
),
|
||||
reason = "selectedReconCommand"
|
||||
@@ -1035,11 +1046,11 @@ object MidGameAIClient {
|
||||
gameState: GameState,
|
||||
acs: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] = {
|
||||
val faction = gameState.factions(actingFactionId)
|
||||
val faction = gameState.factions(actingFactionId)
|
||||
val factionLeaders = faction.leaders
|
||||
flatSelectionForType[MarchAvailableCommand](acs) { marchAvailableCommand =>
|
||||
marchAvailableCommand.oneProvinceCommands.flatMap { opmc =>
|
||||
val originProvince = gameState.provinces(opmc.originProvinceId)
|
||||
val originProvince = gameState.provinces(opmc.originProvinceId)
|
||||
val leadersInOriginProvince =
|
||||
originProvince.rulingFactionHeroIds.intersect(factionLeaders)
|
||||
MoreOption.flatWhen(
|
||||
@@ -1047,12 +1058,12 @@ object MidGameAIClient {
|
||||
.intersect(factionLeaders)
|
||||
.nonEmpty
|
||||
) {
|
||||
val leaderIdToMove =
|
||||
val leaderIdToMove =
|
||||
opmc.availableHeroIds
|
||||
.intersect(factionLeaders)
|
||||
.maxBy(hid => LegacyHeroUtils.seniorityOrder(faction, hid))
|
||||
val validDestinations = opmc.availableDestinationProvinces
|
||||
.map(dest => gameState.provinces(dest.provinceId))
|
||||
.map(gameState.provinces)
|
||||
.filter(
|
||||
_.rulingFactionId.contains(actingFactionId)
|
||||
) // only send to a province we own
|
||||
@@ -1063,26 +1074,27 @@ object MidGameAIClient {
|
||||
IncomingArmyUtils.isUnderAttack(province, gameState)
|
||||
) // don't send a leader into a province under attack
|
||||
|
||||
validDestinations.minByOption(_.rulingFactionHeroIds.size).map { destinationProvince =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = marchAvailableCommand.actingProvinceId,
|
||||
available = marchAvailableCommand,
|
||||
selected = MarchSelectedCommand(
|
||||
gold = 0,
|
||||
food = 0,
|
||||
originProvince = originProvince.id,
|
||||
destinationProvinceId = destinationProvince.id,
|
||||
marchingUnits = Vector(
|
||||
CombatUnit(
|
||||
factionId = actingFactionId,
|
||||
heroId = leaderIdToMove,
|
||||
battalionId = None
|
||||
validDestinations.minByOption(_.rulingFactionHeroIds.size).map {
|
||||
destinationProvince =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = marchAvailableCommand.actingProvinceId,
|
||||
available = marchAvailableCommand,
|
||||
selected = MarchSelectedCommand(
|
||||
gold = 0,
|
||||
food = 0,
|
||||
originProvince = originProvince.id,
|
||||
destinationProvinceId = destinationProvince.id,
|
||||
marchingUnits = Vector(
|
||||
CombatUnit(
|
||||
factionId = actingFactionId,
|
||||
heroId = leaderIdToMove,
|
||||
battalionId = None
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
reason = "Spreading out the faction leaders"
|
||||
)
|
||||
),
|
||||
reason = "Spreading out the faction leaders"
|
||||
)
|
||||
}
|
||||
}
|
||||
}.headOption
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, MarchAvailableCommand, MarchCommandFromOneProvince}
|
||||
import net.eagle0.eagle.api.available_command.{
|
||||
AvailableCommand,
|
||||
MarchAvailableCommand,
|
||||
MarchCommandFromOneProvince
|
||||
}
|
||||
import net.eagle0.eagle.internal.faction.Faction
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.library.util.ProvinceDistances
|
||||
import net.eagle0.eagle.library.util.ProvinceDistances.ProvinceAndDistance
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
object MoveLeaderToBetterProvinceCommandChooser {
|
||||
private case class MCFOPWithDestinationAndDistance(
|
||||
@@ -26,7 +30,8 @@ object MoveLeaderToBetterProvinceCommandChooser {
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gs,
|
||||
acs = acs,
|
||||
betterDestinationChooser = FactionLeaderProvinceRanker.bestSettlementPidForFactionLeader
|
||||
betterDestinationChooser =
|
||||
FactionLeaderProvinceRanker.bestSettlementPidForFactionLeader
|
||||
)
|
||||
|
||||
private def candidatesForMarchCommand(
|
||||
@@ -52,21 +57,22 @@ object MoveLeaderToBetterProvinceCommandChooser {
|
||||
actingFactionId,
|
||||
mcfop.originProvinceId,
|
||||
gs
|
||||
).flatMap { desiredPid =>
|
||||
ProvinceDistances
|
||||
.closestProvinceThroughFriendliesOption(
|
||||
desiredPid,
|
||||
mcfop.availableDestinationProvinces.map(_.provinceId).toVector,
|
||||
actingFactionId,
|
||||
gs
|
||||
)
|
||||
.map { provinceAndDistance =>
|
||||
MCFOPWithDestinationAndDistance(
|
||||
mcfop = mcfop,
|
||||
provinceAndDistance = provinceAndDistance
|
||||
)
|
||||
.flatMap { desiredPid =>
|
||||
ProvinceDistances
|
||||
.closestProvinceThroughFriendliesOption(
|
||||
desiredPid,
|
||||
mcfop.availableDestinationProvinces.toVector,
|
||||
actingFactionId,
|
||||
gs
|
||||
)
|
||||
}
|
||||
}
|
||||
.map { provinceAndDistance =>
|
||||
MCFOPWithDestinationAndDistance(
|
||||
mcfop = mcfop,
|
||||
provinceAndDistance = provinceAndDistance
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def maybeMoveLeaderToBetterProvinceCommandWithChooser(
|
||||
@@ -76,14 +82,15 @@ object MoveLeaderToBetterProvinceCommandChooser {
|
||||
betterDestinationChooser: DestinationChooser
|
||||
): Option[CommandSelection] = {
|
||||
val cs = for {
|
||||
faction <- gs.factions.get(actingFactionId)
|
||||
faction <- gs.factions.get(actingFactionId)
|
||||
marchCommand <- acs.collectFirst { case ac: MarchAvailableCommand => ac }
|
||||
} yield {
|
||||
// Find the march command origin provinces that contain a faction leader
|
||||
val candidatesWithDistances = candidateCommandsWithDistances(
|
||||
actingFactionId = actingFactionId,
|
||||
gs = gs,
|
||||
candidateCommands = candidatesForMarchCommand(marchCommand, gs, faction),
|
||||
candidateCommands =
|
||||
candidatesForMarchCommand(marchCommand, gs, faction),
|
||||
betterDestinationChooser = betterDestinationChooser
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{
|
||||
AvailableCommand,
|
||||
ResolveAllianceOfferAvailableCommand,
|
||||
@@ -25,25 +26,24 @@ import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus.{
|
||||
}
|
||||
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship.RelationshipLevel.ALLY
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
AiAllianceAcceptanceBaseChance,
|
||||
AiTruceAcceptanceBaseChance,
|
||||
MinimumPrestigeDifferenceToInvite
|
||||
}
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.{
|
||||
randomSelectionForType,
|
||||
selectionForType
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
CommandChooser,
|
||||
CommandChooserImplicits,
|
||||
RansomOfferHelpers
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.{
|
||||
randomSelectionForType,
|
||||
selectionForType
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object ResolveDiplomacyCommandSelector {
|
||||
import CommandChooserImplicits.*
|
||||
@@ -59,10 +59,10 @@ object ResolveDiplomacyCommandSelector {
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
resolveInvitationSelectedCommand _,
|
||||
resolveAllianceOfferSelectedCommand _,
|
||||
resolveBreakAllianceSelectedCommand _,
|
||||
resolveTruceOfferSelectedCommand _,
|
||||
resolveInvitationSelectedCommand,
|
||||
resolveAllianceOfferSelectedCommand,
|
||||
resolveBreakAllianceSelectedCommand,
|
||||
resolveTruceOfferSelectedCommand,
|
||||
resolveRansomOfferSelectedCommand _
|
||||
),
|
||||
functionalRandom = functionalRandom
|
||||
@@ -77,24 +77,23 @@ object ResolveDiplomacyCommandSelector {
|
||||
randomSelectionForType[ResolveInvitationAvailableCommand](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
case (resolveInvitationAc, fr) =>
|
||||
selectionForResolveInvitationCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveInvitationAc,
|
||||
gs = gameState,
|
||||
functionalRandom = fr
|
||||
).map { resolveInviteSelection =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = 0,
|
||||
available = resolveInvitationAc,
|
||||
selected = resolveInviteSelection,
|
||||
reason = "resolving invitation"
|
||||
)
|
||||
) { case (resolveInvitationAc, fr) =>
|
||||
selectionForResolveInvitationCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveInvitationAc,
|
||||
gs = gameState,
|
||||
functionalRandom = fr
|
||||
).map { resolveInviteSelection =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = ProvinceId(0),
|
||||
available = resolveInvitationAc,
|
||||
selected = resolveInviteSelection,
|
||||
reason = "resolving invitation"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def selectionForResolveInvitationCommand(
|
||||
@@ -103,9 +102,15 @@ object ResolveDiplomacyCommandSelector {
|
||||
gs: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[SelectedCommand] = functionalRandom.nextDouble.map { roll =>
|
||||
val invitedFid = actingFactionId
|
||||
val invitedFid = actingFactionId
|
||||
val bestInvitation = ac.invitations
|
||||
.maxBy(inv => invitationAcceptanceChance(inv.originatingFactionId, invitedFid, gs))
|
||||
.maxBy(inv =>
|
||||
invitationAcceptanceChance(
|
||||
FactionId(inv.originatingFactionId),
|
||||
invitedFid,
|
||||
gs
|
||||
)
|
||||
)
|
||||
|
||||
internalRequire(
|
||||
bestInvitation.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_ACCEPTED),
|
||||
@@ -122,7 +127,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
originatingFactionId = bestOriginatingFid,
|
||||
resolution =
|
||||
if 100.0 * roll < invitationAcceptanceChance(
|
||||
bestOriginatingFid,
|
||||
FactionId(bestOriginatingFid),
|
||||
invitedFid,
|
||||
gs
|
||||
)
|
||||
@@ -149,24 +154,23 @@ object ResolveDiplomacyCommandSelector {
|
||||
randomSelectionForType[ResolveTruceOfferAvailableCommand](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
case (resolveTruceAc, fr) =>
|
||||
selectionForResolveTruceOfferSelectedCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveTruceAc,
|
||||
gs = gameState,
|
||||
functionalRandom = fr
|
||||
).map { resolveTruceOfferSelection =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = 0,
|
||||
available = resolveTruceAc,
|
||||
selected = resolveTruceOfferSelection,
|
||||
reason = "resolving truce offer"
|
||||
)
|
||||
) { case (resolveTruceAc, fr) =>
|
||||
selectionForResolveTruceOfferSelectedCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveTruceAc,
|
||||
gs = gameState,
|
||||
functionalRandom = fr
|
||||
).map { resolveTruceOfferSelection =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = ProvinceId(0),
|
||||
available = resolveTruceAc,
|
||||
selected = resolveTruceOfferSelection,
|
||||
reason = "resolving truce offer"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def selectionForResolveTruceOfferSelectedCommand(
|
||||
@@ -177,7 +181,13 @@ object ResolveDiplomacyCommandSelector {
|
||||
): RandomState[SelectedCommand] = functionalRandom.nextDouble.map { roll =>
|
||||
val actingFid = actingFactionId
|
||||
val bestOffer = ac.offers
|
||||
.maxBy(inv => truceOfferAcceptanceChance(inv.originatingFactionId, actingFid, gs))
|
||||
.maxBy(inv =>
|
||||
truceOfferAcceptanceChance(
|
||||
FactionId(inv.originatingFactionId),
|
||||
actingFid,
|
||||
gs
|
||||
)
|
||||
)
|
||||
|
||||
internalRequire(
|
||||
bestOffer.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_ACCEPTED),
|
||||
@@ -194,7 +204,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
originatingFactionId = bestOriginatingFid,
|
||||
resolution =
|
||||
if 100.0 * roll < truceOfferAcceptanceChance(
|
||||
originatingFid = bestOriginatingFid,
|
||||
originatingFid = FactionId(bestOriginatingFid),
|
||||
targetFid = actingFid,
|
||||
gs = gs
|
||||
)
|
||||
@@ -223,20 +233,21 @@ object ResolveDiplomacyCommandSelector {
|
||||
originatingFid: FactionId,
|
||||
targetFid: FactionId,
|
||||
gs: GameState
|
||||
): Int =
|
||||
): Int = {
|
||||
// Always reject if the faction already has an alliance
|
||||
if gs
|
||||
.factions(targetFid)
|
||||
.factions(targetFid.value)
|
||||
.factionRelationships
|
||||
.exists(_.relationshipLevel == ALLY)
|
||||
then 0
|
||||
|
||||
// reject if this is our only neighbor and we have no expansion possibilities
|
||||
else if LegacyFactionUtils.provinces(targetFid, gs).forall { province =>
|
||||
province.neighbors.map(n => gs.provinces(n.provinceId)).forall { neighborProvince =>
|
||||
neighborProvince.rulingFactionId.contains(
|
||||
originatingFid
|
||||
) || neighborProvince.rulingFactionId.contains(targetFid)
|
||||
province.neighbors.map { n => gs.provinces(n.provinceId) }.forall {
|
||||
neighborProvince =>
|
||||
neighborProvince.rulingFactionId.contains(
|
||||
originatingFid
|
||||
) || neighborProvince.rulingFactionId.contains(targetFid)
|
||||
}
|
||||
}
|
||||
then 0
|
||||
@@ -251,24 +262,26 @@ object ResolveDiplomacyCommandSelector {
|
||||
gameState = gs
|
||||
)
|
||||
).floor.toInt.max(0)
|
||||
}
|
||||
|
||||
private def resolveRansomOfferSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
selectionForType[ResolveRansomOfferAvailableCommand](availableCommands) { resolveRansomAc =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = 0,
|
||||
available = resolveRansomAc,
|
||||
selected = selectionForResolveRansomOfferSelectedCommand(
|
||||
selectionForType[ResolveRansomOfferAvailableCommand](availableCommands) {
|
||||
resolveRansomAc =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveRansomAc,
|
||||
gs = gameState
|
||||
),
|
||||
reason = "resolving ransom offer"
|
||||
)
|
||||
actingProvinceId = ProvinceId(0),
|
||||
available = resolveRansomAc,
|
||||
selected = selectionForResolveRansomOfferSelectedCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveRansomAc,
|
||||
gs = gameState
|
||||
),
|
||||
reason = "resolving ransom offer"
|
||||
)
|
||||
}
|
||||
|
||||
private def selectionForResolveRansomOfferSelectedCommand(
|
||||
@@ -283,7 +296,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
ransomOffer = Some(ac.offers.head),
|
||||
resolution = RansomOfferHelpers.chosenResolution(rod)
|
||||
)
|
||||
case _ => throw new EagleInternalException("Not a ransom offer")
|
||||
case _ => throw new EagleInternalException("Not a ransom offer")
|
||||
}
|
||||
|
||||
private def resolveAllianceOfferSelectedCommand(
|
||||
@@ -295,24 +308,23 @@ object ResolveDiplomacyCommandSelector {
|
||||
randomSelectionForType[ResolveAllianceOfferAvailableCommand](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
case (resolveAllianceAc, fr) =>
|
||||
selectionForResolveAllianceOfferSelectedCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveAllianceAc,
|
||||
gs = gameState,
|
||||
functionalRandom = fr
|
||||
).map { resolveAllianceOfferSelection =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = 0,
|
||||
available = resolveAllianceAc,
|
||||
selected = resolveAllianceOfferSelection,
|
||||
reason = "resolving alliance offer"
|
||||
)
|
||||
) { case (resolveAllianceAc, fr) =>
|
||||
selectionForResolveAllianceOfferSelectedCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveAllianceAc,
|
||||
gs = gameState,
|
||||
functionalRandom = fr
|
||||
).map { resolveAllianceOfferSelection =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = ProvinceId(0),
|
||||
available = resolveAllianceAc,
|
||||
selected = resolveAllianceOfferSelection,
|
||||
reason = "resolving alliance offer"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def resolveBreakAllianceSelectedCommand(
|
||||
@@ -324,24 +336,23 @@ object ResolveDiplomacyCommandSelector {
|
||||
randomSelectionForType[ResolveBreakAllianceAvailableCommand](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
case (resolveBreakAllianceAc, fr) =>
|
||||
selectionForResolveBreakAllianceSelectedCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveBreakAllianceAc,
|
||||
gs = gameState,
|
||||
functionalRandom = fr
|
||||
).map { resolveBreakAllianceSelection =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = 0,
|
||||
available = resolveBreakAllianceAc,
|
||||
selected = resolveBreakAllianceSelection,
|
||||
reason = "resolving break alliance"
|
||||
)
|
||||
) { case (resolveBreakAllianceAc, fr) =>
|
||||
selectionForResolveBreakAllianceSelectedCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
ac = resolveBreakAllianceAc,
|
||||
gs = gameState,
|
||||
functionalRandom = fr
|
||||
).map { resolveBreakAllianceSelection =>
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = ProvinceId(0),
|
||||
available = resolveBreakAllianceAc,
|
||||
selected = resolveBreakAllianceSelection,
|
||||
reason = "resolving break alliance"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def selectionForResolveAllianceOfferSelectedCommand(
|
||||
@@ -352,7 +363,13 @@ object ResolveDiplomacyCommandSelector {
|
||||
): RandomState[SelectedCommand] = functionalRandom.nextDouble.map { roll =>
|
||||
val actingFid = actingFactionId
|
||||
val bestOffer = ac.offers
|
||||
.maxBy(inv => allianceOfferAcceptanceChance(inv.originatingFactionId, actingFid, gs))
|
||||
.maxBy(inv =>
|
||||
allianceOfferAcceptanceChance(
|
||||
FactionId(inv.originatingFactionId),
|
||||
actingFid,
|
||||
gs
|
||||
)
|
||||
)
|
||||
|
||||
internalRequire(
|
||||
bestOffer.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_ACCEPTED),
|
||||
@@ -369,7 +386,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
originatingFactionId = bestOriginatingFid,
|
||||
resolution =
|
||||
if 100.0 * roll < allianceOfferAcceptanceChance(
|
||||
originatingFid = bestOriginatingFid,
|
||||
originatingFid = FactionId(bestOriginatingFid),
|
||||
targetFid = actingFid,
|
||||
gs = gs
|
||||
)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.common.MoreOption
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, MarchAvailableCommand}
|
||||
import net.eagle0.eagle.api.available_command.{
|
||||
AvailableCommand,
|
||||
MarchAvailableCommand
|
||||
}
|
||||
import net.eagle0.eagle.api.selected_command.MarchSelectedCommand
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.{LoyaltyGainFromFeast, MinimumLoyaltyForSwearBrotherhood}
|
||||
import net.eagle0.eagle.library.util.{CommandSelection, ProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
CommandChoiceHelpers,
|
||||
@@ -15,9 +17,13 @@ import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
SwearBrotherhoodCommandSelector,
|
||||
SwornBrotherChooser
|
||||
}
|
||||
import net.eagle0.eagle.FactionId
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
LoyaltyGainFromFeast,
|
||||
MinimumLoyaltyForSwearBrotherhood
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object SeekMoreLeadersCommandChooser {
|
||||
private def needsMoreLeaders(
|
||||
@@ -37,22 +43,21 @@ object SeekMoreLeadersCommandChooser {
|
||||
factionHeadProvince: Province,
|
||||
gameState: GameState
|
||||
): Option[CommandSelection] = {
|
||||
val opmc = command.oneProvinceCommands
|
||||
val opmc = command.oneProvinceCommands
|
||||
.find(_.originProvinceId == startingProvince.id)
|
||||
.get
|
||||
val bestDestination =
|
||||
opmc.availableDestinationProvinces
|
||||
.map(dest => (opmc, gameState.provinces(dest.provinceId)))
|
||||
.flatMap {
|
||||
case (opmc, p) =>
|
||||
ProvinceDistances
|
||||
.distanceThroughFriendliesOption(
|
||||
factionHeadProvince.id,
|
||||
p.id,
|
||||
factionHeadProvince.rulingFactionId.get,
|
||||
gameState
|
||||
)
|
||||
.map(distance => (opmc, p, distance))
|
||||
.map(dest => (opmc, gameState.provinces(dest)))
|
||||
.flatMap { case (opmc, p) =>
|
||||
ProvinceDistances
|
||||
.distanceThroughFriendliesOption(
|
||||
factionHeadProvince.id,
|
||||
p.id,
|
||||
factionHeadProvince.rulingFactionId.get,
|
||||
gameState
|
||||
)
|
||||
.map(distance => (opmc, p, distance))
|
||||
}
|
||||
.minByOption(_._3)
|
||||
|
||||
@@ -83,7 +88,7 @@ object SeekMoreLeadersCommandChooser {
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
acs: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
): Option[CommandSelection] = {
|
||||
LegacyFactionUtils
|
||||
.provinces(actingFactionId, gameState)
|
||||
.find(
|
||||
@@ -101,34 +106,36 @@ object SeekMoreLeadersCommandChooser {
|
||||
province.rulingFactionHeroIds.map(gameState.heroes),
|
||||
gameState.factions(actingFactionId).leaders.toVector
|
||||
)
|
||||
.map(hero => Candidate(hero, province))
|
||||
.map { hero => Candidate(hero, province) }
|
||||
}
|
||||
|
||||
val marchACs = acs.collect { case ac: MarchAvailableCommand => ac }
|
||||
val marchACs = acs
|
||||
.collect { case ac: MarchAvailableCommand => ac }
|
||||
|
||||
val marchableCandidates = candidates.flatMap { c =>
|
||||
marchACs.find { mac =>
|
||||
mac.oneProvinceCommands.exists(opmc =>
|
||||
opmc.originProvinceId == c.province.id && opmc.availableHeroIds.toVector
|
||||
.contains(c.hero.id)
|
||||
)
|
||||
}
|
||||
marchACs
|
||||
.find { mac =>
|
||||
mac.oneProvinceCommands.exists(opmc =>
|
||||
opmc.originProvinceId == c.province.id && opmc.availableHeroIds.toVector
|
||||
.contains(c.hero.id)
|
||||
)
|
||||
}
|
||||
.map(mac => (mac, c))
|
||||
}
|
||||
|
||||
marchableCandidates
|
||||
.maxByOption(x => LegacyHeroUtils.power(x._2.hero))
|
||||
.flatMap {
|
||||
case (command, candidate) =>
|
||||
maybeMoveTowardDestination(
|
||||
command = command,
|
||||
hero = candidate.hero,
|
||||
startingProvince = candidate.province,
|
||||
factionHeadProvince = provinceWithFactionHead,
|
||||
gameState = gameState
|
||||
)
|
||||
.flatMap { case (command, candidate) =>
|
||||
maybeMoveTowardDestination(
|
||||
command = command,
|
||||
hero = candidate.hero,
|
||||
startingProvince = candidate.province,
|
||||
factionHeadProvince = provinceWithFactionHead,
|
||||
gameState = gameState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def maybeSeekMoreLeadersCommand(
|
||||
actingFactionId: FactionId,
|
||||
@@ -147,36 +154,37 @@ object SeekMoreLeadersCommandChooser {
|
||||
faction.leaders.toVector
|
||||
)
|
||||
|
||||
desiredHero.flatMap { hero =>
|
||||
SwearBrotherhoodCommandSelector
|
||||
.chosenCommandForSpecificHero(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = acs,
|
||||
desiredHeroId = hero.id
|
||||
)
|
||||
.orElse {
|
||||
MoreOption.flatWhen(
|
||||
MinimumLoyaltyForSwearBrotherhood.doubleValue - hero.loyalty < LoyaltyGainFromFeast.doubleValue
|
||||
) {
|
||||
CommandChoiceHelpers.chosenFeastCommandIfAvailable(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = acs,
|
||||
reason = s"want to make hero ${hero.id} a leader"
|
||||
)
|
||||
}
|
||||
}
|
||||
.orElse {
|
||||
HeroGiftCommandSelector.chosenCommandForSpecificHero(
|
||||
desiredHero
|
||||
.flatMap { hero =>
|
||||
SwearBrotherhoodCommandSelector
|
||||
.chosenCommandForSpecificHero(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = acs,
|
||||
heroId = hero.id,
|
||||
reason = "want to make this hero a leader"
|
||||
desiredHeroId = hero.id
|
||||
)
|
||||
}
|
||||
}
|
||||
.orElse {
|
||||
MoreOption.flatWhen(
|
||||
MinimumLoyaltyForSwearBrotherhood.doubleValue - hero.loyalty < LoyaltyGainFromFeast.doubleValue
|
||||
) {
|
||||
CommandChoiceHelpers.chosenFeastCommandIfAvailable(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = acs,
|
||||
reason = s"want to make hero ${hero.id} a leader"
|
||||
)
|
||||
}
|
||||
}
|
||||
.orElse {
|
||||
HeroGiftCommandSelector.chosenCommandForSpecificHero(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = acs,
|
||||
heroId = hero.id,
|
||||
reason = "want to make this hero a leader"
|
||||
)
|
||||
}
|
||||
}
|
||||
.orElse(
|
||||
// Then try moving a hero this way
|
||||
maybeMoveBestCandidateCommand(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package net.eagle0.eagle.client_text
|
||||
|
||||
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
|
||||
import net.eagle0.eagle.ClientTextId
|
||||
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
|
||||
|
||||
sealed trait ClientText {
|
||||
def id: ClientTextId
|
||||
@@ -37,5 +37,5 @@ case class UnrequestedClientText(
|
||||
llmRequest: GeneratedTextRequest
|
||||
) extends ClientText {
|
||||
def complete: Boolean = false
|
||||
def text: String = ""
|
||||
def text: String = ""
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
package net.eagle0.eagle.client_text
|
||||
|
||||
import net.eagle0.common.ProtoParser
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId}
|
||||
import net.eagle0.eagle.internal.client_text.{
|
||||
IncompleteText,
|
||||
IncompleteTextStore,
|
||||
UnrequestedText
|
||||
}
|
||||
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.service.persistence.Persister
|
||||
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import scala.io.Source
|
||||
import scala.util.Try
|
||||
|
||||
import net.eagle0.common.ProtoParser
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId}
|
||||
import net.eagle0.eagle.internal.client_text.{IncompleteText, IncompleteTextStore, UnrequestedText}
|
||||
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.service.persistence.Persister
|
||||
|
||||
case class ClientTextStoreImpl(
|
||||
pregenerated: PregeneratedClientTextStore,
|
||||
persister: Persister,
|
||||
@@ -35,24 +38,25 @@ case class ClientTextStoreImpl(
|
||||
requestedAfterHistoryCount: Int
|
||||
): ClientTextStore = {
|
||||
internalRequire(
|
||||
pregenerated.getText(id).isEmpty,
|
||||
pregenerated.getText(id.value).isEmpty,
|
||||
s"Text with id $id is pregenerated"
|
||||
)
|
||||
|
||||
if completeTexts.contains(id) then {
|
||||
if completeTexts.contains(id.value) then {
|
||||
println(s"Text with id $id already exists")
|
||||
this
|
||||
} else if incompleteTexts.contains(id) then {
|
||||
} else if incompleteTexts.contains(id.value) then {
|
||||
println(s"Incomplete text with id $id already exists")
|
||||
this
|
||||
} else {
|
||||
copy(
|
||||
unrequestedTexts = unrequestedTexts + (id -> UnrequestedClientText(
|
||||
id = id,
|
||||
requestedAfterHistoryCount = requestedAfterHistoryCount,
|
||||
llmRequest = llmRequest
|
||||
)),
|
||||
accessibleTo = this.accessibleTo + (id -> accessibleTo),
|
||||
unrequestedTexts =
|
||||
unrequestedTexts + (id.value -> UnrequestedClientText(
|
||||
id = id,
|
||||
requestedAfterHistoryCount = requestedAfterHistoryCount,
|
||||
llmRequest = llmRequest
|
||||
)),
|
||||
accessibleTo = this.accessibleTo + (id.value -> accessibleTo),
|
||||
accessibleToIsSaved = false,
|
||||
incompleteTextsAreSaved = false
|
||||
)
|
||||
@@ -61,17 +65,18 @@ case class ClientTextStoreImpl(
|
||||
|
||||
def withMarkedRequested(id: ClientTextId): ClientTextStore =
|
||||
unrequestedTexts
|
||||
.get(id)
|
||||
.get(id.value)
|
||||
.map { unrequested =>
|
||||
copy(
|
||||
incompleteTexts = incompleteTexts + (id ->
|
||||
incompleteTexts = incompleteTexts + (id.value ->
|
||||
IncompleteClientText(
|
||||
id = id,
|
||||
partialText = "",
|
||||
llmRequest = unrequested.llmRequest,
|
||||
requestedAfterHistoryCount = unrequested.requestedAfterHistoryCount
|
||||
requestedAfterHistoryCount =
|
||||
unrequested.requestedAfterHistoryCount
|
||||
)),
|
||||
unrequestedTexts = unrequestedTexts - id,
|
||||
unrequestedTexts = unrequestedTexts - id.value,
|
||||
incompleteTextsAreSaved = false
|
||||
)
|
||||
}
|
||||
@@ -83,7 +88,7 @@ case class ClientTextStoreImpl(
|
||||
|
||||
def withBypassed(id: ClientTextId): ClientTextStore =
|
||||
copy(
|
||||
unrequestedTexts = unrequestedTexts - id,
|
||||
unrequestedTexts = unrequestedTexts - id.value,
|
||||
incompleteTextsAreSaved = false
|
||||
)
|
||||
|
||||
@@ -91,18 +96,18 @@ case class ClientTextStoreImpl(
|
||||
pregenerated
|
||||
.getText(id)
|
||||
.orElse {
|
||||
completeTexts.get(id).map(_.text)
|
||||
completeTexts.get(id.value).map(_.text)
|
||||
}
|
||||
.map(text => TextGenerationSuccess(text))
|
||||
.map { text => TextGenerationSuccess(text) }
|
||||
.getOrElse {
|
||||
incompleteTexts
|
||||
.get(id)
|
||||
.get(id.value)
|
||||
.map { ict =>
|
||||
TextGenerationDependencyInProgress(ict.id, ict.text)
|
||||
}
|
||||
.getOrElse {
|
||||
unrequestedTexts
|
||||
.get(id)
|
||||
.get(id.value)
|
||||
.map { ut =>
|
||||
TextGenerationDependencyWaiting(ut.id)
|
||||
}
|
||||
@@ -118,19 +123,19 @@ case class ClientTextStoreImpl(
|
||||
complete: Boolean
|
||||
): ClientTextStoreWithUpdate = {
|
||||
internalRequire(
|
||||
!unrequestedTexts.contains(id),
|
||||
!unrequestedTexts.contains(id.value),
|
||||
s"Text with id $id is unrequested"
|
||||
)
|
||||
internalRequire(
|
||||
!completeTexts.contains(id),
|
||||
!completeTexts.contains(id.value),
|
||||
s"Text with id $id is already complete"
|
||||
)
|
||||
internalRequire(
|
||||
pregenerated.getText(id).isEmpty,
|
||||
pregenerated.getText(id.value).isEmpty,
|
||||
s"Text with id $id is pregenerated"
|
||||
)
|
||||
val update = incompleteTexts
|
||||
.get(id)
|
||||
.get(id.value)
|
||||
.map(t => t.append(newText))
|
||||
.getOrElse {
|
||||
throw new EagleInternalException(s"Text with id $id not found")
|
||||
@@ -145,8 +150,8 @@ case class ClientTextStoreImpl(
|
||||
|
||||
ClientTextStoreWithUpdate(
|
||||
copy(
|
||||
completeTexts = completeTexts + (id -> updatedText),
|
||||
incompleteTexts = incompleteTexts - id,
|
||||
completeTexts = completeTexts + (id.value -> updatedText),
|
||||
incompleteTexts = incompleteTexts - id.value,
|
||||
incompleteTextsAreSaved = false
|
||||
),
|
||||
updatedText
|
||||
@@ -154,7 +159,7 @@ case class ClientTextStoreImpl(
|
||||
} else {
|
||||
ClientTextStoreWithUpdate(
|
||||
copy(
|
||||
incompleteTexts = incompleteTexts + (id -> update),
|
||||
incompleteTexts = incompleteTexts + (id.value -> update),
|
||||
incompleteTextsAreSaved = false
|
||||
),
|
||||
update
|
||||
@@ -167,28 +172,30 @@ case class ClientTextStoreImpl(
|
||||
addedFactionIds: Vector[FactionId]
|
||||
): ClientTextStore = {
|
||||
val originalFactionIds = accessibleTo.getOrElse(id, Vector()).sorted
|
||||
val updatedFactionIds =
|
||||
val updatedFactionIds =
|
||||
(originalFactionIds ++ addedFactionIds).distinct.sorted
|
||||
|
||||
if updatedFactionIds == originalFactionIds then this
|
||||
else
|
||||
copy(
|
||||
accessibleTo = accessibleTo + (id -> updatedFactionIds),
|
||||
accessibleTo = accessibleTo + (id.value -> updatedFactionIds),
|
||||
accessibleToIsSaved = false
|
||||
)
|
||||
}
|
||||
|
||||
def withMovedBackToUnrequested(id: ClientTextId): ClientTextStore =
|
||||
incompleteTexts
|
||||
.get(id)
|
||||
.get(id.value)
|
||||
.map { incomplete =>
|
||||
copy(
|
||||
unrequestedTexts = unrequestedTexts + (id -> UnrequestedClientText(
|
||||
id = id,
|
||||
requestedAfterHistoryCount = incomplete.requestedAfterHistoryCount,
|
||||
llmRequest = incomplete.llmRequest
|
||||
)),
|
||||
incompleteTexts = incompleteTexts - id,
|
||||
unrequestedTexts =
|
||||
unrequestedTexts + (id.value -> UnrequestedClientText(
|
||||
id = id,
|
||||
requestedAfterHistoryCount =
|
||||
incomplete.requestedAfterHistoryCount,
|
||||
llmRequest = incomplete.llmRequest
|
||||
)),
|
||||
incompleteTexts = incompleteTexts - id.value,
|
||||
incompleteTextsAreSaved = false
|
||||
)
|
||||
}
|
||||
@@ -201,22 +208,22 @@ case class ClientTextStoreImpl(
|
||||
}
|
||||
|
||||
object ClientTextStoreImpl {
|
||||
private val separator = "\n******\n"
|
||||
private val separator = "\n******\n"
|
||||
private val separatorPattern = Pattern.quote(separator)
|
||||
|
||||
private def completeTextsKey = "completeText.txt"
|
||||
private def completeTextsKey = "completeText.txt"
|
||||
private def incompleteTextsKey = "incompleteText.dat"
|
||||
private def visibilityKey = "visibility.txt"
|
||||
private def visibilityKey = "visibility.txt"
|
||||
|
||||
def saved(clientTextStore: ClientTextStoreImpl): ClientTextStore = {
|
||||
val completeSaved =
|
||||
if clientTextStore.completeTexts.size > clientTextStore.savedCompleteCount
|
||||
then {
|
||||
// FIXME: make this append, will require Persister changes
|
||||
val saveData = clientTextStore.completeTexts.map {
|
||||
case (_: String, ct: CompleteClientText) =>
|
||||
val saveData = clientTextStore.completeTexts
|
||||
.map { case (_: String, ct: CompleteClientText) =>
|
||||
s"${ct.id}\n${ct.requestedAfterHistoryCount}\n${ct.text}$separator"
|
||||
}
|
||||
}
|
||||
.mkString("")
|
||||
.getBytes(StandardCharsets.UTF_8)
|
||||
|
||||
@@ -226,7 +233,9 @@ object ClientTextStoreImpl {
|
||||
)
|
||||
}
|
||||
|
||||
clientTextStore.copy(savedCompleteCount = clientTextStore.completeTexts.size)
|
||||
clientTextStore.copy(savedCompleteCount =
|
||||
clientTextStore.completeTexts.size
|
||||
)
|
||||
} else clientTextStore
|
||||
|
||||
val incompleteSaved =
|
||||
@@ -263,10 +272,10 @@ object ClientTextStoreImpl {
|
||||
|
||||
val accessibleToSaved =
|
||||
if !incompleteSaved.accessibleToIsSaved then {
|
||||
val accessibleToData = incompleteSaved.accessibleTo.map {
|
||||
case (id, factions) =>
|
||||
val accessibleToData = incompleteSaved.accessibleTo
|
||||
.map { case (id, factions) =>
|
||||
s"$id\n${factions.mkString(",")}$separator"
|
||||
}
|
||||
}
|
||||
.mkString("")
|
||||
.getBytes(StandardCharsets.UTF_8)
|
||||
|
||||
@@ -309,10 +318,9 @@ object ClientTextStoreImpl {
|
||||
}
|
||||
.toVector
|
||||
}
|
||||
.recover {
|
||||
case _: FileNotFoundException =>
|
||||
println("No complete text store found")
|
||||
Vector()
|
||||
.recover { case _: FileNotFoundException =>
|
||||
println("No complete text store found")
|
||||
Vector()
|
||||
}
|
||||
|
||||
private def loadedIncompleteTexts(
|
||||
@@ -344,10 +352,9 @@ object ClientTextStoreImpl {
|
||||
}
|
||||
.getOrElse((Vector(), Vector()))
|
||||
}
|
||||
.recover {
|
||||
case _: FileNotFoundException =>
|
||||
println("No incomplete text store found")
|
||||
(Vector(), Vector())
|
||||
.recover { case _: FileNotFoundException =>
|
||||
println("No incomplete text store found")
|
||||
(Vector(), Vector())
|
||||
}
|
||||
|
||||
private def loadedAccessibleTo(
|
||||
@@ -371,10 +378,9 @@ object ClientTextStoreImpl {
|
||||
}
|
||||
.toMap
|
||||
}
|
||||
.recover {
|
||||
case _: FileNotFoundException =>
|
||||
println("No accessibleTo store found")
|
||||
Map()
|
||||
.recover { case _: FileNotFoundException =>
|
||||
println("No accessibleTo store found")
|
||||
Map()
|
||||
}
|
||||
|
||||
def loaded(
|
||||
@@ -382,18 +388,22 @@ object ClientTextStoreImpl {
|
||||
persister: Persister
|
||||
): Try[ClientTextStore] =
|
||||
for {
|
||||
loadedCompletedTexts <- loadedCompleteTexts(persister)
|
||||
loadedCompletedTexts <- loadedCompleteTexts(persister)
|
||||
loadedIncompleteTexts <- loadedIncompleteTexts(persister)
|
||||
loadedAccessibleTo <- loadedAccessibleTo(persister)
|
||||
} yield ClientTextStoreImpl(
|
||||
pregenerated = pregenerated,
|
||||
persister = persister,
|
||||
completeTexts = loadedCompletedTexts.map(ct => ct.id -> ct).toMap,
|
||||
incompleteTexts = loadedIncompleteTexts._1.map(ict => ict.id -> ict).toMap,
|
||||
unrequestedTexts = loadedIncompleteTexts._2.map(ut => ut.id -> ut).toMap,
|
||||
accessibleTo = loadedAccessibleTo,
|
||||
savedCompleteCount = loadedCompletedTexts.size,
|
||||
incompleteTextsAreSaved = true,
|
||||
accessibleToIsSaved = true
|
||||
)
|
||||
loadedAccessibleTo <- loadedAccessibleTo(persister)
|
||||
} yield {
|
||||
ClientTextStoreImpl(
|
||||
pregenerated = pregenerated,
|
||||
persister = persister,
|
||||
completeTexts = loadedCompletedTexts.map(ct => ct.id -> ct).toMap,
|
||||
incompleteTexts =
|
||||
loadedIncompleteTexts._1.map(ict => ict.id -> ict).toMap,
|
||||
unrequestedTexts =
|
||||
loadedIncompleteTexts._2.map(ut => ut.id -> ut).toMap,
|
||||
accessibleTo = loadedAccessibleTo,
|
||||
savedCompleteCount = loadedCompletedTexts.size,
|
||||
incompleteTextsAreSaved = true,
|
||||
accessibleToIsSaved = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.eagle0.eagle.client_text
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
object TextGenerationResult {
|
||||
def mkString(
|
||||
results: Seq[TextGenerationResult],
|
||||
@@ -22,7 +22,7 @@ object TextGenerationResult {
|
||||
acc :+ result,
|
||||
tail
|
||||
)
|
||||
case x => x
|
||||
case x => x
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,28 +47,30 @@ trait TextGenerationResult {
|
||||
}
|
||||
case class TextGenerationSuccess(result: String) extends TextGenerationResult {
|
||||
override def resolved: Boolean = true
|
||||
override def get: String = result
|
||||
override def get: String = result
|
||||
}
|
||||
case class TextGenerationDependencyInProgress(
|
||||
inProgressTextId: String,
|
||||
partialText: String
|
||||
) extends TextGenerationResult {
|
||||
override def resolved: Boolean = false
|
||||
override def get: String =
|
||||
override def get: String =
|
||||
throw new EagleInternalException(
|
||||
s"Text generation dependency in progress: $inProgressTextId"
|
||||
)
|
||||
}
|
||||
case class TextGenerationDependencyWaiting(notSatisfiedTextId: String) extends TextGenerationResult {
|
||||
case class TextGenerationDependencyWaiting(notSatisfiedTextId: String)
|
||||
extends TextGenerationResult {
|
||||
override def resolved: Boolean = false
|
||||
override def get: String =
|
||||
override def get: String =
|
||||
throw new EagleInternalException(
|
||||
s"Text generation dependency not satisfied: $notSatisfiedTextId"
|
||||
)
|
||||
}
|
||||
case class TextGenerationDependencyUnknown(unknownTextId: String) extends TextGenerationResult {
|
||||
case class TextGenerationDependencyUnknown(unknownTextId: String)
|
||||
extends TextGenerationResult {
|
||||
override def resolved: Boolean = false
|
||||
override def get: String =
|
||||
override def get: String =
|
||||
throw new EagleInternalException(
|
||||
s"Text generation dependency unknown: $unknownTextId"
|
||||
)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package net.eagle0.eagle.library
|
||||
|
||||
import net.eagle0.eagle.FactionId
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
import net.eagle0.eagle.views.game_state_view.GameStateViewDiff
|
||||
import net.eagle0.eagle.common.action_result_notification_details.Notification
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.*
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.util.GameStateViewDiffer
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.view_filters.GameStateViewFilter
|
||||
import net.eagle0.eagle.library.util.GameStateViewDiffer
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
import net.eagle0.eagle.views.game_state_view.GameStateViewDiff
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object ActionResultFilter {
|
||||
private val UNIVERSALLY_VISIBLE_TYPES: Vector[ActionResultType] = Vector(
|
||||
@@ -84,8 +84,8 @@ object ActionResultFilter {
|
||||
RoundPhase.HERO_DEPARTURES,
|
||||
RoundPhase.UNCONTESTED_CONQUEST,
|
||||
RoundPhase.BATTLE_RESOLUTION,
|
||||
RoundPhase.BATTLE_AFTERMATH, // MIGHT BE WRONG
|
||||
RoundPhase.ATTACK_DECISION, // MIGHT BE WRONG
|
||||
RoundPhase.BATTLE_AFTERMATH, // MIGHT BE WRONG
|
||||
RoundPhase.ATTACK_DECISION, // MIGHT BE WRONG
|
||||
RoundPhase.DIPLOMACY_RESOLUTION // IS WRONG
|
||||
)
|
||||
|
||||
@@ -97,7 +97,8 @@ object ActionResultFilter {
|
||||
GameStateViewDiffer.diff(
|
||||
before = GameStateViewFilter
|
||||
.filteredGameState(gs = before, factionId = factionId),
|
||||
after = GameStateViewFilter.filteredGameState(gs = after, factionId = factionId)
|
||||
after =
|
||||
GameStateViewFilter.filteredGameState(gs = after, factionId = factionId)
|
||||
)
|
||||
|
||||
private def transformOne(
|
||||
@@ -114,7 +115,9 @@ object ActionResultFilter {
|
||||
)
|
||||
|
||||
def shouldInclude(note: Notification): Boolean =
|
||||
note.targetFactions.isEmpty || factionId.exists(fid => note.targetFactions.exists(_.factionId == fid))
|
||||
note.targetFactions.isEmpty || factionId.exists(fid =>
|
||||
note.targetFactions.exists(_.factionId == fid)
|
||||
)
|
||||
|
||||
if gsDiff.isDefined &&
|
||||
result.actionResult.player.isDefined &&
|
||||
@@ -140,7 +143,8 @@ object ActionResultFilter {
|
||||
province = actionResult.province,
|
||||
leader = actionResult.leader,
|
||||
gameStateDiff = gsDiff,
|
||||
notifications = actionResult.notificationsToDeliver.filter(shouldInclude)
|
||||
notifications =
|
||||
actionResult.notificationsToDeliver.filter(shouldInclude)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -150,9 +154,10 @@ object ActionResultFilter {
|
||||
results: Vector[ActionWithResultingState],
|
||||
factionId: Option[FactionId]
|
||||
): Vector[ActionResultView] =
|
||||
factionId.map { pid =>
|
||||
filterForPlayer(startingState, results, pid)
|
||||
}
|
||||
factionId
|
||||
.map { pid =>
|
||||
filterForPlayer(startingState, results, pid)
|
||||
}
|
||||
.getOrElse(filterForObserver(startingState, results))
|
||||
|
||||
def filterForPlayer(
|
||||
@@ -177,5 +182,7 @@ object ActionResultFilter {
|
||||
startingState: GameState,
|
||||
results: Vector[ActionWithResultingState]
|
||||
): Vector[ActionResultView] =
|
||||
results.flatMap(res => observeOne(result = res, startingState = startingState))
|
||||
results.flatMap(res =>
|
||||
observeOne(result = res, startingState = startingState)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
@@ -215,18 +214,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
package net.eagle0.eagle.library
|
||||
|
||||
class EagleValidationException(message: String) extends EagleInternalException(message) {}
|
||||
class EagleValidationException(message: String)
|
||||
extends EagleInternalException(message) {}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package net.eagle0.eagle.library
|
||||
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
import net.eagle0.eagle.api.command.AvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
|
||||
trait EngineAndResults {
|
||||
def engine: Engine
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
package net.eagle0.eagle.library
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.common.SeededRandom
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.api.command.AvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.applier.{ActionResultProtoApplier, ActionResultProtoApplierImpl}
|
||||
import net.eagle0.eagle.library.EngineImpl.{appliedResults, withUpdateChecks}
|
||||
import net.eagle0.eagle.library.actions.applier.{
|
||||
ActionResultProtoApplier,
|
||||
ActionResultProtoApplierImpl
|
||||
}
|
||||
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
|
||||
import net.eagle0.eagle.library.actions.impl.action.{
|
||||
CheckForFulfilledQuestsAction,
|
||||
HeroBackstoryUpdateActionGenerator,
|
||||
ResolveBattleAction
|
||||
}
|
||||
import net.eagle0.eagle.library.actions.impl.command.{AvailableCommandTypeMap, CommandFactory}
|
||||
import net.eagle0.eagle.library.actions.impl.common.{ActionWithResultingState, RandomStateProtoSequencer}
|
||||
import net.eagle0.eagle.library.actions.impl.command.{
|
||||
AvailableCommandTypeMap,
|
||||
CommandFactory
|
||||
}
|
||||
import net.eagle0.eagle.library.actions.impl.common.{
|
||||
ActionWithResultingState,
|
||||
RandomStateProtoSequencer
|
||||
}
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
|
||||
import net.eagle0.eagle.library.util.validations.RuntimeValidator
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EngineImpl.{appliedResults, withUpdateChecks}
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
object EngineImpl {
|
||||
def apply(
|
||||
@@ -74,9 +82,11 @@ object EngineImpl {
|
||||
.toVector,
|
||||
battalions = eng.currentState.battalions.values.toVector
|
||||
.map(BattalionConverter.fromProto),
|
||||
getHero = hid => eng.currentState.heroes.get(hid).map(HeroConverter.fromProto),
|
||||
getHero =
|
||||
hid => eng.currentState.heroes.get(hid).map(HeroConverter.fromProto),
|
||||
battalionTypes = eng.currentState.battalionTypes.toVector,
|
||||
heroBackstoryTextIdLookup = hid => eng.currentState.heroes(hid).backstoryVersions.last.textId
|
||||
heroBackstoryTextIdLookup =
|
||||
hid => eng.currentState.heroes(hid).backstoryVersions.last.textId
|
||||
).results
|
||||
)
|
||||
|
||||
@@ -88,7 +98,8 @@ object EngineImpl {
|
||||
withPhaseAdvancement(engineAndResults)
|
||||
)
|
||||
|
||||
if newEar.results.length > engineAndResults.results.length then withUpdateChecks(newEar)
|
||||
if newEar.results.length > engineAndResults.results.length then
|
||||
withUpdateChecks(newEar)
|
||||
else newEar
|
||||
}
|
||||
|
||||
@@ -98,7 +109,8 @@ object EngineImpl {
|
||||
): EngineAndResults = withUpdateChecks(
|
||||
EngineAndResultsImpl(
|
||||
engine = engine.copy(
|
||||
currentState = results.lastOption.map(_.gameState).getOrElse(engine.currentState),
|
||||
currentState =
|
||||
results.lastOption.map(_.gameState).getOrElse(engine.currentState),
|
||||
history = engine.history.withNewResults(results)
|
||||
),
|
||||
results = results.map(_.actionResult)
|
||||
@@ -133,14 +145,14 @@ final case class EngineAndResultsImpl(
|
||||
|
||||
def recursiveTransformT(
|
||||
f: EngineImpl => Vector[ActionResultT]
|
||||
): EngineAndResultsImpl = recursiveTransform { eng =>
|
||||
): EngineAndResultsImpl = recursiveTransform(eng => {
|
||||
val results = f(eng)
|
||||
RandomStateProtoSequencer(
|
||||
initialStateProto = eng.currentState,
|
||||
initialState = eng.currentState,
|
||||
actionResultProtoApplier = eng.actionResultProtoApplier,
|
||||
functionalRandom = SeededRandom(eng.currentState.randomSeed)
|
||||
).withActionResultTs(_ => results).results.newValue
|
||||
}
|
||||
})
|
||||
|
||||
def saveNow: EngineAndResultsImpl =
|
||||
this.copy(engine = this.engine.saveNow)
|
||||
@@ -156,7 +168,8 @@ case class EngineImpl(
|
||||
override val currentState: GameState,
|
||||
heroGenerator: HeroGenerator,
|
||||
override val history: GameHistory,
|
||||
actionResultProtoApplier: ActionResultProtoApplier = new ActionResultProtoApplierImpl(validator = RuntimeValidator)
|
||||
actionResultProtoApplier: ActionResultProtoApplier =
|
||||
new ActionResultProtoApplierImpl(validator = RuntimeValidator)
|
||||
) extends Engine {
|
||||
import net.eagle0.eagle.library.util.EagleRequire.commandRequire
|
||||
|
||||
@@ -284,17 +297,17 @@ case class EngineImpl(
|
||||
availableCommandOpt.isDefined,
|
||||
s"No matching available command for selected command $selectedCommand"
|
||||
)
|
||||
val availableCommand = availableCommandOpt.get
|
||||
val availableCommand = availableCommandOpt.get
|
||||
|
||||
val sequencer = RandomStateProtoSequencer(
|
||||
initialStateProto = this.currentState,
|
||||
initialState = this.currentState,
|
||||
actionResultProtoApplier = actionResultProtoApplier,
|
||||
functionalRandom = SeededRandom(this.currentState.randomSeed)
|
||||
).withActionResults { gs =>
|
||||
val results = commandFactory
|
||||
.makeCommand(
|
||||
actingFactionId = factionId,
|
||||
gameState = GameStateConverter.fromProto(gs),
|
||||
gameState = gs,
|
||||
availableCommand = availableCommand,
|
||||
selectedCommand = selectedCommand
|
||||
)
|
||||
@@ -312,7 +325,9 @@ case class EngineImpl(
|
||||
)
|
||||
|
||||
results
|
||||
}.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator.fromGameState(gs))
|
||||
}.withProtolessSequentialResultsAction(gs =>
|
||||
HeroBackstoryUpdateActionGenerator.fromGameState(gs)
|
||||
)
|
||||
|
||||
appliedResults(
|
||||
engine = this,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package net.eagle0.eagle.library
|
||||
|
||||
import net.eagle0.eagle.{FactionId, RoundId, ShardokGameId}
|
||||
import net.eagle0.eagle.common.date.Date
|
||||
import net.eagle0.eagle.{FactionId, RoundId, ShardokGameId}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.shardok.storage.action_result.ActionResult as ShardokActionResult
|
||||
import net.eagle0.shardok.api.action_result_view.ActionResultView as ShardokActionResultView
|
||||
import net.eagle0.shardok.api.command_descriptor.AvailableCommands as ShardokAvailableCommands
|
||||
import net.eagle0.shardok.storage.action_result.ActionResult as ShardokActionResult
|
||||
|
||||
case class CappedResults(
|
||||
results: Vector[ActionWithResultingState],
|
||||
@@ -17,7 +17,8 @@ trait GameHistory {
|
||||
def all: Vector[ActionWithResultingState]
|
||||
def since(count: Int): Vector[ActionWithResultingState]
|
||||
def sinceCapped(count: Int, resultSizeCap: Int): CappedResults =
|
||||
if this.count - count <= resultSizeCap then CappedResults(since(count), None)
|
||||
if this.count - count <= resultSizeCap then
|
||||
CappedResults(since(count), None)
|
||||
else
|
||||
CappedResults(
|
||||
since(this.count - resultSizeCap),
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
package net.eagle0.eagle.library
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
import net.eagle0.common.SeededRandom
|
||||
import net.eagle0.eagle.RoundId
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase.*
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.applier.{ActionResultProtoApplier, ActionResultTApplierImpl}
|
||||
import net.eagle0.eagle.library.actions.applier.{
|
||||
ActionResultProtoApplier,
|
||||
ActionResultTApplierImpl
|
||||
}
|
||||
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
|
||||
import net.eagle0.eagle.library.actions.impl.action.*
|
||||
import net.eagle0.eagle.library.actions.impl.command.CommandFactory
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalValidated
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.RoundId
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
object RoundPhaseAdvancer {
|
||||
private val times: mutable.Map[RoundPhase, Long] =
|
||||
mutable.Map(RoundPhase.values.map(_ -> 0L)*)
|
||||
private val print = false
|
||||
private val roundsBetweenPrint = 100
|
||||
private val print = false
|
||||
private val roundsBetweenPrint = 100
|
||||
|
||||
private def printTimings(roundId: RoundId): Unit = {
|
||||
val totalTime = times.values.sum.toDouble / 1000.0
|
||||
@@ -36,7 +34,7 @@ object RoundPhaseAdvancer {
|
||||
case (phase, time) =>
|
||||
val timeInSecs = time.toDouble / 1000.0
|
||||
val msPerRound = time.toDouble / roundId.toDouble
|
||||
val timePct = timeInSecs / totalTime * 100.0
|
||||
val timePct = timeInSecs / totalTime * 100.0
|
||||
println(
|
||||
f"$phase%-25s: $timeInSecs%6.2fs ($msPerRound%4.2fms per round), $timePct%4.1f%%"
|
||||
)
|
||||
@@ -53,7 +51,7 @@ object RoundPhaseAdvancer {
|
||||
commandFactory: CommandFactory
|
||||
): Vector[ActionWithResultingState] = {
|
||||
val currentPhase = currentState.currentPhase
|
||||
val startTime = System.currentTimeMillis
|
||||
val startTime = System.currentTimeMillis
|
||||
|
||||
if print && currentPhase == NEW_ROUND && currentState.currentRoundId % roundsBetweenPrint == 0
|
||||
then {
|
||||
@@ -194,27 +192,7 @@ object RoundPhaseAdvancer {
|
||||
case UNCONTESTED_CONQUEST =>
|
||||
actionResultProtoApplier.applyActionResults(
|
||||
currentState,
|
||||
PerformUncontestedConquestAction(
|
||||
gameId = currentState.gameId,
|
||||
currentRoundId = currentState.currentRoundId,
|
||||
currentDate = DateConverter.fromProto(currentState.currentDate),
|
||||
provinces = currentState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.map(p => p.id -> p)
|
||||
.toMap,
|
||||
factions = currentState.factions.values
|
||||
.map(FactionConverter.fromProto)
|
||||
.map(f => f.id -> f)
|
||||
.toMap,
|
||||
heroes = currentState.heroes.values
|
||||
.map(HeroConverter.fromProto)
|
||||
.map(h => h.id -> h)
|
||||
.toMap,
|
||||
battalions = currentState.battalions.values
|
||||
.map(BattalionConverter.fromProto)
|
||||
.map(b => (b.id, b))
|
||||
.toMap
|
||||
).results
|
||||
PerformUncontestedConquestAction(currentState).results
|
||||
.map(ActionResultProtoConverter.toProto)
|
||||
)
|
||||
|
||||
@@ -262,32 +240,8 @@ object RoundPhaseAdvancer {
|
||||
TruceTurnBackPhaseAction(currentState).execute(actionResultProtoApplier)
|
||||
|
||||
case BATTLE_REQUEST =>
|
||||
val requestBattlesAction = RequestBattlesAction(
|
||||
gameId = currentState.gameId,
|
||||
currentRoundId = currentState.currentRoundId,
|
||||
currentDate = DateConverter.fromProto(currentState.currentDate),
|
||||
battleCounter = currentState.battleCounter,
|
||||
heroes = currentState.heroes.view.mapValues(HeroConverter.fromProto).toMap,
|
||||
battalions = currentState.battalions.view
|
||||
.mapValues(BattalionConverter.fromProto)
|
||||
.toMap,
|
||||
provinces = currentState.provinces.view
|
||||
.mapValues(ProvinceConverter.fromProto)
|
||||
.toMap,
|
||||
factions = currentState.factions.view
|
||||
.mapValues(FactionConverter.fromProto)
|
||||
.toMap,
|
||||
battalionTypes = currentState.battalionTypes.map { bt =>
|
||||
val converted = BattalionTypeConverter.fromProto(bt)
|
||||
converted.typeId -> converted
|
||||
}.toMap
|
||||
)
|
||||
val requestResults = actionResultProtoApplier.applyActionResults(
|
||||
currentState,
|
||||
requestBattlesAction.results.map(
|
||||
ActionResultProtoConverter.toProto(_)
|
||||
)
|
||||
)
|
||||
val requestResults =
|
||||
RequestBattlesAction(currentState).execute(actionResultProtoApplier)
|
||||
requestResults ++ EndBattleRequestPhaseAction(
|
||||
requestResults.lastOption.map(_.gameState).getOrElse(currentState)
|
||||
).execute(actionResultProtoApplier)
|
||||
@@ -333,7 +287,8 @@ object RoundPhaseAdvancer {
|
||||
currentState,
|
||||
EndDiplomacyResolutionPhaseAction(
|
||||
currentState,
|
||||
actionResultTApplier = ActionResultTApplierImpl(actionResultProtoApplier)
|
||||
actionResultTApplier =
|
||||
ActionResultTApplierImpl(actionResultProtoApplier)
|
||||
).randomResults(SeededRandom(currentState.randomSeed))
|
||||
.newValue
|
||||
.map(ActionResultProtoConverter.toProto)
|
||||
@@ -347,7 +302,7 @@ object RoundPhaseAdvancer {
|
||||
case Unrecognized(x) =>
|
||||
throw new IllegalStateException(s"Unknown round phase $x")
|
||||
}
|
||||
val timeSpent = System.currentTimeMillis - startTime
|
||||
val timeSpent = System.currentTimeMillis - startTime
|
||||
times(currentPhase) = times(currentPhase) + timeSpent
|
||||
|
||||
validateResults(results, currentState, availableCommandsFactory)
|
||||
|
||||
+163
-136
@@ -7,7 +7,6 @@ import net.eagle0.eagle.common.action_result_type.ActionResultType
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionType
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.common.date.Date
|
||||
import net.eagle0.eagle.common.hero_backstory_version.BackstoryVersion
|
||||
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType
|
||||
import net.eagle0.eagle.common.recruitment_info.RecruitmentInfo
|
||||
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.{
|
||||
@@ -31,7 +30,10 @@ import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.army.{HostileArmyGroup, MovingArmy}
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.changed_faction.{ChangedFaction, TrustLevelUpdate}
|
||||
import net.eagle0.eagle.internal.changed_faction.{
|
||||
ChangedFaction,
|
||||
TrustLevelUpdate
|
||||
}
|
||||
import net.eagle0.eagle.internal.changed_hero.ChangedHero
|
||||
import net.eagle0.eagle.internal.changed_hero.ChangedHero.{Loyalty, Vigor}
|
||||
import net.eagle0.eagle.internal.changed_province.ChangedProvince
|
||||
@@ -42,23 +44,29 @@ import net.eagle0.eagle.internal.faction.Faction.OutgoingOfferRound
|
||||
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.common.hero_backstory_version.BackstoryVersion
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.run_status.RunStatus
|
||||
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.settings.{ExtraXpForStatBumpOver100, XpForStatBump}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.validations.Validator
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.ProvinceEventUtils
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
ExtraXpForStatBumpOver100,
|
||||
XpForStatBump
|
||||
}
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.validations.Validator
|
||||
import net.eagle0.eagle.library.util.ProvinceEventUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
|
||||
class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultProtoApplier {
|
||||
class ActionResultProtoApplierImpl(validator: Validator)
|
||||
extends ActionResultProtoApplier {
|
||||
import validator.validate
|
||||
|
||||
override def xpForStatBump(stat: Int): Int =
|
||||
if stat <= 99 then XpForStatBump.intValue
|
||||
else XpForStatBump.intValue + ExtraXpForStatBumpOver100.intValue * (stat - 99)
|
||||
else
|
||||
XpForStatBump.intValue + ExtraXpForStatBumpOver100.intValue * (stat - 99)
|
||||
|
||||
private val trustMax: Int = 100
|
||||
|
||||
@@ -85,11 +93,10 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
if changedProvinces.isEmpty then gameState
|
||||
else
|
||||
changedProvinces
|
||||
.foldLeft(gameState) {
|
||||
case (gs, cp) =>
|
||||
val after = gs.applyChangedProvince(cp)
|
||||
validate(after.provinces(cp.id), gs.currentPhase)
|
||||
after
|
||||
.foldLeft(gameState) { case (gs, cp) =>
|
||||
val after = gs.applyChangedProvince(cp)
|
||||
validate(after.provinces(cp.id), gs.currentPhase)
|
||||
after
|
||||
}
|
||||
|
||||
def applyChangedProvince(cp: ChangedProvince): GameState = {
|
||||
@@ -127,7 +134,7 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
_.incomingShipments.modify { is =>
|
||||
val holdovers =
|
||||
is.filterNot(sh => cp.removedIncomingShipmentIds.contains(sh.id))
|
||||
val nextId = holdovers
|
||||
val nextId = holdovers
|
||||
.map(_.id)
|
||||
.reduceOption(_ max _)
|
||||
.getOrElse(0) + 1
|
||||
@@ -142,7 +149,9 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
da.map { nda =>
|
||||
nda.withUnits(
|
||||
nda.units
|
||||
.filterNot(u => cp.removedRulingPlayerHeroIds.contains(u.heroId))
|
||||
.filterNot(u =>
|
||||
cp.removedRulingPlayerHeroIds.contains(u.heroId)
|
||||
)
|
||||
.map {
|
||||
case CombatUnit(
|
||||
pid,
|
||||
@@ -176,11 +185,17 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
newF
|
||||
},
|
||||
_.priceIndex.setIfDefined(cp.newPriceIndex),
|
||||
_.economy.modify(e => (e + cp.economyDelta.getOrElse(0.0)).max(0.0).min(100.0)),
|
||||
_.economy.modify(e =>
|
||||
(e + cp.economyDelta.getOrElse(0.0)).max(0.0).min(100.0)
|
||||
),
|
||||
_.agriculture
|
||||
.modify(a => (a + cp.agricultureDelta.getOrElse(0.0)).max(0.0).min(100.0)),
|
||||
.modify(a =>
|
||||
(a + cp.agricultureDelta.getOrElse(0.0)).max(0.0).min(100.0)
|
||||
),
|
||||
_.infrastructure
|
||||
.modify(i => (i + cp.infrastructureDelta.getOrElse(0.0)).max(0.0).min(100.0)),
|
||||
.modify(i =>
|
||||
(i + cp.infrastructureDelta.getOrElse(0.0)).max(0.0).min(100.0)
|
||||
),
|
||||
_.economyDevastation.modify(d =>
|
||||
(d + cp.economyDevastationDelta.getOrElse(0.0))
|
||||
.max(0.0)
|
||||
@@ -196,12 +211,16 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
.max(0.0)
|
||||
.min(provinceBefore.infrastructure)
|
||||
),
|
||||
_.support.modify(s => (s + cp.supportDelta.getOrElse(0.0)).max(0.0).min(100.0)),
|
||||
_.support.modify(s =>
|
||||
(s + cp.supportDelta.getOrElse(0.0)).max(0.0).min(100.0)
|
||||
),
|
||||
_.hasActed.setIfDefined(cp.setHasActed),
|
||||
_.rulerIsTraveling.setIfDefined(cp.setRulerIsTraveling),
|
||||
_.unaffiliatedHeroes.modify(uhs =>
|
||||
uhs
|
||||
.filterNot(uh => cp.removedUnaffiliatedHeroIds.contains(uh.heroId))
|
||||
.filterNot(uh =>
|
||||
cp.removedUnaffiliatedHeroIds.contains(uh.heroId)
|
||||
)
|
||||
.filterNot(uh =>
|
||||
cp.changedUnaffiliatedHeroes
|
||||
.map(_.heroId)
|
||||
@@ -228,14 +247,14 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
cp.newProvinceEvents.exists(
|
||||
_.newEvents.exists(ProvinceEventUtils.isBeastsEvent)
|
||||
)
|
||||
)(gameState.currentDate.get)
|
||||
) { gameState.currentDate.get }
|
||||
),
|
||||
_.lastRiotDate.setIfDefined(
|
||||
Option.when(
|
||||
cp.newProvinceEvents.exists(
|
||||
_.newEvents.exists(ProvinceEventUtils.isImminentRiotEvent)
|
||||
)
|
||||
)(gameState.currentDate.get)
|
||||
) { gameState.currentDate.get }
|
||||
),
|
||||
_.incomingEndTurnActions :++= cp.addedIncomingEndTurnActions,
|
||||
_.incomingEndTurnActions.modify(
|
||||
@@ -246,7 +265,9 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
.map(idx => dcs.patch(idx, Nil, 1))
|
||||
.getOrElse(dcs) ++ cp.addedDeferredChange.asNonEmpty
|
||||
),
|
||||
_.battleRevelations.modify(brs => brs.diff(cp.removedBattleRevelations) ++ cp.addedBattleRevelations)
|
||||
_.battleRevelations.modify(brs =>
|
||||
brs.diff(cp.removedBattleRevelations) ++ cp.addedBattleRevelations
|
||||
)
|
||||
)
|
||||
|
||||
val provinceFixedForRuler =
|
||||
@@ -254,27 +275,27 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
then
|
||||
provinceApplied.update(
|
||||
_.optionalRulingFactionId := None,
|
||||
_.optionalRulingHeroId := None,
|
||||
_.rulingFactionHeroIds := Vector.empty,
|
||||
_.support := 0,
|
||||
_.provinceOrders := ProvinceOrderType.UNKNOWN_ORDERS,
|
||||
_.optionalRulingHeroId := None,
|
||||
_.rulingFactionHeroIds := Vector.empty,
|
||||
_.support := 0,
|
||||
_.provinceOrders := ProvinceOrderType.UNKNOWN_ORDERS,
|
||||
_.unaffiliatedHeroes.modify(uhs =>
|
||||
uhs.map(uh =>
|
||||
uh.update(
|
||||
_.recruitmentInfo := RecruitmentInfo(
|
||||
status = uh.`type` match {
|
||||
case UNAFFILIATED_HERO_PRISONER =>
|
||||
case UNAFFILIATED_HERO_PRISONER =>
|
||||
RECRUITMENT_STATUS_PRISONER
|
||||
case UNAFFILIATED_HERO_MOVING_PRISONER =>
|
||||
case UNAFFILIATED_HERO_MOVING_PRISONER =>
|
||||
RECRUITMENT_STATUS_MOVING_PRISONER
|
||||
case UNAFFILIATED_HERO_RETURNING_PRISONER =>
|
||||
case UNAFFILIATED_HERO_RETURNING_PRISONER =>
|
||||
RECRUITMENT_STATUS_NO_RULER_IN_PROVINCE
|
||||
case UNAFFILIATED_HERO_OUTLAW => RECRUITMENT_STATUS_OUTLAW
|
||||
case UNAFFILIATED_HERO_TRAVELER =>
|
||||
case UNAFFILIATED_HERO_OUTLAW => RECRUITMENT_STATUS_OUTLAW
|
||||
case UNAFFILIATED_HERO_TRAVELER =>
|
||||
RECRUITMENT_STATUS_TRAVELER
|
||||
case UNAFFILIATED_HERO_RESIDENT =>
|
||||
case UNAFFILIATED_HERO_RESIDENT =>
|
||||
RECRUITMENT_STATUS_NO_RULER_IN_PROVINCE
|
||||
case UNAFFILIATED_HERO_UNKNOWN =>
|
||||
case UNAFFILIATED_HERO_UNKNOWN =>
|
||||
throw new EagleInternalException(
|
||||
"Unknown unaffiliated hero type"
|
||||
)
|
||||
@@ -304,13 +325,12 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
): Vector[MovingArmy] = {
|
||||
val holdovers =
|
||||
existingArmies.filterNot(ma => removedIds.contains(ma.id))
|
||||
val nextId = holdovers
|
||||
val nextId = holdovers
|
||||
.map(_.id)
|
||||
.reduceOption(_ max _)
|
||||
.getOrElse(0) + 1
|
||||
holdovers.toVector ++ addedArmies.zipWithIndex.map {
|
||||
case (army, index) =>
|
||||
army.withId(index + nextId)
|
||||
holdovers.toVector ++ addedArmies.zipWithIndex.map { case (army, index) =>
|
||||
army.withId(index + nextId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +339,7 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
removedFactionIds: Seq[FactionId],
|
||||
addedArmies: Seq[HostileArmyGroup],
|
||||
statusChanges: Seq[HostileArmyGroupStatusChange]
|
||||
): Vector[HostileArmyGroup] =
|
||||
): Vector[HostileArmyGroup] = {
|
||||
statusChanges
|
||||
.foldLeft(
|
||||
existingGroups
|
||||
@@ -340,8 +360,9 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
}
|
||||
}
|
||||
.toVector ++ addedArmies
|
||||
}
|
||||
|
||||
def applyProvinceActed(actedProvince: Option[Int]): GameState = {
|
||||
def applyProvinceActed(actedProvince: Option[Int]): GameState = {
|
||||
internalRequire(
|
||||
!actedProvince.contains(0),
|
||||
"Province acted has id 0"
|
||||
@@ -375,19 +396,20 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
): GameState =
|
||||
if newBattalions.isEmpty then gameState
|
||||
else
|
||||
provinceId.map { pid =>
|
||||
val maxCurrentId =
|
||||
if gameState.battalions.isEmpty then 0
|
||||
else gameState.battalions.keys.max
|
||||
val newIds =
|
||||
(maxCurrentId + 1) to (maxCurrentId + newBattalions.size)
|
||||
gameState.update(
|
||||
_.battalions :++= newIds.zip(newBattalions).map {
|
||||
case (id, batt) => id -> batt.withId(id)
|
||||
},
|
||||
_.provinces(pid).battalionIds :++= newIds
|
||||
)
|
||||
}
|
||||
provinceId
|
||||
.map { pid =>
|
||||
val maxCurrentId =
|
||||
if gameState.battalions.isEmpty then 0
|
||||
else gameState.battalions.keys.max
|
||||
val newIds =
|
||||
(maxCurrentId + 1) to (maxCurrentId + newBattalions.size)
|
||||
gameState.update(
|
||||
_.battalions :++= newIds.zip(newBattalions).map {
|
||||
case (id, batt) => id -> batt.withId(id)
|
||||
},
|
||||
_.provinces(pid).battalionIds :++= newIds
|
||||
)
|
||||
}
|
||||
.getOrElse(
|
||||
gameState
|
||||
.update(_.battalions :++= newBattalions.map(b => b.id -> b))
|
||||
@@ -422,15 +444,14 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
_.battalions := gameState.battalions -- destroyedBattalionIds,
|
||||
_.destroyedBattalions :++= destroyedBattalionIds
|
||||
.filterNot(_ == -1)
|
||||
.map(bid => bid -> gameState.battalions(bid)),
|
||||
_.provinces := gameState.provinces.map {
|
||||
case (pid, p) =>
|
||||
pid -> p.update(
|
||||
_.battalionIds := p.battalionIds
|
||||
.filterNot(destroyedBattalionIds.contains),
|
||||
_.incomingArmies := p.incomingArmies
|
||||
.map(a => movingArmyWithoutBattalions(a, destroyedBattalionIds))
|
||||
)
|
||||
.map { bid => bid -> gameState.battalions(bid) },
|
||||
_.provinces := gameState.provinces.map { case (pid, p) =>
|
||||
pid -> p.update(
|
||||
_.battalionIds := p.battalionIds
|
||||
.filterNot(destroyedBattalionIds.contains),
|
||||
_.incomingArmies := p.incomingArmies
|
||||
.map(a => movingArmyWithoutBattalions(a, destroyedBattalionIds))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -463,14 +484,14 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
def applyChangedHeroes(
|
||||
changedHeroes: Seq[ChangedHero],
|
||||
date: Date
|
||||
): GameState =
|
||||
): GameState = {
|
||||
if changedHeroes.isEmpty then gameState
|
||||
else
|
||||
changedHeroes
|
||||
.foldLeft(gameState) {
|
||||
case (gs, ch) =>
|
||||
gs.applyChangedHero(ch, date)
|
||||
.foldLeft(gameState) { case (gs, ch) =>
|
||||
gs.applyChangedHero(ch, date)
|
||||
}
|
||||
}
|
||||
|
||||
private def statBump(
|
||||
stat: Int,
|
||||
@@ -612,10 +633,9 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
)
|
||||
}
|
||||
|
||||
def applyRemovedFactions(removedFactionIds: Set[FactionId]): GameState =
|
||||
gameState.factions.partition {
|
||||
case (fid, _) =>
|
||||
removedFactionIds(fid)
|
||||
def applyRemovedFactions(removedFactionIds: Set[FactionId]): GameState = {
|
||||
gameState.factions.partition { case (fid, _) =>
|
||||
removedFactionIds(fid)
|
||||
} match {
|
||||
case (removed, remaining) =>
|
||||
gameState.update(
|
||||
@@ -623,17 +643,16 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
_.factions := remaining
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def applyRemovedHeroes(removedHeroIds: Set[HeroId]): GameState =
|
||||
gameState.heroes.partition {
|
||||
case (k, _) =>
|
||||
removedHeroIds(k)
|
||||
gameState.heroes.partition { case (k, _) =>
|
||||
removedHeroIds(k)
|
||||
} match {
|
||||
case (removed, remaining) =>
|
||||
gameState.update(
|
||||
_.killedHeroes :++= removed.map {
|
||||
case (hid, hero) =>
|
||||
(hid, hero.clearFactionId)
|
||||
_.killedHeroes :++= removed.map { case (hid, hero) =>
|
||||
(hid, hero.clearFactionId)
|
||||
},
|
||||
_.heroes := remaining
|
||||
)
|
||||
@@ -651,11 +670,12 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
actionResultType: ActionResultType,
|
||||
notification: Option[Notification]
|
||||
): GameState =
|
||||
notification.map { note =>
|
||||
gameState.update(
|
||||
_.deferredNotifications :+= note
|
||||
)
|
||||
}
|
||||
notification
|
||||
.map { note =>
|
||||
gameState.update(
|
||||
_.deferredNotifications :+= note
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
|
||||
def applyRemovedNotifications(
|
||||
@@ -707,34 +727,36 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
)
|
||||
) ++ cf.updatedReconnedProvinces
|
||||
},
|
||||
_.factionRelationships.modify { (relationships: Seq[FactionRelationship]) =>
|
||||
cf.trustLevelUpdates
|
||||
.foldLeft(relationships) {
|
||||
(
|
||||
relationships: Seq[FactionRelationship],
|
||||
update: TrustLevelUpdate
|
||||
) =>
|
||||
val existingRelationship =
|
||||
relationships
|
||||
.find(
|
||||
_.targetFactionId == update.targetFactionId
|
||||
)
|
||||
.getOrElse(
|
||||
FactionRelationship(
|
||||
targetFactionId = update.targetFactionId,
|
||||
relationshipLevel = FactionRelationship.RelationshipLevel.HOSTILE,
|
||||
trustValue = 0
|
||||
_.factionRelationships
|
||||
.modify { (relationships: Seq[FactionRelationship]) =>
|
||||
cf.trustLevelUpdates
|
||||
.foldLeft(relationships) {
|
||||
(
|
||||
relationships: Seq[FactionRelationship],
|
||||
update: TrustLevelUpdate
|
||||
) =>
|
||||
val existingRelationship =
|
||||
relationships
|
||||
.find(
|
||||
_.targetFactionId == update.targetFactionId
|
||||
)
|
||||
)
|
||||
val newValue = Math.min(
|
||||
trustMax,
|
||||
update.delta + existingRelationship.trustValue
|
||||
)
|
||||
relationships.filterNot(
|
||||
_.targetFactionId == update.targetFactionId
|
||||
) :+ existingRelationship.withTrustValue(newValue)
|
||||
}
|
||||
},
|
||||
.getOrElse(
|
||||
FactionRelationship(
|
||||
targetFactionId = update.targetFactionId,
|
||||
relationshipLevel =
|
||||
FactionRelationship.RelationshipLevel.HOSTILE,
|
||||
trustValue = 0
|
||||
)
|
||||
)
|
||||
val newValue = Math.min(
|
||||
trustMax,
|
||||
update.delta + existingRelationship.trustValue
|
||||
)
|
||||
relationships.filterNot(
|
||||
_.targetFactionId == update.targetFactionId
|
||||
) :+ existingRelationship.withTrustValue(newValue)
|
||||
}
|
||||
},
|
||||
_.lastActedProvinceIdThisRound.modify { lpid =>
|
||||
if cf.clearLastActedProvinceId then 0
|
||||
else cf.newLastActedProvinceId.getOrElse(lpid)
|
||||
@@ -813,53 +835,58 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
)
|
||||
|
||||
def applyNewBattle(battle: Option[ShardokBattle]): GameState =
|
||||
battle.map { b =>
|
||||
gameState.update(
|
||||
_.outstandingBattles.modify(_ :+ b),
|
||||
_.battleCounter.modify(_.max(b.battleIndex))
|
||||
)
|
||||
}
|
||||
battle
|
||||
.map { b =>
|
||||
gameState.update(
|
||||
_.outstandingBattles.modify(_ :+ b),
|
||||
_.battleCounter.modify(_.max(b.battleIndex))
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
|
||||
def applyResolvedBattle(shardokGameId: Option[ShardokGameId]): GameState =
|
||||
gameState
|
||||
.withOutstandingBattles(
|
||||
gameState.outstandingBattles.filterNot(batt => shardokGameId.contains(batt.shardokGameId))
|
||||
gameState.outstandingBattles.filterNot(batt =>
|
||||
shardokGameId.contains(batt.shardokGameId)
|
||||
)
|
||||
)
|
||||
|
||||
def applyNewSeed(newSeed: Option[Long]): GameState =
|
||||
newSeed.map(s => gameState.withRandomSeed(s)).getOrElse(gameState)
|
||||
newSeed.map { s => gameState.withRandomSeed(s) }.getOrElse(gameState)
|
||||
|
||||
def applyChronicleEntry(
|
||||
chronicleEntry: Option[ChronicleEntry]
|
||||
): GameState =
|
||||
chronicleEntry.map { ce =>
|
||||
gameState.update(
|
||||
_.chronicleEntries.modify { ces =>
|
||||
ces.indexWhere(_.date == ce.date) match {
|
||||
case -1 => ces :+ ce
|
||||
case idx => ces.updated(idx, ce)
|
||||
chronicleEntry
|
||||
.map { ce =>
|
||||
gameState.update(
|
||||
_.chronicleEntries.modify { ces =>
|
||||
ces.indexWhere(_.date == ce.date) match {
|
||||
case -1 => ces :+ ce
|
||||
case idx => ces.updated(idx, ce)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
|
||||
def applyCommandCountUpdate(actingFactionId: Option[FactionId]): GameState =
|
||||
actingFactionId.map { factionId =>
|
||||
val existingCount =
|
||||
gameState.factionCommandCounts.getOrElse(factionId, 0)
|
||||
gameState.update(
|
||||
_.factionCommandCounts(factionId) := existingCount + 1
|
||||
)
|
||||
}
|
||||
actingFactionId
|
||||
.map { factionId =>
|
||||
val existingCount =
|
||||
gameState.factionCommandCounts.getOrElse(factionId, 0)
|
||||
gameState.update(
|
||||
_.factionCommandCounts(factionId) := existingCount + 1
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
|
||||
override def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResult]
|
||||
): Vector[ActionWithResultingState] =
|
||||
): Vector[ActionWithResultingState] = {
|
||||
results
|
||||
.foldLeft((startingState, Vector.empty[ActionWithResultingState])) {
|
||||
case ((gameState, acc), result) =>
|
||||
@@ -869,7 +896,7 @@ class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultPro
|
||||
}
|
||||
}
|
||||
._2
|
||||
.toVector
|
||||
}.toVector
|
||||
|
||||
override def applyActionResult(
|
||||
startingState: GameState,
|
||||
|
||||
+7
-7
@@ -13,7 +13,8 @@ object ActionResultTApplierImpl {
|
||||
new ActionResultTApplierImpl(protoApplier)
|
||||
}
|
||||
|
||||
class ActionResultTApplierImpl(protoApplier: ActionResultProtoApplier) extends ActionResultTApplier {
|
||||
class ActionResultTApplierImpl(protoApplier: ActionResultProtoApplier)
|
||||
extends ActionResultTApplier {
|
||||
override def xpForStatBump(stat: Int): Int = protoApplier.xpForStatBump(stat)
|
||||
|
||||
override def applyActionResults(
|
||||
@@ -25,12 +26,11 @@ class ActionResultTApplierImpl(protoApplier: ActionResultProtoApplier) extends A
|
||||
results.map(ActionResultProtoConverter.toProto)
|
||||
)
|
||||
.zip(results)
|
||||
.map {
|
||||
case (actionWithResultingState, actionResult) =>
|
||||
ActionResultTWithResultingState(
|
||||
actionResult = actionResult,
|
||||
resultingState = actionWithResultingState.gameState
|
||||
)
|
||||
.map { case (actionWithResultingState, actionResult) =>
|
||||
ActionResultTWithResultingState(
|
||||
actionResult = actionResult,
|
||||
resultingState = actionWithResultingState.gameState
|
||||
)
|
||||
}
|
||||
|
||||
override def applyActionResult(
|
||||
|
||||
+18
-9
@@ -1,32 +1,41 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.AlmsAvailableCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.settings.{MaxAlmsFood, MinVigorForAlms}
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
|
||||
object AvailableAlmsCommandFactory extends AvailableCommandsFactoryForType {
|
||||
|
||||
def availableHeroIds(gs: GameState, pid: ProvinceId): Vector[HeroId] =
|
||||
gs.provinces(pid)
|
||||
gs.provinces(pid.value)
|
||||
.rulingFactionHeroIds
|
||||
.toVector
|
||||
.filter(hid => gs.heroes(hid).vigor >= MinVigorForAlms.doubleValue)
|
||||
.sortBy(hid => (!gs.heroes(hid).profession.isPaladin, -gs.heroes(hid).vigor))
|
||||
.map(HeroId(_))
|
||||
.filter(hid => gs.heroes(hid.value).vigor >= MinVigorForAlms.doubleValue)
|
||||
.sortBy(hid =>
|
||||
(
|
||||
!gs.heroes(hid.value).profession.isPaladin,
|
||||
-gs.heroes(hid.value).vigor
|
||||
)
|
||||
)
|
||||
|
||||
override def availableCommand(
|
||||
gameState: GameState,
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
): Option[AlmsAvailableCommand] = {
|
||||
val hids = availableHeroIds(gameState, provinceId)
|
||||
val province = gameState.provinces(provinceId)
|
||||
val hids = availableHeroIds(gameState, provinceId)
|
||||
val province = gameState.provinces(provinceId.value)
|
||||
|
||||
Option.when(hids.nonEmpty && province.food > 0) {
|
||||
AlmsAvailableCommand(
|
||||
availableHeroIds = hids,
|
||||
actingProvinceId = provinceId,
|
||||
foodAvailable = Math.min(MaxAlmsFood.intValue, gameState.provinces(provinceId).food)
|
||||
availableHeroIds = hids.map(_.value),
|
||||
actingProvinceId = provinceId.value,
|
||||
foodAvailable = Math.min(
|
||||
MaxAlmsFood.intValue,
|
||||
gameState.provinces(provinceId.value).food
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-8
@@ -1,16 +1,20 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{ApprehendOutlawAvailableCommand, ResidentOutlaw}
|
||||
import net.eagle0.eagle.api.available_command.{
|
||||
ApprehendOutlawAvailableCommand,
|
||||
ResidentOutlaw
|
||||
}
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.UNAFFILIATED_HERO_OUTLAW
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
import net.eagle0.eagle.library.settings.MinVigorForApprehendOutlaw
|
||||
import net.eagle0.eagle.library.util.view_filters.HeroViewFilter
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
object AvailableApprehendOutlawCommandFactory extends AvailableCommandsFactoryForType {
|
||||
object AvailableApprehendOutlawCommandFactory
|
||||
extends AvailableCommandsFactoryForType {
|
||||
|
||||
private def availableToCapture(province: Province): Vector[UnaffiliatedHero] =
|
||||
province.unaffiliatedHeroes
|
||||
@@ -31,13 +35,13 @@ object AvailableApprehendOutlawCommandFactory extends AvailableCommandsFactoryFo
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
): Option[ApprehendOutlawAvailableCommand] = {
|
||||
val province = gameState.provinces(provinceId)
|
||||
val outlaws = availableToCapture(province)
|
||||
val actors = availableActors(gameState, province)
|
||||
val province = gameState.provinces(provinceId.value)
|
||||
val outlaws = availableToCapture(province)
|
||||
val actors = availableActors(gameState, province)
|
||||
|
||||
Option.when(outlaws.nonEmpty && actors.nonEmpty) {
|
||||
ApprehendOutlawAvailableCommand(
|
||||
actingProvinceId = provinceId,
|
||||
actingProvinceId = provinceId.value,
|
||||
availableHeroIds = actors.map(_.id),
|
||||
outlaws = outlaws.map(outlaw =>
|
||||
ResidentOutlaw(
|
||||
|
||||
+12
-7
@@ -1,18 +1,22 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{ArmTroopsAvailableCommand, ArmamentCost}
|
||||
import net.eagle0.eagle.api.available_command.{
|
||||
ArmTroopsAvailableCommand,
|
||||
ArmamentCost
|
||||
}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.LegacyBattalionUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
object AvailableArmTroopsCommandFactory extends AvailableCommandsFactoryForType {
|
||||
object AvailableArmTroopsCommandFactory
|
||||
extends AvailableCommandsFactoryForType {
|
||||
override def availableCommand(
|
||||
gameState: GameState,
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
): Option[ArmTroopsAvailableCommand] = {
|
||||
val province = gameState.provinces(provinceId)
|
||||
val province = gameState.provinces(provinceId.value)
|
||||
|
||||
if !province.rulerIsTraveling then return None
|
||||
|
||||
@@ -52,10 +56,11 @@ object AvailableArmTroopsCommandFactory extends AvailableCommandsFactoryForType
|
||||
|
||||
Some(
|
||||
ArmTroopsAvailableCommand(
|
||||
actingProvinceId = provinceId,
|
||||
actingProvinceId = provinceId.value,
|
||||
availableBattalions = affordableBattalions.map(_.id),
|
||||
armamentCosts = armamentCosts,
|
||||
maxArmament = LegacyProvinceUtils.effectiveInfrastructure(province).floor.toInt,
|
||||
maxArmament =
|
||||
LegacyProvinceUtils.effectiveInfrastructure(province).floor.toInt,
|
||||
availableBattalionTypeIds = gameState.battalionTypes.map(_.typeId)
|
||||
)
|
||||
)
|
||||
|
||||
+48
-37
@@ -1,7 +1,6 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.common.MoreOption
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.AttackDecisionAvailableCommand
|
||||
import net.eagle0.eagle.api.command.util.army_stats.ArmyStats
|
||||
import net.eagle0.eagle.api.command.util.attack_decision_type.{
|
||||
@@ -15,11 +14,13 @@ import net.eagle0.eagle.common.tribute_amount.TributeAmount
|
||||
import net.eagle0.eagle.internal.army.{HostileArmyGroup, MovingArmy}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.util.{IncomingArmyUtils, ProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.{IncomingArmyUtils, ProvinceDistances}
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryForType {
|
||||
object AvailableAttackDecisionCommandFactory
|
||||
extends AvailableCommandsFactoryForType {
|
||||
|
||||
// We need to include the armies that have already withdrawn or advanced, in a consistent sort order,
|
||||
// so that the player doesn't learn what others have done by the shifts.
|
||||
@@ -27,7 +28,7 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
pid: ProvinceId,
|
||||
gs: GameState
|
||||
): Vector[MovingArmy] =
|
||||
gs.provinces(pid)
|
||||
gs.provinces(pid.value)
|
||||
.hostileArmies
|
||||
.flatMap(_.armies)
|
||||
.sortBy(ma => (ma.getArmy.factionId, ma.getArmy.units.head.heroId))
|
||||
@@ -38,9 +39,9 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
fid: FactionId,
|
||||
gs: GameState
|
||||
): Option[HostileArmyGroup] =
|
||||
gs.provinces(pid)
|
||||
gs.provinces(pid.value)
|
||||
.hostileArmies
|
||||
.find(_.factionId == fid)
|
||||
.find(ha => FactionId(ha.factionId) == fid)
|
||||
.filter(_.status.asMessage.sealedValue.isAwaitingDecision)
|
||||
|
||||
private def defendingArmyStats(
|
||||
@@ -48,7 +49,7 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
pid: ProvinceId,
|
||||
gs: GameState
|
||||
): Option[ArmyStats] = {
|
||||
val province = gs.provinces(pid)
|
||||
val province = gs.provinces(pid.value)
|
||||
|
||||
val heroCount = province.rulingFactionHeroIds.size + province.defendingArmy
|
||||
.map(_.units.size)
|
||||
@@ -71,8 +72,9 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
factionId = defenderFid,
|
||||
heroCount = heroCount,
|
||||
troopCount = troopCount,
|
||||
hostility = LegacyFactionUtils.hostilityStatus(defenderFid, attackerFid, gs),
|
||||
originProvinceId = pid
|
||||
hostility = LegacyFactionUtils
|
||||
.hostilityStatus(FactionId(defenderFid), attackerFid, gs),
|
||||
originProvinceId = pid.value
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +85,8 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
gs: GameState
|
||||
): Vector[ArmyStats] =
|
||||
relevantArmies(pid, gs).map(ia =>
|
||||
IncomingArmyUtils.stats(toFid, ia.getArmy, gs, ia.originProvince)
|
||||
IncomingArmyUtils
|
||||
.stats(toFid, ia.getArmy, gs, ProvinceId(ia.originProvince))
|
||||
) ++ defendingArmyStats(
|
||||
toFid,
|
||||
pid,
|
||||
@@ -110,9 +113,9 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
fid: FactionId
|
||||
): Option[AttackDecisionType] =
|
||||
Option.when(
|
||||
gs.provinces(pid).rulingFactionId.isEmpty ||
|
||||
gs.provinces(pid.value).rulingFactionId.isEmpty ||
|
||||
IncomingArmyUtils.isHostileProvince(
|
||||
gs.provinces(pid),
|
||||
gs.provinces(pid.value),
|
||||
fid,
|
||||
gs
|
||||
)
|
||||
@@ -124,9 +127,9 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
fid: FactionId
|
||||
): Option[AttackDecisionType] =
|
||||
Option.when(
|
||||
hasResourcesToDemand(gs.provinces(pid))
|
||||
hasResourcesToDemand(gs.provinces(pid.value))
|
||||
&& IncomingArmyUtils.isHostileProvince(
|
||||
gs.provinces(pid),
|
||||
gs.provinces(pid.value),
|
||||
fid,
|
||||
gs
|
||||
) && allArmiesCanFlee(gs, pid, fid)
|
||||
@@ -134,8 +137,8 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
DemandTributeDecision(
|
||||
Some(
|
||||
TributeAmount(
|
||||
food = gs.provinces(pid).food,
|
||||
gold = gs.provinces(pid).gold
|
||||
food = gs.provinces(pid.value).food,
|
||||
gold = gs.provinces(pid.value).gold
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -158,7 +161,7 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
MoreOption.flatWhen(
|
||||
!allArmiesCanFlee(gs, pid, fid) &&
|
||||
!IncomingArmyUtils.isHostileProvince(
|
||||
gs.provinces(pid),
|
||||
gs.provinces(pid.value),
|
||||
fid,
|
||||
gs
|
||||
)
|
||||
@@ -172,14 +175,19 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
.map(_.id)
|
||||
.toVector match {
|
||||
case items if items.isEmpty => None
|
||||
case emptyProvinces =>
|
||||
case emptyProvinceIds =>
|
||||
ProvinceDistances
|
||||
.closestProvinceOption(
|
||||
pid,
|
||||
emptyProvinces,
|
||||
p => gs.provinces(p).neighbors.map(_.provinceId).toVector
|
||||
emptyProvinceIds.map(ProvinceId(_)),
|
||||
p =>
|
||||
gs.provinces(p.value)
|
||||
.neighbors
|
||||
.map(_.provinceId)
|
||||
.map(ProvinceId(_))
|
||||
.toVector
|
||||
)
|
||||
.map(pwd => gs.provinces(pwd.provinceId))
|
||||
.map(pwd => gs.provinces(pwd.provinceId.value))
|
||||
}
|
||||
}
|
||||
.map { returnProvince =>
|
||||
@@ -211,27 +219,30 @@ object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryFor
|
||||
): Option[AttackDecisionAvailableCommand] = {
|
||||
internalRequire(
|
||||
IncomingArmyUtils.incomingArmiesAreMutuallyAllied(
|
||||
gameState.provinces(provinceId),
|
||||
gameState.provinces(provinceId.value),
|
||||
gameState
|
||||
),
|
||||
s"Incoming armies in attack decision phase are not mutually allied in province $provinceId"
|
||||
)
|
||||
|
||||
myDecidingHostileArmies(provinceId, factionId, gameState).flatMap { hostileArmyGroup =>
|
||||
decisionOptions(gameState, provinceId, factionId) match {
|
||||
case items if items.isEmpty => None
|
||||
case nonemptyDecisionOptions =>
|
||||
Some(
|
||||
AttackDecisionAvailableCommand(
|
||||
actingProvinceId = provinceId,
|
||||
armies = armyStats(factionId, provinceId, gameState),
|
||||
actingUnits = hostileArmyGroup.armies
|
||||
.flatMap(_.getArmy.units)
|
||||
.map(cu => ExpandedCombatUnitUtils.expandedCombatUnit(cu, gameState)),
|
||||
availableDecisions = nonemptyDecisionOptions
|
||||
myDecidingHostileArmies(provinceId, factionId, gameState).flatMap {
|
||||
hostileArmyGroup =>
|
||||
decisionOptions(gameState, provinceId, factionId) match {
|
||||
case items if items.isEmpty => None
|
||||
case nonemptyDecisionOptions =>
|
||||
Some(
|
||||
AttackDecisionAvailableCommand(
|
||||
actingProvinceId = provinceId.value,
|
||||
armies = armyStats(factionId, provinceId, gameState),
|
||||
actingUnits = hostileArmyGroup.armies
|
||||
.flatMap(_.getArmy.units)
|
||||
.map(cu =>
|
||||
ExpandedCombatUnitUtils.expandedCombatUnit(cu, gameState)
|
||||
),
|
||||
availableDecisions = nonemptyDecisionOptions
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+118
-101
@@ -1,9 +1,6 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import scala.collection.immutable.SortedMap
|
||||
|
||||
import net.eagle0.common.MoreOption
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.*
|
||||
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.{
|
||||
@@ -22,8 +19,11 @@ import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.actions.impl.command.AvailableCommandTypeMap
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.IncomingArmyUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
import scala.collection.immutable.SortedMap
|
||||
|
||||
object AvailableCommandsFactory {
|
||||
def shouldFollow(
|
||||
@@ -38,7 +38,7 @@ object AvailableCommandsFactory {
|
||||
case (_: OrganizeTroopsSelectedCommand, _) => false
|
||||
case (_: ExileVassalSelectedCommand, _) => false
|
||||
case (_: SwearBrotherhoodSelectedCommand, _) => false
|
||||
case (last, next) => AvailableCommandTypeMap.matches(next, last)
|
||||
case (last, next) => AvailableCommandTypeMap.matches(next, last)
|
||||
}
|
||||
|
||||
def suggestedProvinceId(
|
||||
@@ -74,20 +74,22 @@ object AvailableCommandsFactory {
|
||||
}
|
||||
|
||||
class AvailableCommandsFactory(
|
||||
private val travelingFactories: Vector[AvailableCommandsFactoryForType] = Vector(
|
||||
AvailableRecruitHeroesCommandFactory,
|
||||
AvailableDeclineQuestCommandsFactory,
|
||||
AvailableDivineCommandsFactory,
|
||||
AvailableArmTroopsCommandFactory,
|
||||
AvailableTradeCommandFactory,
|
||||
AvailableManagePrisonersCommandFactory,
|
||||
AvailableReturnCommandsFactory
|
||||
),
|
||||
private val handleRiotFactories: Vector[AvailableCommandsFactoryForType] = Vector(
|
||||
AvailableHandleRiotCrackDownCommandFactory,
|
||||
AvailableHandleRiotDoNothingCommandFactory,
|
||||
AvailableHandleRiotGiveCommandFactory
|
||||
),
|
||||
private val travelingFactories: Vector[AvailableCommandsFactoryForType] =
|
||||
Vector(
|
||||
AvailableRecruitHeroesCommandFactory,
|
||||
AvailableDeclineQuestCommandsFactory,
|
||||
AvailableDivineCommandsFactory,
|
||||
AvailableArmTroopsCommandFactory,
|
||||
AvailableTradeCommandFactory,
|
||||
AvailableManagePrisonersCommandFactory,
|
||||
AvailableReturnCommandsFactory
|
||||
),
|
||||
private val handleRiotFactories: Vector[AvailableCommandsFactoryForType] =
|
||||
Vector(
|
||||
AvailableHandleRiotCrackDownCommandFactory,
|
||||
AvailableHandleRiotDoNothingCommandFactory,
|
||||
AvailableHandleRiotGiveCommandFactory
|
||||
),
|
||||
private val freeForAllDecisionPhaseFactories: Vector[
|
||||
AvailableCommandsFactoryForType
|
||||
] = Vector(
|
||||
@@ -98,31 +100,33 @@ class AvailableCommandsFactory(
|
||||
] = Vector(
|
||||
AvailableAttackDecisionCommandFactory
|
||||
),
|
||||
private val defensePhaseFactories: Vector[AvailableCommandsFactoryForType] = Vector(
|
||||
AvailableResolveTributeCommandsFactory,
|
||||
AvailableDefendCommandsFactory
|
||||
),
|
||||
private val commandPhaseFactories: Vector[AvailableCommandsFactoryForType] = Vector(
|
||||
AvailableApprehendOutlawCommandFactory,
|
||||
AvailableControlWeatherCommandsFactory,
|
||||
AvailableDiplomacyCommandsFactory,
|
||||
AvailableExileVassalCommandFactory,
|
||||
AvailableFeastCommandFactory,
|
||||
AvailableHeroGiftCommandFactory,
|
||||
AvailableImproveCommandsFactory,
|
||||
AvailableMarchCommandFactory,
|
||||
AvailableIssueOrdersCommandFactory,
|
||||
AvailableOrganizeTroopsCommandsFactory,
|
||||
AvailableAlmsCommandFactory,
|
||||
AvailableReconCommandFactory,
|
||||
AvailableRestCommandsFactory,
|
||||
AvailableSendSuppliesCommandFactory,
|
||||
AvailableStartEpidemicCommandFactory,
|
||||
AvailableSuppressBeastsCommandFactory,
|
||||
AvailableSwearBrotherhoodCommandFactory,
|
||||
AvailableTrainCommandsFactory,
|
||||
AvailableTravelCommandsFactory
|
||||
)
|
||||
private val defensePhaseFactories: Vector[AvailableCommandsFactoryForType] =
|
||||
Vector(
|
||||
AvailableResolveTributeCommandsFactory,
|
||||
AvailableDefendCommandsFactory
|
||||
),
|
||||
private val commandPhaseFactories: Vector[AvailableCommandsFactoryForType] =
|
||||
Vector(
|
||||
AvailableApprehendOutlawCommandFactory,
|
||||
AvailableControlWeatherCommandsFactory,
|
||||
AvailableDiplomacyCommandsFactory,
|
||||
AvailableExileVassalCommandFactory,
|
||||
AvailableFeastCommandFactory,
|
||||
AvailableHeroGiftCommandFactory,
|
||||
AvailableImproveCommandsFactory,
|
||||
AvailableMarchCommandFactory,
|
||||
AvailableIssueOrdersCommandFactory,
|
||||
AvailableOrganizeTroopsCommandsFactory,
|
||||
AvailableAlmsCommandFactory,
|
||||
AvailableReconCommandFactory,
|
||||
AvailableRestCommandsFactory,
|
||||
AvailableSendSuppliesCommandFactory,
|
||||
AvailableStartEpidemicCommandFactory,
|
||||
AvailableSuppressBeastsCommandFactory,
|
||||
AvailableSwearBrotherhoodCommandFactory,
|
||||
AvailableTrainCommandsFactory,
|
||||
AvailableTravelCommandsFactory
|
||||
)
|
||||
) {
|
||||
private def allCommands(
|
||||
factories: Vector[AvailableCommandsFactoryForType],
|
||||
@@ -177,13 +181,13 @@ class AvailableCommandsFactory(
|
||||
pid: ProvinceId,
|
||||
commands: Vector[AvailableCommand]
|
||||
): Int =
|
||||
commands.zipWithIndex.find {
|
||||
case (ac, _) =>
|
||||
commands.zipWithIndex
|
||||
.find { case (ac, _) =>
|
||||
gs.provinces
|
||||
.get(pid)
|
||||
.map(_.lastCommand)
|
||||
.forall(last => AvailableCommandsFactory.shouldFollow(last, ac))
|
||||
}
|
||||
}
|
||||
.map(_._2)
|
||||
.getOrElse(0)
|
||||
|
||||
@@ -216,7 +220,8 @@ class AvailableCommandsFactory(
|
||||
gs = gs,
|
||||
pid = pid,
|
||||
commands =
|
||||
if gs.provinces(pid).rulerIsTraveling then allCommands(travelingFactories, gs, fid(gs, pid).get, pid)
|
||||
if gs.provinces(pid).rulerIsTraveling then
|
||||
allCommands(travelingFactories, gs, fid(gs, pid).get, pid)
|
||||
else
|
||||
allCommands(
|
||||
commandPhaseFactories,
|
||||
@@ -230,7 +235,8 @@ class AvailableCommandsFactory(
|
||||
gs: GameState,
|
||||
pid: ProvinceId
|
||||
): Boolean =
|
||||
if gs.provinces(pid).rulerIsTraveling then hasCommand(travelingFactories, gs, fid(gs, pid).get, pid)
|
||||
if gs.provinces(pid).rulerIsTraveling then
|
||||
hasCommand(travelingFactories, gs, fid(gs, pid).get, pid)
|
||||
else
|
||||
hasCommand(
|
||||
commandPhaseFactories,
|
||||
@@ -434,19 +440,27 @@ class AvailableCommandsFactory(
|
||||
gs: GameState,
|
||||
fid: FactionId
|
||||
): Boolean =
|
||||
factionLeaderProvinces(gs, fid).exists(p => defensePhaseCommandsForProvince(gs, p.id).isDefined)
|
||||
factionLeaderProvinces(gs, fid).exists(p =>
|
||||
defensePhaseCommandsForProvince(gs, p.id).isDefined
|
||||
)
|
||||
|
||||
def hasAvailableHandleRiotPhaseCommands(gs: GameState): Boolean =
|
||||
gs.factions.keys.exists(fid => handleRiotPhaseCommandsForFactionLeader(gs, fid).nonEmpty)
|
||||
gs.factions.keys.exists(fid =>
|
||||
handleRiotPhaseCommandsForFactionLeader(gs, fid).nonEmpty
|
||||
)
|
||||
|
||||
def hasAvailablePlayerCommandsPhaseCommands(gs: GameState): Boolean =
|
||||
gs.factions.keys.exists(fid => hasCommandsPhaseCommandsForPlayer(gs, fid))
|
||||
|
||||
def hasAvailableFreeForAllDecisionPhaseCommands(gs: GameState): Boolean =
|
||||
gs.factions.keys.exists(fid => freeForAllDecisionPhasePlayerCommands(gs, fid).nonEmpty)
|
||||
gs.factions.keys.exists(fid =>
|
||||
freeForAllDecisionPhasePlayerCommands(gs, fid).nonEmpty
|
||||
)
|
||||
|
||||
def hasAvailableAttackDecisionPhaseCommands(gs: GameState): Boolean =
|
||||
gs.factions.keys.exists(fid => attackDecisionPhasePlayerCommands(gs, fid).nonEmpty)
|
||||
gs.factions.keys.exists(fid =>
|
||||
attackDecisionPhasePlayerCommands(gs, fid).nonEmpty
|
||||
)
|
||||
|
||||
def hasAvailablePlayerDefenseCommands(gs: GameState): Boolean =
|
||||
gs.factions.keys.exists(fid => hasPlayerDefenseCommandsForPlayer(gs, fid))
|
||||
@@ -457,30 +471,30 @@ class AvailableCommandsFactory(
|
||||
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
|
||||
(
|
||||
for {
|
||||
faction <- gs.factions.get(fid)
|
||||
diplomacyCommand <- AvailableResolveInvitationCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
.orElse(
|
||||
AvailableResolveTruceOfferCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
)
|
||||
.orElse(
|
||||
AvailableResolveRansomOfferCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
)
|
||||
.orElse(
|
||||
AvailableResolveAllianceOfferCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
)
|
||||
.orElse(
|
||||
AvailableResolveBreakAllianceCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
)
|
||||
faction <- gs.factions.get(fid)
|
||||
diplomacyCommand <- AvailableResolveInvitationCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
.orElse(
|
||||
AvailableResolveTruceOfferCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
)
|
||||
.orElse(
|
||||
AvailableResolveRansomOfferCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
)
|
||||
.orElse(
|
||||
AvailableResolveAllianceOfferCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
)
|
||||
.orElse(
|
||||
AvailableResolveBreakAllianceCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
)
|
||||
oneProvinceAvailableCommand <- optionalOneProvinceAvailableCommands(
|
||||
gs = gs,
|
||||
0,
|
||||
Vector(diplomacyCommand)
|
||||
)
|
||||
gs = gs,
|
||||
0,
|
||||
Vector(diplomacyCommand)
|
||||
)
|
||||
} yield SortedMap(0 -> oneProvinceAvailableCommand)
|
||||
).getOrElse(SortedMap.empty)
|
||||
|
||||
@@ -490,14 +504,14 @@ class AvailableCommandsFactory(
|
||||
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
|
||||
(
|
||||
for {
|
||||
faction <- gs.factions.get(fid)
|
||||
pleaseRecruitMeCommand <- AvailablePleaseRecruitMeCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
faction <- gs.factions.get(fid)
|
||||
pleaseRecruitMeCommand <- AvailablePleaseRecruitMeCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
oneProvinceAvailableCommand <- optionalOneProvinceAvailableCommands(
|
||||
gs = gs,
|
||||
0,
|
||||
Vector(pleaseRecruitMeCommand)
|
||||
)
|
||||
gs = gs,
|
||||
0,
|
||||
Vector(pleaseRecruitMeCommand)
|
||||
)
|
||||
} yield SortedMap(0 -> oneProvinceAvailableCommand)
|
||||
).getOrElse(SortedMap.empty)
|
||||
|
||||
@@ -513,7 +527,9 @@ class AvailableCommandsFactory(
|
||||
LegacyFactionUtils
|
||||
.provinces(fid, gs)
|
||||
.map(_.id)
|
||||
.flatMap(pid => availableAftermathCommandsForProvince(gs, pid).map(pid -> _))*
|
||||
.flatMap(pid =>
|
||||
availableAftermathCommandsForProvince(gs, pid).map(pid -> _)
|
||||
)*
|
||||
)
|
||||
|
||||
def availablePlayerCommands(
|
||||
@@ -521,41 +537,42 @@ class AvailableCommandsFactory(
|
||||
fid: FactionId
|
||||
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
|
||||
gs.currentPhase match {
|
||||
case HANDLE_RIOT => handleRiotPhaseCommandsForFactionLeader(gs, fid)
|
||||
case PLEASE_RECRUIT_ME => pleaseRecruitMePhasePlayerCommands(gs, fid)
|
||||
case PLAYER_COMMANDS => commandsPhasePlayerCommands(gs, fid)
|
||||
case HANDLE_RIOT => handleRiotPhaseCommandsForFactionLeader(gs, fid)
|
||||
case PLEASE_RECRUIT_ME => pleaseRecruitMePhasePlayerCommands(gs, fid)
|
||||
case PLAYER_COMMANDS => commandsPhasePlayerCommands(gs, fid)
|
||||
case FREE_FOR_ALL_DECISION =>
|
||||
freeForAllDecisionPhasePlayerCommands(gs, fid)
|
||||
case ATTACK_DECISION => attackDecisionPhasePlayerCommands(gs, fid)
|
||||
case DEFENSE_DECISION => defensePhasePlayerCommands(gs, fid)
|
||||
case BATTLE_AFTERMATH => availableAftermathPhasePlayerCommands(gs, fid)
|
||||
case DIPLOMACY_RESOLUTION =>
|
||||
case ATTACK_DECISION => attackDecisionPhasePlayerCommands(gs, fid)
|
||||
case DEFENSE_DECISION => defensePhasePlayerCommands(gs, fid)
|
||||
case BATTLE_AFTERMATH => availableAftermathPhasePlayerCommands(gs, fid)
|
||||
case DIPLOMACY_RESOLUTION =>
|
||||
diplomacyResolutionPhasePlayerCommands(gs, fid)
|
||||
case _ => SortedMap.empty[ProvinceId, OneProvinceAvailableCommands]
|
||||
case _ => SortedMap.empty[ProvinceId, OneProvinceAvailableCommands]
|
||||
}
|
||||
|
||||
def hasAvailablePlayerCommands(gs: GameState): Boolean =
|
||||
def hasAvailablePlayerCommands(gs: GameState): Boolean = {
|
||||
gs.currentPhase match {
|
||||
case DIPLOMACY_RESOLUTION =>
|
||||
case DIPLOMACY_RESOLUTION =>
|
||||
hasAvailableDiplomacyResolutionPhaseCommands(gs)
|
||||
case HANDLE_RIOT =>
|
||||
case HANDLE_RIOT =>
|
||||
hasAvailableHandleRiotPhaseCommands(gs)
|
||||
case PLEASE_RECRUIT_ME =>
|
||||
case PLEASE_RECRUIT_ME =>
|
||||
hasAvailablePleaseRecruitMePhaseCommands(gs)
|
||||
case DEFENSE_DECISION =>
|
||||
case DEFENSE_DECISION =>
|
||||
hasAvailablePlayerDefenseCommands(gs)
|
||||
case ATTACK_DECISION =>
|
||||
case ATTACK_DECISION =>
|
||||
hasAvailableAttackDecisionPhaseCommands(gs)
|
||||
case FREE_FOR_ALL_DECISION =>
|
||||
hasAvailableFreeForAllDecisionPhaseCommands(gs)
|
||||
case PLAYER_COMMANDS =>
|
||||
case PLAYER_COMMANDS =>
|
||||
gs.factions.keys.exists { fid =>
|
||||
hasCommandsPhaseCommandsForPlayer(gs, fid)
|
||||
}
|
||||
case BATTLE_AFTERMATH =>
|
||||
case BATTLE_AFTERMATH =>
|
||||
gs.factions.keys.exists { fid =>
|
||||
availableAftermathPhasePlayerCommands(gs, fid).nonEmpty
|
||||
}
|
||||
case _ => false
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
trait AvailableCommandsFactoryForType {
|
||||
def availableCommand(
|
||||
|
||||
+11
-9
@@ -1,6 +1,5 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.ControlWeatherAvailableCommand
|
||||
import net.eagle0.eagle.api.available_command.ControlWeatherAvailableCommand.TargetProvinceOptions
|
||||
import net.eagle0.eagle.api.command.util.control_weather_type.ControlWeatherType.{
|
||||
@@ -13,8 +12,10 @@ import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.MinVigorForControlWeather
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
object AvailableControlWeatherCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
object AvailableControlWeatherCommandsFactory
|
||||
extends AvailableCommandsFactoryForType {
|
||||
|
||||
private def makeControlWeatherCommand(
|
||||
gameState: GameState,
|
||||
@@ -33,10 +34,12 @@ object AvailableControlWeatherCommandsFactory extends AvailableCommandsFactoryFo
|
||||
TargetProvinceOptions(
|
||||
provinceId = pid,
|
||||
controlWeatherTypes = (
|
||||
if LegacyProvinceUtils.hasBlizzard(gameState.provinces(pid)) then Vector(CONTROL_WEATHER_END_BLIZZARD)
|
||||
if LegacyProvinceUtils.hasBlizzard(gameState.provinces(pid)) then
|
||||
Vector(CONTROL_WEATHER_END_BLIZZARD)
|
||||
else Vector(CONTROL_WEATHER_START_BLIZZARD)
|
||||
) ++ (
|
||||
if LegacyProvinceUtils.hasDrought(gameState.provinces(pid)) then Vector(CONTROL_WEATHER_END_DROUGHT)
|
||||
if LegacyProvinceUtils.hasDrought(gameState.provinces(pid)) then
|
||||
Vector(CONTROL_WEATHER_END_DROUGHT)
|
||||
else Vector(CONTROL_WEATHER_START_DROUGHT)
|
||||
)
|
||||
)
|
||||
@@ -57,9 +60,8 @@ object AvailableControlWeatherCommandsFactory extends AvailableCommandsFactoryFo
|
||||
gameState: GameState,
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
): Option[ControlWeatherAvailableCommand] =
|
||||
makeControlWeatherCommand(
|
||||
gameState,
|
||||
gameState.provinces(provinceId)
|
||||
)
|
||||
): Option[ControlWeatherAvailableCommand] = makeControlWeatherCommand(
|
||||
gameState,
|
||||
gameState.provinces(provinceId.value)
|
||||
)
|
||||
}
|
||||
|
||||
+7
-4
@@ -8,13 +8,14 @@ import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.availability.ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
|
||||
object AvailableDeclineQuestCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
object AvailableDeclineQuestCommandsFactory
|
||||
extends AvailableCommandsFactoryForType {
|
||||
override def availableCommand(
|
||||
gameState: GameState,
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
): Option[DeclineQuestAvailableCommand] = {
|
||||
val province = gameState.provinces(provinceId)
|
||||
val province = gameState.provinces(provinceId.value)
|
||||
|
||||
MoreOption.flatWhen(
|
||||
province.rulerIsTraveling
|
||||
@@ -25,8 +26,10 @@ object AvailableDeclineQuestCommandsFactory extends AvailableCommandsFactoryForT
|
||||
|
||||
Option.when(withQuests.nonEmpty) {
|
||||
DeclineQuestAvailableCommand(
|
||||
actingProvinceId = provinceId,
|
||||
declinableHeroes = withQuests.map(uh => expandedUnaffiliatedHero(gs = gameState, uh = uh))
|
||||
actingProvinceId = provinceId.value,
|
||||
declinableHeroes = withQuests.map(uh =>
|
||||
expandedUnaffiliatedHero(gs = gameState, uh = uh)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+27
-15
@@ -1,15 +1,21 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.DefendAvailableCommand
|
||||
import net.eagle0.eagle.api.command.util.battalion_with_food_cost.BattalionWithFoodCost
|
||||
import net.eagle0.eagle.internal.army.Attacking
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.{MaxCombatUnitCountPerSide, MinVigorForDefend}
|
||||
import net.eagle0.eagle.library.util.{BattalionSuitability, LegacyBattalionUtils}
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
MaxCombatUnitCountPerSide,
|
||||
MinVigorForDefend
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CombatUnitSelector
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.{
|
||||
BattalionSuitability,
|
||||
LegacyBattalionUtils
|
||||
}
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
private def sortedFleeProvinces(
|
||||
@@ -18,7 +24,7 @@ object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
): Vector[ProvinceId] =
|
||||
if LegacyFactionUtils
|
||||
.provinces(
|
||||
factionId = from.rulingFactionId.get,
|
||||
factionId = FactionId(from.rulingFactionId.get),
|
||||
gameState = gs
|
||||
)
|
||||
.map(_.id)
|
||||
@@ -37,6 +43,7 @@ object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
)
|
||||
.toVector
|
||||
.map(_.id)
|
||||
.map(ProvinceId(_))
|
||||
|
||||
private def makeDefendCommand(
|
||||
gameState: GameState,
|
||||
@@ -46,7 +53,7 @@ object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
army.status match {
|
||||
case Attacking(_) =>
|
||||
true
|
||||
case _ => false
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +65,7 @@ object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
.filter(_.vigor >= MinVigorForDefend.doubleValue)
|
||||
if availableHeroes.isEmpty then return None
|
||||
|
||||
val availableBattalions = province.battalionIds.map(gameState.battalions)
|
||||
val availableBattalions = province.battalionIds.map(gameState.battalions)
|
||||
val availableBattalionsWithCosts = availableBattalions.map(batt =>
|
||||
BattalionWithFoodCost(
|
||||
battalionId = batt.id,
|
||||
@@ -70,12 +77,13 @@ object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
)
|
||||
|
||||
val recommendedUnits = CombatUnitSelector.selectedCombatBattalions(
|
||||
desiredCount = availableHeroes.size.min(MaxCombatUnitCountPerSide.intValue),
|
||||
desiredCount =
|
||||
availableHeroes.size.min(MaxCombatUnitCountPerSide.intValue),
|
||||
heroes = availableHeroes.toVector,
|
||||
battalions = availableBattalions.toVector,
|
||||
battalionTypes = gameState.battalionTypes.toVector
|
||||
)
|
||||
val hostileUnits = hostileArmies
|
||||
val hostileUnits = hostileArmies
|
||||
.flatMap(_.armies)
|
||||
.flatMap(_.army)
|
||||
.flatMap(_.units)
|
||||
@@ -86,13 +94,17 @@ object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
.toVector
|
||||
.map(_.id),
|
||||
actingProvinceId = province.id,
|
||||
suitableBattalionsForHeroes = BattalionSuitability.suitableBattalionsForHeroes(
|
||||
hs = availableHeroes.toVector,
|
||||
bs = availableBattalions.toVector,
|
||||
bts = gameState.battalionTypes.toVector
|
||||
),
|
||||
suitableBattalionsForHeroes = BattalionSuitability
|
||||
.suitableBattalionsForHeroes(
|
||||
hs = availableHeroes.toVector,
|
||||
bs = availableBattalions.toVector,
|
||||
bts = gameState.battalionTypes.toVector
|
||||
)
|
||||
.map { case (hid, suitability) => (hid.value, suitability) },
|
||||
availableBattalions = availableBattalionsWithCosts,
|
||||
availableFleeProvinceIds = sortedFleeProvinces(from = province, gs = gameState),
|
||||
availableFleeProvinceIds =
|
||||
sortedFleeProvinces(from = province, gs = gameState)
|
||||
.map(_.value),
|
||||
recommendedUnits = recommendedUnits,
|
||||
hostileFactionIds = hostileArmies.toVector.map(_.factionId).distinct,
|
||||
hostileHeroCount = hostileUnits.size,
|
||||
@@ -114,6 +126,6 @@ object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
): Option[DefendAvailableCommand] =
|
||||
makeDefendCommand(
|
||||
gameState,
|
||||
gameState.provinces(provinceId)
|
||||
gameState.provinces(provinceId.value)
|
||||
)
|
||||
}
|
||||
|
||||
+97
-81
@@ -1,5 +1,4 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.DiplomacyAvailableCommand
|
||||
import net.eagle0.eagle.api.command.util.diplomacy_option.{
|
||||
AllianceOption,
|
||||
@@ -28,20 +27,28 @@ import net.eagle0.eagle.library.settings.{
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.ProvinceDistances
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
|
||||
object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
object AvailableDiplomacyCommandsFactory
|
||||
extends AvailableCommandsFactoryForType {
|
||||
|
||||
def availableHeroIds(gs: GameState, pid: ProvinceId): Vector[HeroId] =
|
||||
gs.provinces(pid)
|
||||
gs.provinces(pid.value)
|
||||
.rulingFactionHeroIds
|
||||
.filter(hid => gs.heroes(hid).vigor >= MinVigorForDiplomacy.doubleValue)
|
||||
.map(HeroId(_))
|
||||
.toVector
|
||||
|
||||
def recommendedHeroId(gs: GameState, availableHids: Seq[HeroId]): HeroId =
|
||||
availableHids
|
||||
.sortBy(hid => (LegacyHeroUtils.fatigue(gs.heroes(hid)), -gs.heroes(hid).vigor)) match {
|
||||
.sortBy(hid =>
|
||||
(
|
||||
LegacyHeroUtils.fatigue(gs.heroes(hid.value)),
|
||||
-gs.heroes(hid.value).vigor
|
||||
)
|
||||
) match {
|
||||
case sortedHeroes =>
|
||||
sortedHeroes
|
||||
.find(hid => !LegacyFactionUtils.isFactionHead(hid, gs))
|
||||
@@ -58,19 +65,19 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
targetFid: FactionId
|
||||
): Option[DiplomacyOption] =
|
||||
Option.when(
|
||||
gs.provinces(pid).gold >= TruceGoldCost.intValue
|
||||
gs.provinces(pid.value).gold >= TruceGoldCost.intValue
|
||||
&& LegacyProvinceUtils.ruledByFactionLeader(
|
||||
province = gs.provinces(pid),
|
||||
province = gs.provinces(pid.value),
|
||||
gameState = gs
|
||||
)
|
||||
&& !LegacyFactionUtils.hasAlliance(
|
||||
fid1 = gs.provinces(pid).rulingFactionId.get,
|
||||
fid1 = FactionId(gs.provinces(pid.value).rulingFactionId.get),
|
||||
fid2 = targetFid,
|
||||
gs = gs
|
||||
)
|
||||
)(
|
||||
TruceOption(
|
||||
targetFactionId = targetFid,
|
||||
targetFactionId = targetFid.value,
|
||||
goldCost = TruceGoldCost.intValue
|
||||
)
|
||||
)
|
||||
@@ -82,15 +89,15 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
targetFid: FactionId
|
||||
): Option[DiplomacyOption] =
|
||||
Option.when(
|
||||
gs.provinces(pid).gold >= AllianceGoldCost.intValue
|
||||
gs.provinces(pid.value).gold >= AllianceGoldCost.intValue
|
||||
&& LegacyProvinceUtils.ruledByFactionLeader(
|
||||
province = gs.provinces(pid),
|
||||
province = gs.provinces(pid.value),
|
||||
gameState = gs
|
||||
) && !LegacyFactionUtils
|
||||
.hasAlliance(fid1 = actingFid, fid2 = targetFid, gs = gs)
|
||||
)(
|
||||
AllianceOption(
|
||||
targetFactionId = targetFid,
|
||||
targetFactionId = targetFid.value,
|
||||
goldCost = AllianceGoldCost.intValue
|
||||
)
|
||||
)
|
||||
@@ -102,15 +109,15 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
targetFid: FactionId
|
||||
): Option[DiplomacyOption] =
|
||||
Option.when(
|
||||
gs.provinces(pid).gold >= BreakAllianceGoldCost.intValue
|
||||
gs.provinces(pid.value).gold >= BreakAllianceGoldCost.intValue
|
||||
&& LegacyProvinceUtils.ruledByFactionLeader(
|
||||
province = gs.provinces(pid),
|
||||
province = gs.provinces(pid.value),
|
||||
gameState = gs
|
||||
) && LegacyFactionUtils
|
||||
.hasAlliance(fid1 = actingFid, fid2 = targetFid, gs = gs)
|
||||
)(
|
||||
BreakAllianceOption(
|
||||
targetFactionId = targetFid,
|
||||
targetFactionId = targetFid.value,
|
||||
goldCost = BreakAllianceGoldCost.intValue
|
||||
)
|
||||
)
|
||||
@@ -120,36 +127,39 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
pid: ProvinceId,
|
||||
actingFid: FactionId,
|
||||
targetFid: FactionId
|
||||
): Boolean =
|
||||
LegacyFactionUtils.provinces(targetFid, gs).headOption.exists { targetProvince =>
|
||||
val eligibleNeighbors: ProvinceId => Vector[ProvinceId] =
|
||||
p =>
|
||||
gs.provinces(p)
|
||||
.neighbors
|
||||
.map(_.provinceId)
|
||||
.map(gs.provinces)
|
||||
.filter(neighborProvince =>
|
||||
// inviting across empty province
|
||||
neighborProvince.rulingFactionId.isEmpty ||
|
||||
// inviting across your own province
|
||||
neighborProvince.rulingFactionId.contains(actingFid) ||
|
||||
// inviting across the target's province
|
||||
neighborProvince.rulingFactionId.contains(targetFid) ||
|
||||
// inviting across your ally's province
|
||||
LegacyFactionUtils
|
||||
.hasAlliance(
|
||||
neighborProvince.rulingFactionId.get,
|
||||
actingFid,
|
||||
gs
|
||||
)
|
||||
)
|
||||
.map(_.id)
|
||||
.toVector
|
||||
): Boolean = {
|
||||
LegacyFactionUtils.provinces(targetFid, gs).headOption.exists {
|
||||
targetProvince =>
|
||||
val eligibleNeighbors: ProvinceId => Vector[ProvinceId] =
|
||||
p =>
|
||||
gs.provinces(p.value)
|
||||
.neighbors
|
||||
.map(_.provinceId)
|
||||
.map(gs.provinces)
|
||||
.filter(neighborProvince =>
|
||||
// inviting across empty province
|
||||
neighborProvince.rulingFactionId.isEmpty ||
|
||||
// inviting across your own province
|
||||
neighborProvince.rulingFactionId.contains(actingFid) ||
|
||||
// inviting across the target's province
|
||||
neighborProvince.rulingFactionId.contains(targetFid) ||
|
||||
// inviting across your ally's province
|
||||
LegacyFactionUtils
|
||||
.hasAlliance(
|
||||
FactionId(neighborProvince.rulingFactionId.get),
|
||||
actingFid,
|
||||
gs
|
||||
)
|
||||
)
|
||||
.map(_.id)
|
||||
.map(ProvinceId(_))
|
||||
.toVector
|
||||
|
||||
ProvinceDistances
|
||||
.distance(pid, targetProvince.id, eligibleNeighbors)
|
||||
.isDefined
|
||||
ProvinceDistances
|
||||
.distance(pid, ProvinceId(targetProvince.id), eligibleNeighbors)
|
||||
.isDefined
|
||||
}
|
||||
}
|
||||
|
||||
private def inviteOptionForTargetFaction(
|
||||
gs: GameState,
|
||||
@@ -158,8 +168,10 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
targetFid: FactionId
|
||||
): Option[DiplomacyOption] =
|
||||
Option.when(
|
||||
gs.currentRoundId >= gs.factions(actingFid).earliestRoundForInvitation &&
|
||||
gs.provinces(pid).gold >= InviteGoldCost.intValue
|
||||
gs.currentRoundId >= gs
|
||||
.factions(actingFid.value)
|
||||
.earliestRoundForInvitation &&
|
||||
gs.provinces(pid.value).gold >= InviteGoldCost.intValue
|
||||
&& LegacyFactionUtils
|
||||
.provinceCount(
|
||||
targetFid,
|
||||
@@ -168,13 +180,13 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
&& LegacyFactionUtils.provinceCount(actingFid, gs) > LegacyFactionUtils
|
||||
.provinceCount(targetFid, gs)
|
||||
&& LegacyProvinceUtils.ruledByFactionLeader(
|
||||
province = gs.provinces(pid),
|
||||
province = gs.provinces(pid.value),
|
||||
gameState = gs
|
||||
)
|
||||
&& hasClearPath(gs, pid, actingFid, targetFid)
|
||||
)(
|
||||
InvitationOption(
|
||||
targetFactionId = targetFid,
|
||||
targetFactionId = targetFid.value,
|
||||
goldCost = InviteGoldCost.intValue
|
||||
)
|
||||
)
|
||||
@@ -185,9 +197,9 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
actingFid: FactionId,
|
||||
targetFid: FactionId
|
||||
): Vector[DiplomacyOption] = {
|
||||
val prisonersAvailableToOffer = availablePrisoners(gs.provinces(pid))
|
||||
val hostagesAvailableToOffer = availableHostages(gs, pid)
|
||||
val goldAvailableToOffer = gs.provinces(pid).gold
|
||||
val prisonersAvailableToOffer = availablePrisoners(gs.provinces(pid.value))
|
||||
val hostagesAvailableToOffer = availableHostages(gs, pid)
|
||||
val goldAvailableToOffer = gs.provinces(pid.value).gold
|
||||
|
||||
if prisonersAvailableToOffer.nonEmpty || hostagesAvailableToOffer.nonEmpty || goldAvailableToOffer > 0
|
||||
then
|
||||
@@ -195,19 +207,20 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
gs,
|
||||
factionId = actingFid,
|
||||
targetFactionId = targetFid
|
||||
).map { prisonerToBeRansomed =>
|
||||
RansomOfferOption(
|
||||
targetFactionId = targetFid,
|
||||
ransomOffer = Some(
|
||||
RansomOfferDetails(
|
||||
prisonerToBeRansomed = Some(prisonerToBeRansomed),
|
||||
prisonersOffered = prisonersAvailableToOffer,
|
||||
hostagesOffered = hostagesAvailableToOffer,
|
||||
goldOffered = goldAvailableToOffer
|
||||
)
|
||||
.map { prisonerToBeRansomed =>
|
||||
RansomOfferOption(
|
||||
targetFactionId = targetFid.value,
|
||||
ransomOffer = Some(
|
||||
RansomOfferDetails(
|
||||
prisonerToBeRansomed = Some(prisonerToBeRansomed),
|
||||
prisonersOffered = prisonersAvailableToOffer,
|
||||
hostagesOffered = hostagesAvailableToOffer,
|
||||
goldOffered = goldAvailableToOffer
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
else Vector()
|
||||
end if
|
||||
}
|
||||
@@ -250,23 +263,24 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
gameState: GameState
|
||||
): Boolean =
|
||||
gameState
|
||||
.factions(toFid)
|
||||
.factions(toFid.value)
|
||||
.incomingDiplomacyOffers
|
||||
.exists(_.originatingFactionId == fromFid)
|
||||
.exists(dof => FactionId(dof.originatingFactionId) == fromFid)
|
||||
|
||||
override def availableCommand(
|
||||
gameState: GameState,
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
): Option[DiplomacyAvailableCommand] =
|
||||
): Option[DiplomacyAvailableCommand] = {
|
||||
if availableHeroIds(gameState, provinceId).isEmpty then None
|
||||
else if gameState.provinces(provinceId).rulingFactionHeroIds.size < 2 then None
|
||||
else if gameState.provinces(provinceId.value).rulingFactionHeroIds.size < 2
|
||||
then None
|
||||
else {
|
||||
gameState.factions.keys
|
||||
.filterNot(_ == factionId)
|
||||
.filterNot(fid => FactionId(fid) == factionId)
|
||||
.filterNot(fid =>
|
||||
hasIncomingDiplomacyOffer(
|
||||
toFid = fid,
|
||||
toFid = FactionId(fid),
|
||||
fromFid = factionId,
|
||||
gameState = gameState
|
||||
)
|
||||
@@ -276,13 +290,13 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
gs = gameState,
|
||||
pid = provinceId,
|
||||
actingFid = factionId,
|
||||
targetFid = targetFid
|
||||
targetFid = FactionId(targetFid)
|
||||
)
|
||||
)
|
||||
.toVector
|
||||
.sortBy(opt =>
|
||||
LegacyFactionUtils.sortKey(gameState.factions(opt match {
|
||||
case TruceOption(targetFactionId, _, _ /* unknownFieldSet */ ) =>
|
||||
case TruceOption(targetFactionId, _, _ /* unknownFieldSet */ ) =>
|
||||
targetFactionId
|
||||
case InvitationOption(
|
||||
targetFactionId,
|
||||
@@ -304,7 +318,7 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
targetFactionId
|
||||
case DiplomacyOption.Empty => ???
|
||||
case DiplomacyOption.Empty => ???
|
||||
}))
|
||||
) match {
|
||||
case items if items.isEmpty => None
|
||||
@@ -313,14 +327,15 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
|
||||
Some(
|
||||
DiplomacyAvailableCommand(
|
||||
availableHeroIds = heroIds,
|
||||
availableHeroIds = heroIds.map(_.value),
|
||||
options = options,
|
||||
actingProvinceId = provinceId,
|
||||
recommendedHeroId = recommendedHeroId(gameState, heroIds)
|
||||
actingProvinceId = provinceId.value,
|
||||
recommendedHeroId = recommendedHeroId(gameState, heroIds).value
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def availablePrisoners(
|
||||
province: Province
|
||||
@@ -341,14 +356,14 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
provinceId: ProvinceId
|
||||
): Vector[HostageOfferedInExchange] =
|
||||
gameState
|
||||
.provinces(provinceId)
|
||||
.provinces(provinceId.value)
|
||||
.rulingFactionHeroIds
|
||||
.map(heroId => gameState.heroes(heroId))
|
||||
.filter(_.vigor >= MinVigorForDiplomacy.doubleValue)
|
||||
.map(hero =>
|
||||
HostageOfferedInExchange(
|
||||
heroId = hero.id,
|
||||
provinceIdWithHero = provinceId
|
||||
provinceIdWithHero = provinceId.value
|
||||
)
|
||||
)
|
||||
.toVector
|
||||
@@ -357,18 +372,19 @@ object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType
|
||||
gameState: GameState,
|
||||
factionId: FactionId,
|
||||
targetFactionId: FactionId
|
||||
): Vector[PrisonerToBeRansomed] =
|
||||
): Vector[PrisonerToBeRansomed] = {
|
||||
for {
|
||||
province <- gameState.provinces.values
|
||||
.filter(_.rulingFactionId.contains(targetFactionId))
|
||||
.toVector
|
||||
.filter(_.rulingFactionId.contains(targetFactionId))
|
||||
.toVector
|
||||
prisoner <- province.unaffiliatedHeroes.filter(
|
||||
_.`type` == UNAFFILIATED_HERO_PRISONER
|
||||
)
|
||||
_.`type` == UNAFFILIATED_HERO_PRISONER
|
||||
)
|
||||
if prisoner.lastFaction.contains(factionId)
|
||||
if LegacyFactionUtils.isFactionLeader(prisoner.heroId, gameState)
|
||||
if LegacyFactionUtils.isFactionLeader(HeroId(prisoner.heroId), gameState)
|
||||
} yield PrisonerToBeRansomed(
|
||||
prisonerHeroId = prisoner.heroId,
|
||||
provinceIdForPrisoner = province.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,5 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
import net.eagle0.common.MoreOption
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.DivineAvailableCommand
|
||||
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.RECRUITMENT_STATUS_NOT_DIVINED
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
@@ -8,6 +7,7 @@ import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.actions.availability.ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero
|
||||
import net.eagle0.eagle.library.settings.GoldCostForDivine
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
object AvailableDivineCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
private def effectiveCost(province: Province): Int =
|
||||
|
||||
+3
-2
@@ -1,13 +1,14 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.ExileVassalAvailableCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
|
||||
object AvailableExileVassalCommandFactory extends AvailableCommandsFactoryForType {
|
||||
object AvailableExileVassalCommandFactory
|
||||
extends AvailableCommandsFactoryForType {
|
||||
|
||||
def exilableHeroIds(
|
||||
gameState: GameState,
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.FeastAvailableCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.FeastGoldCostPerHero
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
|
||||
object AvailableFeastCommandFactory extends AvailableCommandsFactoryForType {
|
||||
|
||||
@@ -17,7 +17,7 @@ object AvailableFeastCommandFactory extends AvailableCommandsFactoryForType {
|
||||
.map(gameState.heroes)
|
||||
.exists(h =>
|
||||
h.vigor < h.constitution || LegacyHeroUtils
|
||||
.effectiveLoyalty(h.id, gameState) < 100.0
|
||||
.effectiveLoyalty(HeroId(h.id), gameState) < 100.0
|
||||
)
|
||||
|
||||
def feastGoldCost(province: Province): Int =
|
||||
@@ -28,7 +28,7 @@ object AvailableFeastCommandFactory extends AvailableCommandsFactoryForType {
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
): Option[FeastAvailableCommand] = {
|
||||
val province = gameState.provinces(provinceId)
|
||||
val province = gameState.provinces(provinceId.value)
|
||||
val goldCost = feastGoldCost(province)
|
||||
|
||||
Option.when(
|
||||
@@ -36,7 +36,7 @@ object AvailableFeastCommandFactory extends AvailableCommandsFactoryForType {
|
||||
) {
|
||||
FeastAvailableCommand(
|
||||
goldCost = goldCost,
|
||||
actingProvinceId = provinceId
|
||||
actingProvinceId = provinceId.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user