Compare commits

..
Author SHA1 Message Date
adminandClaude Opus 4.5 3c92ef3ac0 Convert battle request and conquest actions to use Scala types
Convert three RoundPhaseAdvancer actions to accept Scala GameState instead of
individual proto-converted fields:
- PerformUncontestedConquestAction
- EndAttackDecisionPhaseAction
- RequestBattlesAction

Each action now extracts the fields it needs internally, simplifying the call
sites in RoundPhaseAdvancer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 07:53:05 -08:00
78 changed files with 4242 additions and 4083 deletions
-3
View File
@@ -29,9 +29,6 @@ common --javacopt="-Xlint:-options"
common --linkopt=-Wl
common:macos --linkopt=-Wl,-no_warn_duplicate_libraries
# Fix Xcode version caching issue - avoids need for `bazel clean --expunge` after Xcode updates
common:macos --repo_env=DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
common --java_language_version=17
common --java_runtime_version=remotejdk_17
common --tool_java_language_version=17
-25
View File
@@ -216,31 +216,6 @@ to be used for different players or game situations within the same server proce
- Map validation tests ensure game content integrity
- Use `GameSettings_test_utils.cpp` and `ShardokEngineBasedTestData.cpp` for C++ test helpers
### Scala Testing Patterns
**Use `inside()` instead of `asInstanceOf` for type matching in tests:**
Never use `asInstanceOf` in tests. Instead, use ScalaTest's `inside()` pattern for safe type matching:
```scala
// BAD - don't do this
val changedHero = result.changedHeroes.head.asInstanceOf[ChangedHeroC]
changedHero.heroId shouldBe 19
// GOOD - use inside() pattern
import org.scalatest.Inside.inside
inside(result.changedHeroes.head) { case changedHero: ChangedHeroC =>
changedHero.heroId shouldBe 19
changedHero.vigorChange shouldBe StatDelta(17.2)
}
```
The `inside()` pattern:
- Provides better error messages when the type doesn't match
- Is idiomatic ScalaTest
- Works with pattern matching for more complex assertions
## Performance Testing
When making performance-related changes to the AI or engine:
+11 -63
View File
@@ -45,12 +45,11 @@
| Phase 4: ActionResultT | **Complete** | All 59 actions return `ActionResultT` |
| Phase 5: Action Base Classes | **Complete** | All `RandomSequentialResultsAction` and `DeterministicSingleResultAction` converted to T-type base classes |
| Phase 5b: Base Class Cleanup | **Complete** | `RandomSequentialResultsAction` and `DeterministicSingleResultAction` deleted |
| Phase 5c: RoundPhaseAdvancer Actions | **Complete** | All actions called by RoundPhaseAdvancer accept Scala GameState |
| Phase 5d: RoundPhaseAdvancer Itself | **Complete** | RoundPhaseAdvancer.checkForPhaseAdvancement takes Scala GameState |
| Phase 5c: RoundPhaseAdvancer Actions | **In Progress** | Converting actions called by RoundPhaseAdvancer to take Scala GameState |
### Phase 5c/5d Progress (Complete)
### Phase 5c Progress
`RoundPhaseAdvancer.checkForPhaseAdvancement` now accepts Scala `GameState` and `ActionResultApplier` directly (PR #4677).
Actions called by `RoundPhaseAdvancer` that need to accept Scala `GameState`:
| Action | PR | Status |
|--------|-----|--------|
@@ -58,16 +57,10 @@
| `PerformForcedTurnBackAction` | #4671 | ✅ Merged |
| `PerformHeroDeparturesAction` | #4672 | ✅ Merged |
| `RequestFreeForAllBattlesAction` | #4673 | ✅ Merged |
| `EndPlayerCommandsPhaseAction` | #4674 | ✅ Merged |
| `EndDiplomacyResolutionPhaseAction` | #4675 | ✅ Merged |
| `RoundPhaseAdvancer` itself | #4677 | ✅ Merged |
### EngineImpl Progress
| Change | PR | Status |
|--------|-----|--------|
| `recursiveTransform` deleted | #4677 | ✅ Merged |
| `recursiveTransformT` uses `RandomStateTSequencer` | #4677 | ✅ Merged |
| `EndPlayerCommandsPhaseAction` | #4674 | ✅ Created |
| `EndDiplomacyResolutionPhaseAction` | - | 🔄 In Progress |
| `EndBattleAftermathPhaseAction` | - | Pending |
| Other RoundPhaseAdvancer actions | - | Pending |
### Current Architecture
@@ -120,11 +113,11 @@ src/main/scala/net/eagle0/eagle/library/actions/applier/ActionResultProtoApplier
```
Create `ActionResultApplier` that applies `ActionResultT` directly to Scala `GameState`.
**Tier 2 - RoundPhaseAdvancer:****Complete**
**Tier 2 - RoundPhaseAdvancer:**
```
src/main/scala/net/eagle0/eagle/library/RoundPhaseAdvancer.scala
```
Now accepts Scala `GameState` and `ActionResultApplier`. Only converts to proto lazily for `AvailableCommandsFactory` calls.
Currently has ~20 calls to `ActionResultProtoConverter.toProto()`. After Tier 1, these become unnecessary.
**Tier 3 - Sequencers:**
```
@@ -147,32 +140,6 @@ Modify `RandomStateTSequencer` to thread Scala `GameState` throughout (currently
4. Once all actions migrated, deprecate/remove proto-based callbacks
5. Remove `lastStateProto` once no longer used
**RandomStateSequencer Migration Progress** (PR #4679 introduced protoless `RandomStateSequencer`):
| Action | Status |
|--------|--------|
| `TruceTurnBackPhaseAction` | ✅ Migrated (PR #4680) |
| `EndHandleRiotsPhaseAction` | ✅ Migrated (PR #4684) |
| `PerformVassalCommandsPhaseAction` | ✅ Migrated |
| `PerformVassalDefenseDecisionsAction` | ✅ Migrated |
| `EndVassalCommandsPhaseAction` | ✅ Migrated |
| `PerformReconResolutionAction` | ✅ Migrated |
| `NewRoundAction` | ✅ Migrated (PR #4698) |
| `EndBattleAftermathPhaseAction` | ✅ Migrated (PR #4699) |
| `EndDiplomacyResolutionPhaseAction` | ✅ Migrated |
| `PerformUnaffiliatedHeroesAction` | ✅ Migrated |
| `EngineImpl.recursiveTransformT` | ✅ Migrated (PR #4704) |
| `ProtolessSequentialResultsActionWrapper` | ✅ Migrated (PR #4705) |
| `LegacyRandomStateTSequencer` | ✅ **Deleted** (PR #4705) |
**TCommandFactory Extraction** (PR #4684):
To enable lightweight mocking of command creation in tests, `TCommandFactory` trait was extracted from `CommandFactory`. This allows tests to mock just the `makeTCommand` method without pulling in all 40+ command dependencies that `CommandFactory` requires.
- `TCommandFactory` - lightweight trait with just `makeTCommand`
- `CommandFactory extends TCommandFactory` - maintains backward compatibility
- Actions accepting command factories now use `TCommandFactory` type for better testability
**Tier 4 - History APIs:**
```
src/main/scala/net/eagle0/eagle/service/InMemoryHistory.scala
@@ -185,7 +152,7 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
| File | Usage | Target |
|------|-------|--------|
| `ActionResultTApplierImpl.scala` | Converts T→Proto, delegates to proto applier | Replace with `ActionResultApplier` |
| `RoundPhaseAdvancer.scala` | ~~20 converter calls~~ | ✅ **Complete** - uses Scala GameState |
| `RoundPhaseAdvancer.scala` | ~20 converter calls | Eliminate after applier conversion |
| `RandomStateTSequencer.scala` | Converts T→Proto internally | Thread Scala GameState throughout |
| `RandomStateProtoSequencer.scala` | Returns `Vector[ActionResultProto]` | Evaluate if still needed |
| `VigorXPApplier.scala` | Wraps proto results | Convert to work with T |
@@ -229,30 +196,11 @@ Remove remaining direct proto imports from utility classes.
| File | Status |
|------|--------|
| `CommandChoiceHelpers.scala` | Accepts proto `GameState`; blocks full deproto of `PerformVassalCommandsPhaseAction` and `PerformVassalDefenseDecisionsAction` |
| `LegacyProvinceUtils.scala` | Replace with `ProvinceUtils.scala` - `hasImminentRiot` added (PR #4683) |
| `LegacyFactionUtils.scala` | Replace proto imports with `FactionT` |
| `LegacyUnaffiliatedHeroUtils.scala` | Replace proto imports with Scala models |
| `BattalionTypeLoader.scala` | Keep proto for file loading, convert immediately after |
| `BeastUtils.scala` | **Complete** - now uses Scala `BeastInfo` only |
### View Filters (Blocking Full Deproto)
The `ProvinceViewFilter` utility currently works entirely with proto types, blocking full deproto of actions that generate province views:
| File | Issue | Needed |
|------|-------|--------|
| `ProvinceViewFilter.scala` | Takes proto `Province`/`GameState`, returns proto `ProvinceView` | Scala `ProvinceViewT` model |
| `GameStateViewFilter.scala` | Uses proto types throughout | Depends on `ProvinceViewT` |
| `GameStateViewDiffer.scala` | Works with view protos | Depends on `ProvinceViewT` |
**Blocked Actions**:
- `EndBattleAftermathPhaseAction` - uses `ProvinceViewFilter` for `revelationChange`, requires lazy proto conversion
- `PerformReconResolutionAction` - uses `ProvinceViewFilter` for reconned provinces
- `GameStateFactionExtensions` - uses `ProvinceViewFilter` for `updatedReconnedProvinces`
**Solution**: Create Scala `ProvinceViewT` (and possibly `ProvinceViewC`) that mirrors the proto `ProvinceView`. Then create a protoless `ProvinceViewFilter` that operates on Scala types. The proto version can delegate to the Scala version + convert, or we maintain both during transition.
---
## Phase 8: Verify Boundaries
@@ -279,7 +227,7 @@ Confirm protos are used correctly at boundaries — and ONLY there.
2. **Shardok Integration**: `ResolveBattleAction` communicates with Shardok. Should the Shardok interface use protos (external service) or Scala models?
3. **View Generation**: `GameStateViewDiffer` works with view protos for client updates. Views need Scala models (`ProvinceViewT`, etc.) to allow actions like `EndBattleAftermathPhaseAction` to be fully protoless. The Scala views would be converted to proto only at the gRPC boundary when sending updates to clients.
3. **View Generation**: `GameStateViewDiffer` works with view protos for client updates. Should views also have Scala models, or is proto acceptable for client-facing projections?
---
@@ -81,7 +81,7 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
pi.is_defender(),
e->GetCurrentGameState()->hex_map(),
e->GetGameSettings()->GetGetter(),
AIAlgorithmType::ITERATIVE_DEEPENING,
AIAlgorithmType::MCTS,
ScoringCalculatorType::MCTS_OPTIMIZED,
mctsConfig);
@@ -24,7 +24,7 @@ namespace eagle {
public void OnTextUpdate(string text, bool completed) { SetUp(); }
public string TextId() { return _entries.Count > 0 ? CurrentEntry.GeneratedTextId : null; }
public string TextId() { return CurrentEntry.GeneratedTextId; }
private void OnEnable() {
ClientTextProvider.Provider.AddListener(this);
@@ -41,13 +41,11 @@ namespace eagle {
public IList<ChronicleEntry> Entries {
get => _entries;
set {
var wasEmpty = _entries.Count == 0;
_entries = value != null ? value.ToList() : new List<ChronicleEntry>();
if (_entries.Count == 0) return;
// Jump to the last entry when first populating, or if not currently viewing
if (wasEmpty || !gameObject.activeSelf) {
if (!gameObject.activeSelf) {
_currentIndex = _entries.Count - 1;
ScrollToTop();
@@ -62,8 +60,6 @@ namespace eagle {
private const string TitleSplitPattern = @"\n\s*=====\s*\n";
private void SetUp() {
if (_entries.Count == 0) return;
previousButton.interactable = _currentIndex > 0;
nextButton.interactable = _currentIndex < _entries.Count - 1;
@@ -224,13 +224,13 @@ namespace eagle {
tp => tp.TypeId == battalionTypeId && tp.MeetsRequirements);
}
private void MaybeActivateRow(
EventBasedTable table,
BattalionTypeId battalionTypeId,
Dictionary<BattalionTypeId, int> extraTroopsByType) {
private void MaybeActivateRow(EventBasedTable table, BattalionTypeId battalionTypeId) {
var parent = table.gameObject.transform.parent;
var allowed = TypeIsAllowed(battalionTypeId) || extraTroopsByType[battalionTypeId] > 0;
var allowed = TypeIsAllowed(battalionTypeId) ||
extraTroops.Where(tfb => tfb.type == battalionTypeId)
.Select(tfb => tfb.count)
.Sum() > 0;
table.gameObject.GetComponent<OrganizeExtrasTable>().Set(
allowed,
@@ -251,12 +251,12 @@ namespace eagle {
parent.GetComponentInChildren<RawImage>().color = color;
}
private void MaybeActivateRows(Dictionary<BattalionTypeId, int> extraTroopsByType) {
MaybeActivateRow(lightInfantryTable, BattalionTypeId.LightInfantry, extraTroopsByType);
MaybeActivateRow(heavyInfantryTable, BattalionTypeId.HeavyInfantry, extraTroopsByType);
MaybeActivateRow(longbowmenTable, BattalionTypeId.Longbowmen, extraTroopsByType);
MaybeActivateRow(dragoonsTable, BattalionTypeId.LightCavalry, extraTroopsByType);
MaybeActivateRow(knightsTable, BattalionTypeId.HeavyCavalry, extraTroopsByType);
private void MaybeActivateRows() {
MaybeActivateRow(lightInfantryTable, BattalionTypeId.LightInfantry);
MaybeActivateRow(heavyInfantryTable, BattalionTypeId.HeavyInfantry);
MaybeActivateRow(longbowmenTable, BattalionTypeId.Longbowmen);
MaybeActivateRow(dragoonsTable, BattalionTypeId.LightCavalry);
MaybeActivateRow(knightsTable, BattalionTypeId.HeavyCavalry);
}
protected override void SetUpUI() {
@@ -330,8 +330,7 @@ namespace eagle {
} else
return false;
// Note: We don't call Update() here - the caller (UpdateTable or MaxClickedImpl)
// will call it when needed. This avoids redundant recalculations.
eb.Update(existingBattalions);
return true;
}
@@ -368,6 +367,7 @@ namespace eagle {
}
// Remove original troops
else {
var updated = eb.Update(existingBattalions);
var availableToRemove = eb.Original.Size - eb.troopsRemoved;
var newlyRemovedCount = Math.Min(KeyModifiedAmount.Amount(), availableToRemove);
@@ -464,8 +464,7 @@ namespace eagle {
} else
return false;
// Note: We don't call Update() here - the caller (UpdateTable or MaxClickedImpl)
// will call it when needed. This avoids redundant recalculations.
newB.Update(existingBattalions);
return true;
}
@@ -654,16 +653,7 @@ namespace eagle {
{ BattalionTypeId.Longbowmen, 0 }
};
// Cache extra troop counts by type to avoid repeated LINQ queries
var extraTroopsByType = new Dictionary<BattalionTypeId, int> {
{ BattalionTypeId.LightInfantry, 0 },
{ BattalionTypeId.HeavyInfantry, 0 },
{ BattalionTypeId.LightCavalry, 0 },
{ BattalionTypeId.HeavyCavalry, 0 },
{ BattalionTypeId.Longbowmen, 0 }
};
foreach (var et in extraTroops) { extraTroopsByType[et.type] += et.count; }
maxAllButton.gameObject.SetActive(false);
foreach (var eb in existingBattalions) {
if (eb.dismissed) continue;
@@ -676,7 +666,9 @@ namespace eagle {
newRow.MaxButtonClickedCallback = () => MaxClicked(eb);
newRow.DismissButtonClickedCallback = () => DismissClicked(eb);
bool canAugment = TypeIsAllowed(eb.TypeId) || extraTroopsByType[eb.TypeId] > 0;
bool canAugment =
TypeIsAllowed(eb.TypeId) ||
extraTroops.Where(tfb => tfb.type == eb.TypeId).Sum(tfb => tfb.count) > 0;
newRow.CanAugment = canAugment;
if (eb.Count < eb.Capacity) {
@@ -747,18 +739,15 @@ namespace eagle {
if (!sufficient) { _disabledReason = "Not enough gold"; }
// Also check that something has changed
// Check newBattalion fields directly instead of calling Update() which is expensive
bool somethingChanged =
(newBattalions.Exists(
b => b.newBattalion.NewTroops > 0 ||
b.newBattalion.TroopsFromOtherBattalion.Count > 0) ||
(newBattalions.Exists(b => b.Update(existingBattalions).Size > 0) ||
existingBattalions.Exists(eb => eb.changed != null || eb.troopsRemoved > 0));
if (!somethingChanged) { _disabledReason = "No battalions have changed"; }
_enableCommit = sufficient && somethingChanged;
resetAllButton.gameObject.SetActive(somethingChanged);
MaybeActivateRows(extraTroopsByType);
MaybeActivateRows();
}
public override AvailableCommand.SealedValueOneofCase CommandType =>
@@ -105,13 +105,9 @@ namespace eagle {
_currentModel.ShardokGameModels
.Select(sgm => {
var needsResync = _shardokNeedsResync.GetValueOrDefault(sgm.Key, false);
// Use thread-safe count from gRPC thread updates, fall back to model
var count = _shardokResultCounts.GetValueOrDefault(
sgm.Key,
sgm.Value.History.Count());
return new IClientConnectionSubscriber.ShardokViewStatus {
shardokGameId = sgm.Key,
filteredResultCount = needsResync ? 0 : count,
filteredResultCount = needsResync ? 0 : sgm.Value.History.Count(),
requestFullResync = needsResync
};
})
@@ -146,11 +142,6 @@ namespace eagle {
private readonly ConcurrentDictionary<string, bool> _shardokNeedsResync =
new ConcurrentDictionary<string, bool>();
// Thread-safe Shardok result counts: updated from gRPC thread via UpdateResultCounts
// Used by ShardokViewStatuses to report accurate counts even when MainQueue is blocked
private readonly ConcurrentDictionary<string, int> _shardokResultCounts =
new ConcurrentDictionary<string, int>();
private readonly RollFetcher _rollFetcher;
// State synced with server
@@ -254,9 +245,9 @@ namespace eagle {
if (battleView == null) {
// Battle was removed (e.g., it ended) before we could create the model.
// This is expected when Eagle's RemovedBattleIds update arrives before a
// pending Shardok update - the UI already shows "Back to Eagle" via
// MarkBattleEnded(), so we just skip this stale update.
// This can happen due to race conditions between Eagle and Shardok updates.
Debug.LogWarning(
$"Cannot create ShardokGameModel for {shardokGameId}: battle not found in ShardokBattles (likely already ended)");
return null;
}
@@ -340,7 +331,6 @@ namespace eagle {
// any stale reference and skip this update
if (shardokGameModel == null) {
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
continue;
}
}
@@ -369,7 +359,6 @@ namespace eagle {
} else {
// Game ended - remove from active models so UI knows battle is over
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
}
}
if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
@@ -404,12 +393,10 @@ namespace eagle {
}
break;
case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
foreach (var response in update.ShardokActionResultResponse
.ShardokGameResponses) {
_shardokResultCounts[response.ShardokGameId] = response.NewResultViewCount;
}
break;
// Note: Shardok counts are tracked in ShardokGameModel.History.Count
// which is updated in ReceiveGameUpdate on the main thread.
// For now, Shardok reconnects may still get duplicate data, but
// the primary issue (Eagle duplicates) is fixed here.
}
}
@@ -775,15 +762,6 @@ namespace eagle {
foreach (ShardokBattleView bv in entry.NewBattles) _currentModel.ShardokBattles.Add(bv);
foreach (string rb in entry.RemovedBattleIds) {
// If there's an active ShardokGameModel for this battle, mark it as ended
// so the UI knows to return to Eagle. This handles the race condition where
// the Eagle update removing the battle arrives before the Shardok Victory update.
if (_currentModel.ShardokGameModels.TryGetValue(rb, out var sgm)) {
sgm.MarkBattleEnded("Battle has ended.");
_currentModel.ShardokGameModels.Remove(rb);
}
_shardokResultCounts.TryRemove(rb, out _);
for (int i = 0; i < _currentModel.ShardokBattles.Count; i++) {
if (_currentModel.ShardokBattles[i].ShardokGameId == rb) {
_currentModel.ShardokBattles.RemoveAt(i);
@@ -18,11 +18,10 @@ namespace eagle {
if (scrollRect) { scrollRect.normalizedPosition = new Vector2(0, 1); }
}
// Always update the view when TextId changes to clear any stale text
if (!String.IsNullOrEmpty(_textId)) {
// If the text ID is set, we want to update the view immediately
// to reflect any existing text.
OnTextUpdate(ClientTextProvider.Provider.GetTextEntry(TextId));
} else {
UpdateView();
}
}
}
@@ -60,14 +59,7 @@ namespace eagle {
}
private void OnTextUpdate(TextEntry entry) {
if (entry != null) {
OnTextUpdate(entry.Text, entry.Completed);
} else {
// Entry doesn't exist yet - clear text and update view to avoid stale content
_currentText = "";
_currentCompleted = false;
UpdateView();
}
if (entry != null) OnTextUpdate(entry.Text, entry.Completed);
}
public void OnTextUpdate(string text, bool completed) {
@@ -17,10 +17,6 @@ namespace eagle {
private readonly Queue<Notification> _notes = new();
// Incremented when DismissAll is clicked; pending AddNote calls check this
// to skip adding if a dismiss happened since they were enqueued
private int _dismissGeneration = 0;
private void SetPopupInfos() {
PopupInfos = _notes.Select(note => new PopupInfo {
titleText = note.Title,
@@ -58,13 +54,7 @@ namespace eagle {
string llmId,
List<ProvinceId> provinceIds,
List<HeroView> displayedHeroes) {
// Capture current generation - if DismissAll is clicked before this executes,
// we'll skip adding the note
var capturedGeneration = _dismissGeneration;
MainQueue.Q.Enqueue(() => {
// Skip if DismissAll was clicked since this was enqueued
if (capturedGeneration != _dismissGeneration) return;
var existingNote = _notes.FirstOrDefault(
n => n.Title == title &&
HeroListsMatch(n.DisplayedHeroes, displayedHeroes));
@@ -89,8 +79,6 @@ namespace eagle {
}
public void DismissAllClicked() {
// Increment generation immediately so pending AddNote calls will skip
_dismissGeneration++;
_notes.Clear();
SetPopupInfos();
}
@@ -226,10 +226,12 @@ namespace eagle {
$"[SUBSCRIBE] Subscription confirmed for game {gameId}, " +
$"confirmedResultCount={ack.ConfirmedResultCount}");
// Note: Shardok resync flags are cleared in EagleGameModel.HandleOneGameUpdate
// AFTER updates are actually received, not here. This ensures that if the
// connection drops between acknowledgment and update delivery, the resync
// will be requested again on the next reconnect.
// Clear resync flags ONLY after successful acknowledgment
if (subscriber is GameModelUpdater updater) {
foreach (var status in shardokStatuses.Where(s => s.requestFullResync)) {
updater.ClearShardokResyncFlag(status.shardokGameId);
}
}
return true;
} else {
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
using Debug = UnityEngine.Debug;
public class MainQueue : MonoBehaviour {
static MainQueue __singletonInstance;
private readonly Queue<Action> _actionQueue = new();
private readonly Queue<Action> _nextUpdateQueue = new();
// Time budget per frame to prevent blocking when resuming from background
// 8ms leaves room for rendering within a 16ms (60fps) frame budget
private const long MaxMillisecondsPerFrame = 8;
// Limit actions per frame to prevent blocking when resuming from background
private const int MaxActionsPerFrame = 10;
// Track queue depth for logging
private int _lastLoggedQueueDepth = 0;
@@ -24,29 +21,19 @@ public class MainQueue : MonoBehaviour {
// Update is called once per frame
void Update() {
int actionsProcessed = 0;
int queueDepthBefore;
lock (_actionQueue) { queueDepthBefore = _actionQueue.Count; }
// Fast path: skip processing if queue is empty
if (queueDepthBefore == 0) {
lock (_nextUpdateQueue) {
if (_nextUpdateQueue.Count > 0) {
foreach (Action action in _nextUpdateQueue) { Enqueue(action); }
_nextUpdateQueue.Clear();
}
}
return;
}
// Log when queue has built up (e.g., after resuming from background)
if (queueDepthBefore > 100 && queueDepthBefore != _lastLoggedQueueDepth) {
if (queueDepthBefore > MaxActionsPerFrame && queueDepthBefore != _lastLoggedQueueDepth) {
Debug.Log($"[MainQueue] Processing backlog: {queueDepthBefore} actions queued");
_lastLoggedQueueDepth = queueDepthBefore;
} else if (queueDepthBefore <= 100) {
} else if (queueDepthBefore <= MaxActionsPerFrame) {
_lastLoggedQueueDepth = 0;
}
var stopwatch = Stopwatch.StartNew();
Action possibleAction;
do {
possibleAction = null;
@@ -54,8 +41,11 @@ public class MainQueue : MonoBehaviour {
if (_actionQueue.Count > 0) { possibleAction = _actionQueue.Dequeue(); }
}
if (possibleAction != null) { possibleAction.Invoke(); }
} while (possibleAction != null && stopwatch.ElapsedMilliseconds < MaxMillisecondsPerFrame);
if (possibleAction != null) {
possibleAction.Invoke();
actionsProcessed++;
}
} while (possibleAction != null && actionsProcessed < MaxActionsPerFrame);
lock (_nextUpdateQueue) {
foreach (Action action in _nextUpdateQueue) { Enqueue(action); }
@@ -22,9 +22,6 @@ public class HexMesh : MonoBehaviour {
}
public void Triangulate(IEnumerable<HexCell> cells) {
// Guard against Update() being called before SetUp() initializes hexMesh
if (hexMesh == null) return;
hexMesh.Clear();
vertices.Clear();
triangles.Clear();
@@ -961,7 +961,7 @@ namespace Shardok {
commandTypeUIManager.CommandGroupForType(command.Type));
}
if (allCommands.Any() && mapMouseCoords != null) {
if (allCommands.Any()) {
var meleeCommands = allCommands.Where(
command => CommandTypeUIManager.MeleeAttackGroup ==
commandTypeUIManager.CommandGroupForType(command.Type));
@@ -111,17 +111,6 @@ public class ShardokGameModel {
public bool InSetUp => GameStatus != null && GameStatus.State == GameStatus.Types.State.SetUp;
/// <summary>
/// Mark this battle as ended (called when battle is removed from Eagle before
/// we receive the final Shardok update, e.g., due to race conditions when
/// Unity was backgrounded).
/// </summary>
public void MarkBattleEnded(string reason) {
GameStatus =
new GameStatus { State = GameStatus.Types.State.Victory, Description = reason };
UpdateAction?.Invoke();
}
private readonly PersistentClientConnection _persistentClientConnection;
private const int StartingHistoryCapacity = 100;
@@ -23,7 +23,6 @@ message IncompleteText {
string partial_text = 2;
.net.eagle0.eagle.internal.GeneratedTextRequest llm_request = 3;
int32 requested_after_history_count = 4;
int64 requested_at_millis = 5;
}
message UnrequestedText {
@@ -21,8 +21,7 @@ case class IncompleteClientText(
id: ClientTextId,
partialText: String,
requestedAfterHistoryCount: Int,
llmRequest: GeneratedTextRequest,
requestedAtMillis: Long
llmRequest: GeneratedTextRequest
) extends ClientText {
def append(newText: String): IncompleteClientText =
copy(partialText = partialText + newText)
@@ -17,15 +17,6 @@ trait ClientTextStore {
def unrequestedTexts: Map[ClientTextId, UnrequestedClientText]
def accessibleTo: Map[ClientTextId, Vector[FactionId]]
/** Returns incomplete texts that have been waiting longer than the threshold */
def stalledIncompleteTexts(
thresholdMillis: Long,
currentTimeMillis: Long = System.currentTimeMillis()
): Vector[IncompleteClientText] =
incompleteTexts.values
.filter(ict => currentTimeMillis - ict.requestedAtMillis > thresholdMillis)
.toVector
def saved: ClientTextStore
def withAddedTextRequest(
@@ -69,8 +69,7 @@ case class ClientTextStoreImpl(
id = id,
partialText = "",
llmRequest = unrequested.llmRequest,
requestedAfterHistoryCount = unrequested.requestedAfterHistoryCount,
requestedAtMillis = System.currentTimeMillis()
requestedAfterHistoryCount = unrequested.requestedAfterHistoryCount
)),
unrequestedTexts = unrequestedTexts - id,
incompleteTextsAreSaved = false
@@ -239,8 +238,7 @@ object ClientTextStoreImpl {
id = ict.id,
partialText = ict.text,
llmRequest = Some(ict.llmRequest),
requestedAfterHistoryCount = ict.requestedAfterHistoryCount,
requestedAtMillis = ict.requestedAtMillis
requestedAfterHistoryCount = ict.requestedAfterHistoryCount
)
}.toVector,
unrequestedTexts = completeSaved.unrequestedTexts.map {
@@ -332,12 +330,7 @@ object ClientTextStoreImpl {
id = it.id,
partialText = it.partialText,
llmRequest = it.llmRequest.get,
requestedAfterHistoryCount = it.requestedAfterHistoryCount,
// Use persisted timestamp if available, otherwise use current time
// (for backwards compatibility with old persisted data)
requestedAtMillis =
if it.requestedAtMillis > 0 then it.requestedAtMillis
else System.currentTimeMillis()
requestedAfterHistoryCount = it.requestedAfterHistoryCount
)
}.toVector,
icts.unrequestedTexts.map { it =>
@@ -48,9 +48,7 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/availability",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:check_for_fulfilled_quests_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:hero_backstory_update_action_generator",
@@ -59,19 +57,18 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator",
"//src/main/scala/net/eagle0/eagle/library/util/validations:runtime_validator",
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_runtime_validator",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//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/game_state",
@@ -152,16 +149,11 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_scala_proto",
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
@@ -211,6 +203,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:vigor_xp_applier",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator",
@@ -225,8 +218,11 @@ scala_library(
"//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/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_type",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
@@ -7,12 +7,7 @@ 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.library.actions.applier.{
ActionResultApplierImpl,
ActionResultProtoApplier,
ActionResultProtoApplierImpl,
ActionResultWithResultingState
}
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,
@@ -20,12 +15,11 @@ import net.eagle0.eagle.library.actions.impl.action.{
ResolveBattleAction
}
import net.eagle0.eagle.library.actions.impl.command.{AvailableCommandTypeMap, CommandFactory}
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.actions.impl.common.{ActionWithResultingState, RandomStateProtoSequencer}
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
import net.eagle0.eagle.library.util.validations.{RuntimeValidator, ScalaRuntimeValidator}
import net.eagle0.eagle.library.util.validations.RuntimeValidator
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.EngineImpl.{appliedResults, appliedResultsScala, withUpdateChecks}
import net.eagle0.eagle.library.EngineImpl.{appliedResults, withUpdateChecks}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
@@ -49,10 +43,10 @@ object EngineImpl {
private def withPhaseAdvancement(
engineAndResultsImpl: EngineAndResultsImpl
): EngineAndResultsImpl =
engineAndResultsImpl.recursiveTransformScala(eng =>
engineAndResultsImpl.recursiveTransform(eng =>
RoundPhaseAdvancer.checkForPhaseAdvancement(
currentState = eng.currentState,
actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator)),
currentState = GameStateConverter.toProto(eng.currentState),
actionResultProtoApplier = eng.actionResultProtoApplier,
history = eng.history,
availableCommandsFactory = eng.availableCommandsFactory,
heroGenerator = eng.heroGenerator,
@@ -103,31 +97,14 @@ object EngineImpl {
results = results.map(_.actionResult)
)
)
def appliedResultsScala(
engine: EngineImpl,
results: Vector[ActionResultWithResultingState]
): EngineAndResults = withUpdateChecks(
EngineAndResultsImpl(
engine = engine.copy(
currentState = results.lastOption
.map(_.resultingState)
.getOrElse(engine.currentState),
history = engine.history.withNewResultsScala(results)
),
results = results.map(awrs =>
net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter.toProto(awrs.actionResult)
)
)
)
}
final case class EngineAndResultsImpl(
engine: EngineImpl,
results: Vector[ActionResult]
) extends EngineAndResults {
def recursiveTransformScala(
f: EngineImpl => Vector[ActionResultWithResultingState]
def recursiveTransform(
f: EngineImpl => Vector[ActionWithResultingState]
): EngineAndResultsImpl = {
@tailrec
def go(
@@ -138,7 +115,7 @@ final case class EngineAndResultsImpl(
if goResults.isEmpty then EngineAndResultsImpl(eng, acc)
else
appliedResultsScala(eng, goResults) match {
appliedResults(eng, goResults) match {
case EngineAndResultsImpl(eng2, res) =>
go(eng2, acc ++ res)
}
@@ -149,13 +126,13 @@ final case class EngineAndResultsImpl(
def recursiveTransformT(
f: EngineImpl => Vector[ActionResultT]
): EngineAndResultsImpl = recursiveTransformScala { eng =>
val actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator))
RandomStateSequencer(
): EngineAndResultsImpl = recursiveTransform { eng =>
val results = f(eng)
RandomStateProtoSequencer(
initialState = eng.currentState,
actionResultApplier = actionResultApplier,
actionResultProtoApplier = eng.actionResultProtoApplier,
functionalRandom = SeededRandom(eng.currentState.randomSeed)
).withActionResults(_ => f(eng)).resultsWithStates.newValue
).withActionResultTs(_ => results).results.newValue
}
def saveNow: EngineAndResultsImpl =
@@ -305,34 +282,37 @@ case class EngineImpl(
)
val availableCommand = availableCommandOpt.get
val sequencer = RandomStateSequencer(
val sequencer = RandomStateProtoSequencer(
initialState = this.currentState,
actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator)),
actionResultProtoApplier = actionResultProtoApplier,
functionalRandom = SeededRandom(this.currentState.randomSeed)
).withTCommand { gs =>
commandFactory.makeTCommand(
actingFactionId = factionId,
gameState = gs,
availableCommand = availableCommand,
selectedCommand = selectedCommand
)
}.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator(gs))
).withActionResults { gs =>
val results = commandFactory
.makeCommand(
actingFactionId = factionId,
gameState = GameStateConverter.fromProto(gs),
availableCommand = availableCommand,
selectedCommand = selectedCommand
)
.execute(actionResultProtoApplier)
.map(_.actionResult)
// Validate that the first result has an acting faction set (required for player commands)
val firstResult = sequencer.actionResults.newValue.headOption
if !firstResult.forall(_.actingFactionId.isDefined) then {
print(
"Result with type " + firstResult.map(_.actionResultType).getOrElse("unknown") + " did not have a player set"
if !results.headOption.forall(_.player.isDefined) then {
print(
"Result with type " + results.head.`type` + " did not have a player set"
)
}
internalRequire(
results.headOption.forall(_.player.isDefined),
s"Result with type ${results.head.`type`} did not have a player set"
)
}
internalRequire(
firstResult.forall(_.actingFactionId.isDefined),
s"Result with type ${firstResult.map(_.actionResultType).getOrElse("unknown")} did not have a player set"
)
appliedResultsScala(
results
}.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator.fromGameState(gs))
appliedResults(
engine = this,
results = sequencer.resultsWithStates.newValue
results = sequencer.results.newValue
)
}
}
@@ -1,10 +1,7 @@
package net.eagle0.eagle.library
import net.eagle0.eagle.{FactionId, RoundId, ShardokGameId}
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.shardok.api.action_result_view.ActionResultView as ShardokActionResultView
@@ -40,14 +37,6 @@ trait GameHistory {
def withNewResults(newResults: Vector[ActionWithResultingState]): GameHistory
def withNewResultsScala(newResults: Vector[ActionResultWithResultingState]): GameHistory =
withNewResults(newResults.map { awrs =>
ActionWithResultingState(
actionResult = ActionResultProtoConverter.toProto(awrs.actionResult),
gameState = GameStateConverter.toProto(awrs.resultingState)
)
})
def shardokCount(shardokGameId: ShardokGameId): Int
def shardokGameState(
@@ -3,34 +3,38 @@ package net.eagle0.eagle.library
import scala.collection.mutable
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.{
ActionResultApplier,
ActionResultTApplierImpl,
ActionResultWithResultingState
}
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.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.VigorXPApplier
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
import net.eagle0.eagle.library.util.validations.ScalaRuntimeValidator
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.game_state.GameStateConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.RoundPhase
import net.eagle0.eagle.model.state.RoundPhase.*
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.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
object RoundPhaseAdvancer {
private val times: mutable.Map[RoundPhase, Long] =
mutable.Map(RoundPhase.allValues.map(_ -> 0L)*)
mutable.Map(RoundPhase.values.map(_ -> 0L)*)
private val print = false
private val roundsBetweenPrint = 100
private def printTimings(roundId: RoundId): Unit = {
val totalTime = times.values.sum.toDouble / 1000.0
times.toVector.sortBy(_._2)(using Ordering.Long.reverse).foreach {
times.toVector.sortBy(_._2)(Ordering.Long.reverse).foreach {
case (phase, time) =>
val timeInSecs = time.toDouble / 1000.0
val msPerRound = time.toDouble / roundId.toDouble
@@ -44,341 +48,332 @@ object RoundPhaseAdvancer {
def checkForPhaseAdvancement(
currentState: GameState,
actionResultApplier: ActionResultApplier,
actionResultProtoApplier: ActionResultProtoApplier,
history: GameHistory,
availableCommandsFactory: AvailableCommandsFactory,
heroGenerator: HeroGenerator,
commandFactory: CommandFactory
): Vector[ActionResultWithResultingState] = {
// Lazy conversion to proto for AvailableCommandsFactory calls
lazy val currentStateProto: GameStateProto = GameStateConverter.toProto(currentState)
// ActionResultTApplier for actions that need internal state tracking
val actionResultTApplier = ActionResultTApplierImpl(actionResultApplier)
): Vector[ActionWithResultingState] = {
val currentPhase = currentState.currentPhase
val startTime = System.currentTimeMillis
if print && currentPhase == NewRound && currentState.currentRoundId % roundsBetweenPrint == 0
if print && currentPhase == NEW_ROUND && currentState.currentRoundId % roundsBetweenPrint == 0
then {
printTimings(currentState.currentRoundId)
}
val results: Vector[ActionResultWithResultingState] = currentPhase match {
case NewRound =>
val actionResults = NewRoundAction(currentState, history, actionResultApplier)
.randomResults(SeededRandom(currentState.randomSeed))
.newValue
.map(VigorXPApplier.withVigorXp)
actionResultApplier.applyActionResults(currentState, actionResults)
case PrisonerExchange =>
actionResultApplier.applyActionResults(
currentState,
PrisonerExchangeAction(currentState).results
val results: Vector[ActionWithResultingState] = currentPhase match {
case UNKNOWN_PHASE =>
throw new IllegalStateException(
"Somehow we're in game state UNKNOWN_PHASE"
)
case ProvinceEvents =>
actionResultApplier.applyActionResults(
case NEW_ROUND =>
NewRoundAction(GameStateConverter.fromProto(currentState), history).execute(actionResultProtoApplier)
case PRISONER_EXCHANGE =>
actionResultProtoApplier.applyActionResults(
currentState,
PerformProvinceEventsAction(currentState)
.results(SeededRandom(currentState.randomSeed))
PrisonerExchangeAction(GameStateConverter.fromProto(currentState)).results
.map(ActionResultProtoConverter.toProto)
)
case ForcedTurnBack =>
actionResultApplier.applyActionResults(
case PROVINCE_EVENTS =>
PerformProvinceEventsAction(
GameStateConverter.fromProto(currentState)
).execute(currentState, actionResultProtoApplier)
case FORCED_TURN_BACK =>
actionResultProtoApplier.applyActionResults(
currentState,
PerformForcedTurnBackAction(currentState).results
PerformForcedTurnBackAction(GameStateConverter.fromProto(currentState)).results
.map(ActionResultProtoConverter.toProto)
)
case ProvinceMoveResolution =>
val actionResults = PerformProvinceMoveResolutionAction(currentState)
.results(SeededRandom(currentState.randomSeed), actionResultTApplier)
.map(VigorXPApplier.withVigorXp)
actionResultApplier.applyActionResults(currentState, actionResults)
case PROVINCE_MOVE_RESOLUTION =>
PerformProvinceMoveResolutionAction(
GameStateConverter.fromProto(currentState)
).execute(actionResultProtoApplier)
case HandleRiot =>
case HANDLE_RIOT =>
if availableCommandsFactory
.hasAvailableHandleRiotPhaseCommands(currentStateProto)
.hasAvailableHandleRiotPhaseCommands(currentState)
then Vector.empty
else
val actionResults = EndHandleRiotsPhaseAction(
gameState = currentState,
EndHandleRiotsPhaseAction(
gameState = GameStateConverter.fromProto(currentState),
commandsForProvince = pid =>
availableCommandsFactory
.handleRiotPhaseCommandsForOneProvince(
currentStateProto,
currentStateProto.provinces(pid)
currentState,
currentState.provinces(pid)
),
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).results(SeededRandom(currentState.randomSeed))
.map(VigorXPApplier.withVigorXp)
actionResultApplier.applyActionResults(currentState, actionResults)
applier = ActionResultTApplierImpl(ScalaRuntimeValidator)
).execute(currentState, actionResultProtoApplier)
case HeroDepartures =>
val actionResults = PerformHeroDeparturesAction(gameState = currentState)
.results(SeededRandom(currentState.randomSeed))
actionResultApplier.applyActionResults(currentState, actionResults)
case HERO_DEPARTURES =>
PerformHeroDeparturesAction(
gameState = GameStateConverter.fromProto(currentState)
).execute(currentState, actionResultProtoApplier)
case UnaffiliatedHeroActions =>
val actionResults = PerformUnaffiliatedHeroesAction(
gameState = currentState,
heroGenerator = heroGenerator,
actionResultApplier = actionResultApplier
).results(SeededRandom(currentState.randomSeed))
.map(VigorXPApplier.withVigorXp)
actionResultApplier.applyActionResults(currentState, actionResults)
case UNAFFILIATED_HERO_ACTIONS =>
PerformUnaffiliatedHeroesAction(
gameState = GameStateConverter.fromProto(currentState),
heroGenerator = heroGenerator
).execute(actionResultProtoApplier)
case PleaseRecruitMe =>
case PLEASE_RECRUIT_ME =>
if availableCommandsFactory
.hasAvailablePleaseRecruitMePhaseCommands(currentStateProto)
.hasAvailablePleaseRecruitMePhaseCommands(currentState)
then Vector.empty
else
Vector(
actionResultApplier.applyActionResult(
actionResultProtoApplier.applyActionResult(
currentState,
EndPleaseRecruitMePhaseAction(currentState).immediateExecute
ActionResultProtoConverter.toProto(
EndPleaseRecruitMePhaseAction(GameStateConverter.fromProto(currentState)).immediateExecute
)
)
)
case VassalCommands =>
case VASSAL_COMMANDS =>
val vassalCommandResults = PerformVassalCommandsPhaseAction(
gameState = currentState,
gameState = GameStateConverter.fromProto(currentState),
commandsForProvince = availableCommandsFactory
.commandPhaseCommandsForProvince(currentStateProto, _),
.commandPhaseCommandsForProvince(currentState, _),
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).results(SeededRandom(currentState.randomSeed))
.map(VigorXPApplier.withVigorXp)
val appliedResults = actionResultApplier.applyActionResults(currentState, vassalCommandResults)
applier = ActionResultTApplierImpl(ScalaRuntimeValidator)
).execute(currentState, actionResultProtoApplier)
if appliedResults.nonEmpty then appliedResults
if vassalCommandResults.nonEmpty then vassalCommandResults
else
val endResults = EndVassalCommandsPhaseAction(currentState, actionResultApplier)
.results(SeededRandom(currentState.randomSeed))
.map(VigorXPApplier.withVigorXp)
actionResultApplier.applyActionResults(currentState, endResults)
EndVassalCommandsPhaseAction(GameStateConverter.fromProto(currentState)).execute(
actionResultProtoApplier
)
case PlayerCommands =>
case PLAYER_COMMANDS =>
if availableCommandsFactory
.hasAvailablePlayerCommandsPhaseCommands(
currentStateProto
currentState
)
then Vector.empty
else
actionResultApplier.applyActionResults(
actionResultProtoApplier.applyActionResults(
currentState,
EndPlayerCommandsPhaseAction(
currentState,
actionResultApplier
).randomResults(functionalRandom = SeededRandom(currentState.randomSeed)).newValue
GameStateConverter.fromProto(currentState),
ActionResultTApplierImpl(ScalaRuntimeValidator)
).randomResults(functionalRandom = SeededRandom(currentState.randomSeed))
.newValue
.map(
ActionResultProtoConverter.toProto(_)
)
)
case HostileArmySetup =>
case HOSTILE_ARMY_SETUP =>
Vector(
actionResultApplier.applyActionResult(
actionResultProtoApplier.applyActionResult(
currentState,
PerformHostileArmySetupAction(currentState).immediateExecute
ActionResultProtoConverter.toProto(
PerformHostileArmySetupAction(
GameStateConverter.fromProto(currentState)
).immediateExecute
)
)
)
case FreeForAllDecision =>
case FREE_FOR_ALL_DECISION =>
if availableCommandsFactory
.hasAvailableFreeForAllDecisionPhaseCommands(
currentStateProto
currentState
)
then Vector.empty
else
// There may eventually be VassalAttackDecisions, but for now we're leaving that on the player
actionResultApplier.applyActionResults(
actionResultProtoApplier.applyActionResults(
currentState,
EndFreeForAllDecisionPhaseAction(currentState).results
EndFreeForAllDecisionPhaseAction(GameStateConverter.fromProto(currentState)).results.map(
ActionResultProtoConverter.toProto(_)
)
)
case FreeForAllBattleRequest =>
val requestResults = actionResultApplier.applyActionResults(
case FREE_FOR_ALL_BATTLE_REQUEST =>
val requestResults = actionResultProtoApplier.applyActionResults(
currentState,
RequestFreeForAllBattlesAction(currentState).results
RequestFreeForAllBattlesAction(
GameStateConverter.fromProto(currentState)
).results.map(ActionResultProtoConverter.toProto)
)
val latestState = requestResults.lastOption.map(_.resultingState).getOrElse(currentState)
requestResults :+ actionResultApplier.applyActionResult(
val latestState = requestResults.lastOption.map(_.gameState).getOrElse(currentState)
requestResults :+ actionResultProtoApplier.applyActionResult(
latestState,
EndFreeForAllBattleRequestPhaseAction.immediateExecute
ActionResultProtoConverter.toProto(EndFreeForAllBattleRequestPhaseAction.immediateExecute)
)
case FreeForAllBattleResolution =>
case FREE_FOR_ALL_BATTLE_RESOLUTION =>
if currentState.outstandingBattles.isEmpty then
Vector(
actionResultApplier.applyActionResult(
actionResultProtoApplier.applyActionResult(
currentState,
EndFreeForAllBattleResolutionPhaseAction.immediateExecute
ActionResultProtoConverter.toProto(EndFreeForAllBattleResolutionPhaseAction.immediateExecute)
)
)
else Vector.empty // wait for battles to resolve
case UncontestedConquest =>
actionResultApplier.applyActionResults(
case UNCONTESTED_CONQUEST =>
actionResultProtoApplier.applyActionResults(
currentState,
PerformUncontestedConquestAction(
gameId = currentState.gameId,
currentRoundId = currentState.currentRoundId,
currentDate = currentState.currentDate.get,
provinces = currentState.provinces,
factions = currentState.factions,
heroes = currentState.heroes,
battalions = currentState.battalions
GameStateConverter.fromProto(currentState)
).results
.map(ActionResultProtoConverter.toProto)
)
case AttackDecision =>
case ATTACK_DECISION =>
if availableCommandsFactory.hasAvailableAttackDecisionPhaseCommands(
currentStateProto
currentState
)
then Vector.empty
else {
// There may eventually be VassalAttackDecisions, but for now we're leaving that on the player
actionResultApplier.applyActionResults(
actionResultProtoApplier.applyActionResults(
currentState,
EndAttackDecisionPhaseAction(
gameId = currentState.gameId,
currentRoundId = currentState.currentRoundId,
currentDate = currentState.currentDate.get,
provinces = currentState.provinces.values.toVector
).results
GameStateConverter.fromProto(currentState)
).results.map(ActionResultProtoConverter.toProto)
)
}
case DefenseDecision =>
case DEFENSE_DECISION =>
if availableCommandsFactory.hasAvailablePlayerDefenseCommands(
currentStateProto
currentState
)
then Vector.empty
else {
val vassalCommandResults = PerformVassalDefenseDecisionsAction(
gameState = currentState,
gameState = GameStateConverter.fromProto(currentState),
commandsForProvince = availableCommandsFactory
.defensePhaseCommandsForProvince(currentStateProto, _),
.defensePhaseCommandsForProvince(currentState, _),
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).results(SeededRandom(currentState.randomSeed))
.map(VigorXPApplier.withVigorXp)
val appliedResults = actionResultApplier.applyActionResults(currentState, vassalCommandResults)
applier = ActionResultTApplierImpl(ScalaRuntimeValidator)
).execute(currentState, actionResultProtoApplier)
if appliedResults.nonEmpty then appliedResults
if vassalCommandResults.nonEmpty then vassalCommandResults
else
Vector(
actionResultApplier.applyActionResult(
actionResultProtoApplier.applyActionResult(
currentState,
EndDefenseDecisionPhaseAction(currentState).immediateExecute
ActionResultProtoConverter.toProto(
EndDefenseDecisionPhaseAction(GameStateConverter.fromProto(currentState)).immediateExecute
)
)
)
}
case TruceTurnBack =>
val actionResults = TruceTurnBackPhaseAction(currentState, actionResultApplier)
.results(SeededRandom(currentState.randomSeed))
.map(VigorXPApplier.withVigorXp)
actionResultApplier.applyActionResults(currentState, actionResults)
case TRUCE_TURN_BACK =>
TruceTurnBackPhaseAction(GameStateConverter.fromProto(currentState)).execute(actionResultProtoApplier)
case BattleRequest =>
case BATTLE_REQUEST =>
val requestBattlesAction = RequestBattlesAction(
gameId = currentState.gameId,
currentRoundId = currentState.currentRoundId,
currentDate = currentState.currentDate.get,
battleCounter = currentState.battleCounter,
heroes = currentState.heroes,
battalions = currentState.battalions,
provinces = currentState.provinces,
factions = currentState.factions,
battalionTypes = currentState.battalionTypes.map(bt => bt.typeId -> bt).toMap
GameStateConverter.fromProto(currentState)
)
val requestResults = actionResultApplier.applyActionResults(
val requestResults = actionResultProtoApplier.applyActionResults(
currentState,
requestBattlesAction.results
requestBattlesAction.results.map(
ActionResultProtoConverter.toProto(_)
)
)
val latestState = requestResults.lastOption.map(_.resultingState).getOrElse(currentState)
requestResults :+ actionResultApplier.applyActionResult(
val latestState = requestResults.lastOption.map(_.gameState).getOrElse(currentState)
requestResults :+ actionResultProtoApplier.applyActionResult(
latestState,
EndBattleRequestPhaseAction(latestState).immediateExecute
)
case FoodConsumption =>
Vector(
actionResultApplier.applyActionResult(
currentState,
PerformFoodConsumptionPhaseAction(currentState).immediateExecute
ActionResultProtoConverter.toProto(
EndBattleRequestPhaseAction(GameStateConverter.fromProto(latestState)).immediateExecute
)
)
case BattleResolution =>
case FOOD_CONSUMPTION =>
Vector(
actionResultProtoApplier.applyActionResult(
currentState,
ActionResultProtoConverter.toProto(
PerformFoodConsumptionPhaseAction(GameStateConverter.fromProto(currentState)).immediateExecute
)
)
)
case BATTLE_RESOLUTION =>
if currentState.outstandingBattles.isEmpty then
Vector(
actionResultApplier.applyActionResult(
actionResultProtoApplier.applyActionResult(
currentState,
EndBattleResolutionPhaseAction.immediateExecute
ActionResultProtoConverter.toProto(EndBattleResolutionPhaseAction.immediateExecute)
)
)
else Vector.empty // wait for battles to resolve
case BattleAftermath =>
case BATTLE_AFTERMATH =>
if currentState.provinces.values
.flatMap(_.capturedHeroes)
.isEmpty
then
actionResultApplier.applyActionResults(
actionResultProtoApplier.applyActionResults(
currentState,
EndBattleAftermathPhaseAction(
currentState,
actionResultApplier
GameStateConverter.fromProto(currentState),
ActionResultTApplierImpl(ScalaRuntimeValidator)
)
.randomResults(
SeededRandom(currentState.randomSeed)
)
.newValue
.map(ActionResultProtoConverter.toProto)
)
else Vector.empty
case DiplomacyResolution =>
case DIPLOMACY_RESOLUTION =>
if availableCommandsFactory.hasAvailablePlayerCommands(
currentStateProto
currentState
)
then Vector.empty
else
actionResultApplier.applyActionResults(
actionResultProtoApplier.applyActionResults(
currentState,
EndDiplomacyResolutionPhaseAction(
currentState,
actionResultApplier = actionResultApplier
).randomResults(SeededRandom(currentState.randomSeed)).newValue
GameStateConverter.fromProto(currentState),
actionResultTApplier = ActionResultTApplierImpl(ScalaRuntimeValidator)
).randomResults(SeededRandom(currentState.randomSeed))
.newValue
.map(ActionResultProtoConverter.toProto)
)
case ReconResolution =>
val actionResults = PerformReconResolutionAction(currentState, actionResultApplier)
.results(SeededRandom(currentState.randomSeed))
.map(VigorXPApplier.withVigorXp)
actionResultApplier.applyActionResults(currentState, actionResults)
case RECON_RESOLUTION =>
PerformReconResolutionAction(GameStateConverter.fromProto(currentState)).execute(
actionResultProtoApplier
)
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, currentStateProto, availableCommandsFactory)
validateResults(results, currentState, availableCommandsFactory)
}
// We should always either return results, be waiting for an LLM request or battle to resolve,
// or have available player commands
private def validateResults(
results: Vector[ActionResultWithResultingState],
results: Vector[ActionWithResultingState],
startingState: GameState,
startingStateProto: GameStateProto,
availableCommandsFactory: AvailableCommandsFactory
): Vector[ActionResultWithResultingState] =
): Vector[ActionWithResultingState] =
internalValidated(
results,
(r: Vector[ActionResultWithResultingState]) =>
(r: Vector[ActionWithResultingState]) =>
r.nonEmpty ||
startingState.outstandingBattles.nonEmpty ||
availableCommandsFactory.hasAvailablePlayerCommands(startingStateProto),
availableCommandsFactory.hasAvailablePlayerCommands(startingState),
"No results were found, but we also don't seem to be waiting for anything"
)
}
@@ -16,13 +16,7 @@ object GameStateMiscExtensions {
def applyNewNotifications(notifications: Vector[NotificationT]): GameState =
if notifications.isEmpty then gameState
else {
// Only add deferred notifications to the deferred list.
// Non-deferred notifications are for immediate delivery and don't affect game state.
val deferredOnly = notifications.filter(_.deferred)
if deferredOnly.isEmpty then gameState
else gameState.copy(deferredNotifications = gameState.deferredNotifications ++ deferredOnly)
}
else gameState.copy(deferredNotifications = gameState.deferredNotifications ++ notifications)
def applyRemovedNotifications(notifications: Vector[NotificationT]): GameState =
gameState.copy(
@@ -172,19 +172,25 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
],
deps = [
":check_for_faction_changes_action",
":hero_backstory_update_action_generator",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:battle_revelation_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_request_generators/captured_hero_helpers:captured_hero_plea_generator",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_against_former_on_exile",
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_from_exile",
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_from_imprisonment",
@@ -209,15 +215,18 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_aftermath_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battle_revelation_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_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/llm_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state:battle_revelation",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/faction:faction_relationship",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/province:deferred_change_trait",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/quest/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
@@ -256,6 +265,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_attack_decision_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
@@ -320,7 +330,6 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
@@ -329,17 +338,20 @@ scala_library(
":check_for_faction_changes_action",
":hero_backstory_update_action",
":hero_backstory_update_action_generator",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_status_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action/diplomacy_helpers:alliance_resolution_helpers",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action/diplomacy_helpers:break_alliance_resolution_helpers",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action/diplomacy_helpers:invitation_resolution_helpers",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action/diplomacy_helpers:ransom_resolution_helpers",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action/diplomacy_helpers:truce_resolution_helpers",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:quest_fulfillment_utils",
"//src/main/scala/net/eagle0/eagle/library/util/ransom_validity",
@@ -352,17 +364,20 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_diplomacy_resolution_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:ransom_invalidated_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_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:round_phase",
"//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/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/hero/backstory_version",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/quest/concrete",
@@ -385,15 +400,20 @@ scala_library(
deps = [
":check_for_faction_changes_action",
":hero_backstory_update_action_generator",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:vigor_xp_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/settings:prisoner_escape_chance",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/library/util:province_event_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
@@ -411,11 +431,20 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/types:prisoner_escaped_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:prisoner_move_took_effect_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:weather_took_effect_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_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/proto_converters/province:event",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province:deferred_change_trait",
"//src/main/scala/net/eagle0/eagle/model/state/province:event",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/quest/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
],
)
@@ -482,24 +511,32 @@ scala_library(
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:handle_riot_utils",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_handle_riots_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
@@ -674,7 +711,7 @@ scala_library(
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
],
deps = [
":check_for_faction_changes_action",
@@ -683,9 +720,9 @@ scala_library(
":hero_backstory_update_action_generator",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
@@ -697,7 +734,11 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_vassal_commands_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
"//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/hero",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/date",
@@ -855,18 +896,23 @@ scala_library(
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
],
deps = [
":chronicle_event_generator",
":hero_stat_gain_action",
":new_year_action",
"//src/main/protobuf/net/eagle0/eagle/common:date_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:profession_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:game_history",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/settings:empty_province_monthly_devastation_delta",
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_minimum_adjustment_per_round",
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_multiplier_per_round",
@@ -875,9 +921,10 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:over_hero_cap_loyalty_delta",
"//src/main/scala/net/eagle0/eagle/library/settings:over_resource_limit_loss",
"//src/main/scala/net/eagle0/eagle/library/settings:trust_delta_per_round",
"//src/main/scala/net/eagle0/eagle/library/util:date_utils",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/library/util:price_index_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
@@ -890,13 +937,18 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/chronicle_event",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:new_round_action_result_type",
"//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:unaffiliated_hero_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//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/llm_request/chronicle_event",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
],
@@ -1111,9 +1163,9 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
@@ -1284,15 +1336,15 @@ scala_library(
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/util:returning_heroes",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:province_view_filter",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
@@ -1301,17 +1353,18 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:client_text_visibility_extension_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_recon_resolution_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:recon_succeeded_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:incoming_end_turn_action_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province:incoming_end_turn_action",
],
)
@@ -1323,20 +1376,26 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator",
],
deps = [
":end_unaffiliated_hero_actions_phase_action",
":unaffiliated_hero_appeared_action",
":unaffiliated_hero_rejoined_action",
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_quest_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/name_generation_request",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/settings:free_hero_move_vigor_cost",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_free_hero_move",
"//src/main/scala/net/eagle0/eagle/library/settings:new_hero_chance",
@@ -1367,6 +1426,10 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/types:hero_changed_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:hero_moved_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:new_quests_result_type",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
@@ -1386,31 +1449,40 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/common:more_option",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:command_chooser",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
)
@@ -1422,27 +1494,36 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
)
@@ -1617,6 +1698,7 @@ scala_library(
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/province",
@@ -1791,16 +1873,16 @@ scala_library(
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
],
deps = [
":withdrawn_army_returns_home_action",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
@@ -1813,6 +1895,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_truce_turn_back_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:withdrawal_for_truce_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
@@ -1,23 +1,23 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.{GameId, RoundId}
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentUtils
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.EndAttackDecisionPhaseResultType
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.quest.concrete.TruceWithFactionQuest
import net.eagle0.eagle.model.state.quest.QuestT
import net.eagle0.eagle.model.state.RoundPhase
case class EndAttackDecisionPhaseAction(
gameId: GameId,
currentRoundId: RoundId,
currentDate: Date,
provinces: Vector[ProvinceT]
gameState: GameState
) extends ProtolessSequentialResultsAction {
private val gameId = gameState.gameId
private val currentRoundId = gameState.currentRoundId
private val currentDate = gameState.currentDate.get
private val provinces = gameState.provinces.values.toVector
override def results: Vector[ActionResultT] =
WithdrawnArmiesReturnHomeAction(
@@ -2,11 +2,25 @@ package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.internal.deferred_change.{
BlizzardEnded,
BlizzardStarted,
CapturedHeroExecuted,
CapturedHeroExiled,
CapturedHeroImprisoned,
CapturedHeroReturned,
DeferredChange,
DroughtEnded,
DroughtStarted,
EpidemicStarted,
PrisonerMoved,
PrisonerReturned
}
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.generated_text_request_generators.captured_hero_helpers.CapturedHeroPleaGenerator
import net.eagle0.eagle.library.actions.impl.action.EndBattleAftermathPhaseAction.RevelationChange
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, RandomStateTSequencer}
import net.eagle0.eagle.library.settings.{
FactionBiasAgainstFormerOnExile,
FactionBiasFromExile,
@@ -22,7 +36,12 @@ import net.eagle0.eagle.model.action_result.{ActionResultT, NotificationDetails,
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC, ChangedHeroC, NotificationC}
import net.eagle0.eagle.model.action_result.types.{CapturedHeroResolvedResultType, EndAftermathPhaseResultType}
import net.eagle0.eagle.model.proto_converters.{BattleRevelationConverter, NotificationConverter}
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.state.{BattleRevelation, RoundPhase}
import net.eagle0.eagle.model.state.faction.FactionRelationship
import net.eagle0.eagle.model.state.game_state.GameState
@@ -32,7 +51,6 @@ import net.eagle0.eagle.model.state.hero.{
CapturedHeroReturnedBackstoryEvent,
EventForHeroBackstoryT
}
import net.eagle0.eagle.model.state.province.{DeferredChange, DeferredChangeT}
import net.eagle0.eagle.model.state.unaffiliated_hero.concrete.UnaffiliatedHeroC
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroType
import net.eagle0.eagle.model.state.BattleRevelationType.{DidBattle, Unknown, Withdrew}
@@ -43,8 +61,8 @@ object EndBattleAftermathPhaseAction {
changedProvince: ChangedProvinceC
)
def allDeferredChanges(gameState: GameState): Vector[DeferredChangeT] =
gameState.provinces.values
def allDeferredChanges(gameStateProto: GameStateProto): Vector[DeferredChange] =
gameStateProto.provinces.values
.flatMap(_.deferredChanges)
.toVector
@@ -56,15 +74,16 @@ object EndBattleAftermathPhaseAction {
capturedHeroFactionId: FactionId,
newFactionBias: Option[Double],
oldFactionBias: Option[Double],
gameState: GameState,
gameStateProto: GameStateProto,
functionalRandom: FunctionalRandom,
newEventForHeroBackstoryDetails: EventForHeroBackstoryT
): RandomState[ActionResultT] = {
val hero = gameState.heroes(capturedHeroId)
val initialUh = UnaffiliatedHeroC(
val scalaGameState = GameStateConverter.fromProto(gameStateProto)
val scalaHero = scalaGameState.heroes(capturedHeroId)
val initialUh = UnaffiliatedHeroC(
heroId = capturedHeroId,
unaffiliatedHeroType = uhType,
lastFactionId = hero.factionId,
lastFactionId = scalaHero.factionId,
factionBiases = (
newFactionBias.map(b => actingFactionId -> b) ++ oldFactionBias.map(b => capturedHeroFactionId -> b)
).toMap
@@ -72,10 +91,10 @@ object EndBattleAftermathPhaseAction {
UnaffiliatedHeroUtils
.updatedForQuest(
gs = gameState,
gs = scalaGameState,
pid = provinceId,
uh = initialUh,
hero = hero,
hero = scalaHero,
functionalRandom = functionalRandom
)
.map { uh =>
@@ -102,22 +121,23 @@ object EndBattleAftermathPhaseAction {
}
def deferredChangeAR(
deferredChange: DeferredChangeT,
gameState: GameState,
deferredChange: DeferredChange,
gameStateProto: GameStateProto,
functionalRandom: FunctionalRandom
): RandomState[ActionResultT] =
deferredChange match {
case DeferredChange.CapturedHeroExiled(
case CapturedHeroExiled(
exiledHeroId,
provinceId,
exilingHeroId,
exilingFactionId,
prisonerFactionId
prisonerFactionId,
_ /* unknownFieldSet */
) =>
val notificationLlmRequest =
CapturedHeroPleaGenerator.exiledNotification(
gameId = gameState.gameId,
currentRoundId = gameState.currentRoundId,
gameId = gameStateProto.gameId,
currentRoundId = gameStateProto.currentRoundId,
capturedHeroId = exiledHeroId,
capturedFromFactionId = prisonerFactionId,
actingHeroId = exilingHeroId,
@@ -132,10 +152,10 @@ object EndBattleAftermathPhaseAction {
capturedHeroFactionId = prisonerFactionId,
newFactionBias = Some(FactionBiasFromExile.doubleValue),
oldFactionBias = Some(FactionBiasAgainstFormerOnExile.doubleValue),
gameState = gameState,
gameStateProto = gameStateProto,
functionalRandom = functionalRandom,
newEventForHeroBackstoryDetails = CapturedHeroExiledBackstoryEvent(
date = gameState.currentDate.get,
date = DateConverter.fromProto(gameStateProto.currentDate),
capturingFactionId = exilingFactionId,
capturingHeroId = exilingHeroId,
provinceId = provinceId
@@ -155,17 +175,18 @@ object EndBattleAftermathPhaseAction {
)
)
}
case DeferredChange.CapturedHeroExecuted(
case CapturedHeroExecuted(
capturedHeroId,
provinceId,
executingHeroId,
executingFactionId,
prisonerFactionId
prisonerFactionId,
_ /* unknownFieldSet */
) =>
val notificationLlmRequest =
CapturedHeroPleaGenerator.executedNotification(
gameId = gameState.gameId,
currentRoundId = gameState.currentRoundId,
gameId = gameStateProto.gameId,
currentRoundId = gameStateProto.currentRoundId,
capturedHeroId = capturedHeroId,
capturedFromFactionId = prisonerFactionId,
actingHeroId = executingHeroId,
@@ -201,17 +222,18 @@ object EndBattleAftermathPhaseAction {
),
functionalRandom
)
case DeferredChange.CapturedHeroImprisoned(
case CapturedHeroImprisoned(
capturedHeroId,
provinceId,
imprisoningHeroId,
imprisoningFactionId,
prisonerFactionId
prisonerFactionId,
_ /* unknownFieldSet */
) =>
val notificationLlmRequest =
CapturedHeroPleaGenerator.imprisonedNotification(
gameId = gameState.gameId,
currentRoundId = gameState.currentRoundId,
gameId = gameStateProto.gameId,
currentRoundId = gameStateProto.currentRoundId,
capturedHeroId = capturedHeroId,
actingHeroId = imprisoningHeroId,
actingFactionId = imprisoningFactionId,
@@ -226,10 +248,10 @@ object EndBattleAftermathPhaseAction {
capturedHeroFactionId = prisonerFactionId,
newFactionBias = Some(FactionBiasFromImprisonment.doubleValue),
oldFactionBias = None,
gameState = gameState,
gameStateProto = gameStateProto,
functionalRandom = functionalRandom,
newEventForHeroBackstoryDetails = CapturedHeroImprisonedBackstoryEvent(
date = gameState.currentDate.get,
date = DateConverter.fromProto(gameStateProto.currentDate),
capturingFactionId = imprisoningFactionId,
capturingHeroId = imprisoningHeroId,
provinceId = provinceId
@@ -249,16 +271,22 @@ object EndBattleAftermathPhaseAction {
)
)
}
case DeferredChange.CapturedHeroReturned(
case CapturedHeroReturned(
returnedHeroId,
actingHeroId,
fromProvinceId,
fromFactionId,
toProvinceId,
toFactionId
toFactionId,
_ /* unknownFieldSet */
) =>
val allFactions = gameState.factions.values
val truceEndDate = gameState.currentDate.get.addMonths(TruceMonthsFromReturningLeader.intValue)
val allFactionTs =
gameStateProto.factions.values.map(FactionConverter.fromProto)
val truceEndDate = DateConverter
.fromProto(gameStateProto.currentDate)
.addMonths(
TruceMonthsFromReturningLeader.intValue
)
RandomState(
ActionResultC(
actionResultType = CapturedHeroResolvedResultType,
@@ -269,7 +297,7 @@ object EndBattleAftermathPhaseAction {
heroId = returnedHeroId,
newEventsForHeroBackstory = Vector(
CapturedHeroReturnedBackstoryEvent(
date = gameState.currentDate.get,
date = DateConverter.fromProto(gameStateProto.currentDate),
capturingFactionId = fromFactionId,
capturingHeroId = actingHeroId,
provinceId = fromProvinceId
@@ -296,7 +324,7 @@ object EndBattleAftermathPhaseAction {
.factionRelationship(
by = fromFactionId,
of = toFactionId,
factions = allFactions
factions = allFactionTs
)
.copy(
relationshipLevel = FactionRelationship.RelationshipLevel.Truce,
@@ -311,7 +339,7 @@ object EndBattleAftermathPhaseAction {
.factionRelationship(
by = toFactionId,
of = fromFactionId,
factions = allFactions
factions = allFactionTs
)
.copy(
relationshipLevel = FactionRelationship.RelationshipLevel.Truce,
@@ -335,10 +363,12 @@ object EndBattleAftermathPhaseAction {
functionalRandom
)
case DeferredChange.Empty =>
throw new EagleInternalException("Empty deferred change")
// the rest are not for this phase
case _: DeferredChange.EpidemicStarted | _: DeferredChange.DroughtStarted | _: DeferredChange.DroughtEnded |
_: DeferredChange.PrisonerMoved | _: DeferredChange.PrisonerReturned | _: DeferredChange.BlizzardStarted |
_: DeferredChange.BlizzardEnded =>
case _: EpidemicStarted | _: DroughtStarted | _: DroughtEnded | _: PrisonerMoved | _: PrisonerReturned |
_: BlizzardStarted | _: BlizzardEnded =>
throw new EagleInternalException(
"Event should not be present in EndBattleAftermathPhaseAction"
)
@@ -347,18 +377,13 @@ object EndBattleAftermathPhaseAction {
case class EndBattleAftermathPhaseAction(
gameState: GameState,
actionResultApplier: ActionResultApplier
actionResultApplier: ActionResultTApplier
) extends ProtolessRandomSequentialResultsAction {
// Revelation changes still need proto for ProvinceViewFilter
// Convert to proto for internal use - proto still needed for DeferredChange, ProvinceViewFilter, etc.
private val gameStateProto: GameStateProto = GameStateConverter.toProto(gameState)
private def revelationChange(
battleRevelation: BattleRevelation,
gs: GameState
): RevelationChange = {
// Lazy conversion to proto for ProvinceViewFilter calls
lazy val gsProto = GameStateConverter.toProto(gs)
lazy val provinceProto = gsProto.provinces(battleRevelation.provinceId)
battleRevelation: BattleRevelation
): RevelationChange =
RevelationChange(
changedFaction = battleRevelation.revelationType match {
case Unknown =>
@@ -369,11 +394,11 @@ case class EndBattleAftermathPhaseAction(
updatedReconnedProvinces = Vector(
ProvinceViewFilter
.withdrawnFromProvinceView(
province = provinceProto,
gs = gsProto,
province = gameStateProto.provinces(battleRevelation.provinceId),
gs = gameStateProto,
factionId = battleRevelation.revealedToFactionId
)
.withAsOf(gsProto.currentDate.get)
.withAsOf(gameStateProto.currentDate.get)
)
)
case DidBattle =>
@@ -382,12 +407,12 @@ case class EndBattleAftermathPhaseAction(
updatedReconnedProvinces = Vector(
ProvinceViewFilter
.filteredProvinceView(
provinceProto,
gsProto
gameStateProto.provinces(battleRevelation.provinceId),
gameStateProto
// FIXME: setting factionId to None right now to grab the full info. This
// probably isn't exactly what we want.
)
.withAsOf(gsProto.currentDate.get)
.withAsOf(gameStateProto.currentDate.get)
)
)
},
@@ -396,26 +421,27 @@ case class EndBattleAftermathPhaseAction(
removedBattleRevelations = Vector(battleRevelation)
)
)
}
def revelationChanges(gs: GameState): Vector[RevelationChange] =
def revelationChanges(gs: GameStateProto): Vector[RevelationChange] =
for {
province <- gs.provinces.values.toVector
revelation <- province.battleRevelations
if gs.factions.contains(revelation.revealedToFactionId)
} yield revelationChange(revelation, gs)
} yield revelationChange(BattleRevelationConverter.fromProto(revelation))
def sequencerWithDeferredChanges(
initialState: GameState,
actionResultApplier: ActionResultApplier,
actionResultApplier: ActionResultTApplier,
functionalRandom: FunctionalRandom
): RandomStateSequencer =
RandomStateSequencer(
): RandomStateTSequencer =
RandomStateTSequencer(
initialState = initialState,
actionResultApplier = actionResultApplier,
functionalRandom = functionalRandom
)
.foldIn(EndBattleAftermathPhaseAction.allDeferredChanges(initialState))(
.foldIn(
EndBattleAftermathPhaseAction.allDeferredChanges(GameStateConverter.toProto(initialState))
)(
EndBattleAftermathPhaseAction.deferredChangeAR
)
@@ -441,25 +467,30 @@ case class EndBattleAftermathPhaseAction(
.withRandomActionResults((gs, fr) =>
CheckForFactionChangesAction(
gameId = gs.gameId,
factions = gs.factions.values.toVector,
provinces = gs.provinces.values.toVector,
heroes = gs.heroes.values.toVector,
factions = gs.factions.values.toVector.map(FactionConverter.fromProto),
provinces = gs.provinces.values.toVector.map(ProvinceConverter.fromProto),
heroes = gs.heroes.values.toVector.map(HeroConverter.fromProto),
killedHeroIds = gs.killedHeroes.keys.toVector
).randomResults(fr)
)
.withActionResults(gs => HeroBackstoryUpdateActionGenerator(gs).results)
.withRandomActionResult { (gs, fr) =>
RandomState(
ActionResultC(
actionResultType = EndAftermathPhaseResultType,
changedFactions = revelationChanges(gs).map(_.changedFaction),
changedProvinces = revelationChanges(gs).map(_.changedProvince),
newRoundPhase = Some(RoundPhase.DiplomacyResolution),
removedNotifications = gs.deferredNotifications.map(_.withDeferred(true)),
newNotifications = gs.deferredNotifications.map(_.withDeferred(false))
),
fr
)
.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator.fromGameState(gs))
.withRandomActionResult {
case (gs, fr) =>
RandomState(
ActionResultC(
actionResultType = EndAftermathPhaseResultType,
changedFactions = revelationChanges(gs).map(_.changedFaction),
changedProvinces = revelationChanges(gs).map(_.changedProvince),
newRoundPhase = Some(RoundPhase.DiplomacyResolution),
removedNotifications = gs.deferredNotifications
.map(note => NotificationConverter.fromProto(note, deferred = true))
.toVector,
newNotifications = gs.deferredNotifications
.map(note => NotificationConverter.fromProto(note, deferred = false))
.toVector
),
fr
)
}
.actionResults
}
@@ -1,7 +1,8 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.action.diplomacy_helpers.{
AllianceResolutionHelpers,
BreakAllianceResolutionHelpers,
@@ -9,8 +10,7 @@ import net.eagle0.eagle.library.actions.impl.action.diplomacy_helpers.{
RansomResolutionHelpers,
TruceResolutionHelpers
}
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, RandomStateTSequencer}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentChecker
import net.eagle0.eagle.library.util.ransom_validity.RansomValidity
@@ -18,6 +18,11 @@ import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC}
import net.eagle0.eagle.model.action_result.types.{EndDiplomacyResolutionPhaseResultType, RansomInvalidatedResultType}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.{BattalionConverter, NotificationConverter}
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.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.diplomacy_offer.{AllianceOffer, BreakAlliance, Invitation, RansomOffer, TruceOffer}
@@ -30,16 +35,16 @@ import net.eagle0.eagle.model.state.RoundPhase.ReconResolution
case class EndDiplomacyResolutionPhaseAction(
gameState: GameState,
actionResultApplier: ActionResultApplier
actionResultTApplier: ActionResultTApplier
) extends ProtolessRandomSequentialResultsAction {
override def randomResults(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
import EndDiplomacyResolutionPhaseActionHelpers.*
RandomStateSequencer(
RandomStateTSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
actionResultApplier = actionResultTApplier,
functionalRandom = functionalRandom
)
.withActionResults(invalidationResultsForState)
@@ -51,9 +56,15 @@ case class EndDiplomacyResolutionPhaseAction(
.withRandomActionResults { (currentGameState, fr) =>
CheckForFactionChangesAction(
gameId = currentGameState.gameId,
factions = currentGameState.factions.values.toVector,
provinces = currentGameState.provinces.values.toVector,
heroes = currentGameState.heroes.values.toVector,
factions = currentGameState.factions.values.toVector.map(
FactionConverter.fromProto
),
provinces = currentGameState.provinces.values.toVector.map(
ProvinceConverter.fromProto
),
heroes = currentGameState.heroes.values.toVector.map(
HeroConverter.fromProto
),
killedHeroIds = currentGameState.killedHeroes.keys.toVector
).randomResults(fr)
}
@@ -61,14 +72,20 @@ case class EndDiplomacyResolutionPhaseAction(
HeroBackstoryUpdateAction(
gameId = currentGameState.gameId,
roundId = currentGameState.currentRoundId,
heroes = currentGameState.heroes.values.toVector,
heroes = currentGameState.heroes.values
.map(HeroConverter.fromProto)
.toVector,
visibleToFactionIds = toFid =>
FactionUtils.alliedFactions(
toFid,
currentGameState.factions.values.toVector
currentGameState.factions.values
.map(FactionConverter.fromProto)
.toVector
),
heroInProvinceOwnedBy = heroId => {
val provinces = currentGameState.provinces.values.toVector
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
provinces
.find(province =>
province.rulingFactionHeroIds.contains(heroId) ||
@@ -92,16 +109,19 @@ case class EndDiplomacyResolutionPhaseAction(
* compiler can become confused about which one is being referenced during imports.
*
* All helper methods take explicit parameters for game state components to ensure they always operate on the current
* state as provided by RandomStateSequencer, preventing use of stale data from the initial game state.
* state as provided by RandomStateTSequencer, preventing use of stale data from the initial game state.
*/
private object EndDiplomacyResolutionPhaseActionHelpers {
// Methods that use the updated game state from the sequencer
def invalidationResultsForState(
currentGameState: GameState
currentGameState: GameStateProto
): Vector[ActionResultT] = {
val factions = currentGameState.factions.values.toVector
val provinces = currentGameState.provinces.values.toVector
val factions =
currentGameState.factions.values.map(FactionConverter.fromProto).toVector
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
for {
faction <- factions
@@ -129,9 +149,11 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
}
def endPhaseResultForState(
currentGameState: GameState
currentGameState: GameStateProto
): ActionResultT = {
val deferredNotifications = currentGameState.deferredNotifications
.map(n => NotificationConverter.fromProto(n, deferred = true))
.toVector
ActionResultC(
actionResultType = EndDiplomacyResolutionPhaseResultType,
@@ -141,6 +163,9 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
)
}
private def factionTs(gs: GameStateProto): Vector[FactionT] =
gs.factions.values.map(FactionConverter.fromProto).toVector
private def resolutionsForType[A](
getter: FactionT => Vector[A],
resolver: A => Vector[ActionResultT]
@@ -164,15 +189,17 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
}
def truceResolutionsForState(
currentGameState: GameState,
currentGameState: GameStateProto,
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
val factions = currentGameState.factions.values.toVector
val factions = factionTs(currentGameState)
randomResolutionsForType(
_.incomingDiplomacyOffers.collect { case to: TruceOffer => to },
(to: TruceOffer, fr: FunctionalRandom) => {
val provinces = currentGameState.provinces.values.toVector
val currentDate = currentGameState.currentDate.get
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
val currentDate = DateConverter.fromProto(currentGameState.currentDate)
resultsForTruceOffer(
to,
fr,
@@ -187,16 +214,19 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
}
def allianceResolutionsForState(
currentGameState: GameState,
currentGameState: GameStateProto,
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
val factions = currentGameState.factions.values.toVector
val factions = factionTs(currentGameState)
randomResolutionsForType(
_.incomingDiplomacyOffers.collect { case ao: AllianceOffer => ao },
(ao: AllianceOffer, fr: FunctionalRandom) => {
val provinces = currentGameState.provinces.values.toVector
val heroes = currentGameState.heroes.values.toVector
val currentDate = currentGameState.currentDate.get
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
val heroes =
currentGameState.heroes.values.map(HeroConverter.fromProto).toVector
val currentDate = DateConverter.fromProto(currentGameState.currentDate)
resultsForAllianceOffer(
ao,
fr,
@@ -211,15 +241,17 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
}
def breakAllianceResolutionsForState(
currentGameState: GameState,
currentGameState: GameStateProto,
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
val factions = currentGameState.factions.values.toVector
val factions = factionTs(currentGameState)
randomResolutionsForType(
_.incomingDiplomacyOffers.collect { case ba: BreakAlliance => ba },
(ba: BreakAlliance, fr: FunctionalRandom) => {
val provinces = currentGameState.provinces.values.toVector
val currentDate = currentGameState.currentDate.get
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
val currentDate = DateConverter.fromProto(currentGameState.currentDate)
resultsForBreakAlliance(ba, fr, provinces, factions, currentDate)
},
functionalRandom
@@ -227,16 +259,20 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
}
def inviteResolutionsForState(
currentGameState: GameState,
currentGameState: GameStateProto,
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
val factions = currentGameState.factions.values.toVector
val factions = factionTs(currentGameState)
randomResolutionsForType(
_.incomingDiplomacyOffers.collect { case id: Invitation => id },
(id: Invitation, fr: FunctionalRandom) => {
val provinces = currentGameState.provinces.values.toVector
val battalions = currentGameState.battalions.values.toVector
val currentDate = currentGameState.currentDate.get
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
val battalions = currentGameState.battalions.values
.map(BattalionConverter.fromProto)
.toVector
val currentDate = DateConverter.fromProto(currentGameState.currentDate)
resultsForInvitation(
id,
fr,
@@ -251,16 +287,18 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
}
def ransomResolutionsForState(
currentGameState: GameState
currentGameState: GameStateProto
): Vector[ActionResultT] = {
val factions = currentGameState.factions.values.toVector
val factions = factionTs(currentGameState)
resolutionsForType(
_.incomingDiplomacyOffers.collect { case ro: RansomOffer => ro },
(ro: RansomOffer) => {
val provinces = currentGameState.provinces.values.toVector
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
resultsForRansomOffer(
ro,
currentGameState.currentDate.get,
DateConverter.fromProto(currentGameState.currentDate),
provinces,
currentGameState
)
@@ -272,7 +310,7 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
ransomOffer: RansomOffer,
currentDate: Date,
provinces: Vector[ProvinceT],
gameState: GameState
gameState: GameStateProto
): Vector[ActionResultT] = ransomOffer.status match {
case Accepted =>
RansomResolutionHelpers.acceptedRansomResults(
@@ -283,7 +321,7 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
gameId = gameState.gameId,
currentDate = currentDate,
currentRoundId = gameState.currentRoundId,
previousBackstoryTextIdLookup = hid => gameState.heroes(hid).backstoryVersions.last.textId
previousBackstoryTextIdLookup = hid => gameState.heroes(hid).backstoryVersions.toVector.last.textId
)
)
case Rejected =>
@@ -310,7 +348,7 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
provinces: Vector[ProvinceT],
factions: Vector[FactionT],
currentDate: Date,
gameState: GameState
gameState: GameStateProto
): RandomState[Vector[ActionResultT]] =
truceOffer.status match {
case Accepted =>
@@ -2,93 +2,78 @@ package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.command.{HandleRiotUtils, TCommandFactory}
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, TCommand}
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.command.{CommandFactory, HandleRiotUtils}
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, RandomStateTSequencer}
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.EndHandleRiotsPhaseResultType
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.RoundPhase
import net.eagle0.eagle.ProvinceId
case class EndHandleRiotsPhaseAction(
gameState: GameState,
commandsForProvince: ProvinceId => Option[OneProvinceAvailableCommands],
commandFactory: TCommandFactory,
actionResultApplier: ActionResultApplier
commandFactory: CommandFactory,
applier: ActionResultTApplier
) extends ProtolessRandomSequentialResultsAction {
private def provincesWithImminentRiot(gs: GameState): Vector[ProvinceT] =
gs.provinces.values.filter(ProvinceUtils.hasImminentRiot).toVector
private def vassalCommandResults(
sequencer: RandomStateSequencer
): RandomStateSequencer = {
val provincesToProcess = provincesWithImminentRiot(sequencer.lastState)
ars: RandomStateTSequencer
): RandomStateTSequencer =
ars.lastStateProto.provinces.values
.filter(LegacyProvinceUtils.hasImminentRiot)
.filterNot(_.hasActed)
provincesToProcess.foldLeft(sequencer) {
case (seq, province) =>
commandsForProvince(province.id).map { opac =>
seq.withRandomActionResults { (gs, fr) =>
// Convert to proto for CommandChoiceHelpers which expects proto GameState
val gsProto = GameStateConverter.toProto(gs)
CommandChoiceHelpers
.handleRiotSelectedCommand(
actingFactionId = province.rulingFactionId.get,
gameState = gsProto,
availableCommands = opac.commands.toVector,
functionalRandom = fr
)
.continue {
case (Some(cs), nextFr) =>
val cmd = commandFactory.makeTCommand(
actingFactionId = province.rulingFactionId.get,
gameState = gs,
availableCommand = cs.available,
selectedCommand = cs.selected
)
cmd match {
case TCommand.Simple(action) =>
RandomState(Vector(action.immediateExecute), nextFr)
case TCommand.RandomSimple(action) =>
action.immediateExecute(nextFr).map(ar => Vector(ar))
case TCommand.Sequential(action) =>
RandomState(action.results, nextFr)
.foldLeft(ars) {
case (sequencer, p) =>
commandsForProvince(p.id).map { opac =>
sequencer.withOptionalRandomTCommand { (gs, fr) =>
CommandChoiceHelpers
.handleRiotSelectedCommand(
actingFactionId = p.getRulingFactionId,
gameState = gs,
availableCommands = opac.commands.toVector,
functionalRandom = fr
)
.map { optCS =>
optCS.map { cs =>
commandFactory.makeTCommand(
actingFactionId = p.getRulingFactionId,
gameState = GameStateConverter.fromProto(gs),
availableCommand = cs.available,
selectedCommand = cs.selected
)
}
case (None, nextFr) =>
RandomState(Vector.empty[ActionResultT], nextFr)
}
}
}
.getOrElse(seq)
}
}
}
}
}.get
}
private def riotOccurredResults(
sequencer: RandomStateSequencer
): RandomStateSequencer =
provincesWithImminentRiot(sequencer.lastState).foldLeft(sequencer) {
case (seq, province) =>
seq.withActionResult(_ =>
HandleRiotUtils.riotOccurredAr(
province.rulingFactionId.get,
province
ars: RandomStateTSequencer
): RandomStateTSequencer =
ars.lastStateProto.provinces.values
.filter(LegacyProvinceUtils.hasImminentRiot)
.foldLeft(ars) {
case (ars, p) =>
ars.withActionResult(_ =>
HandleRiotUtils
.riotOccurredAr(
p.getRulingFactionId,
ProvinceConverter.fromProto(p)
)
)
)
}
}
private def endPhaseResult(
sequencer: RandomStateSequencer
): RandomStateSequencer =
sequencer.withActionResult { gs =>
ars: RandomStateTSequencer
): RandomStateTSequencer =
ars.withActionResultT(gs =>
ActionResultC(
actionResultType = EndHandleRiotsPhaseResultType,
newRoundPhase = Some(RoundPhase.HeroDepartures),
@@ -96,17 +81,17 @@ case class EndHandleRiotsPhaseAction(
.filter(_.hasActed)
.map(p => ChangedProvinceC(provinceId = p.id, setHasActed = Some(false)))
.toVector,
removedNotifications = gs.deferredNotifications,
newNotifications = gs.deferredNotifications.map(_.withDeferred(false))
removedNotifications = gameState.deferredNotifications,
newNotifications = gameState.deferredNotifications.map(_.withDeferred(false))
)
}
)
override def randomResults(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] =
RandomStateSequencer(
RandomStateTSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
actionResultApplier = applier,
functionalRandom = functionalRandom
)
.withRandomContinuance(vassalCommandResults)
@@ -1,12 +1,19 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, VigorXPApplier}
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.internal.deferred_change.*
import net.eagle0.eagle.internal.deferred_change.DeferredChange.Empty
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.common.{
ProtolessRandomSequentialResultsAction,
RandomStateTSequencer,
VigorXPApplier
}
import net.eagle0.eagle.library.settings.PrisonerEscapeChance
import net.eagle0.eagle.library.util.province.ProvinceUtils
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.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
@@ -19,14 +26,14 @@ import net.eagle0.eagle.model.action_result.types.{
WeatherTookEffectResultType
}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.{NotificationConverter, UnaffiliatedHeroConverter}
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, ProvinceEventConverter}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.{
BlizzardEvent,
DeferredChange,
DeferredChangeT,
DroughtEvent,
EpidemicEvent
}
import net.eagle0.eagle.model.state.province.{BlizzardEvent, DroughtEvent, EpidemicEvent}
import net.eagle0.eagle.model.state.unaffiliated_hero.RecruitmentInfo
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroType.{Outlaw, Prisoner}
import net.eagle0.eagle.model.state.RoundPhase
@@ -34,11 +41,13 @@ import net.eagle0.eagle.ProvinceId
case class EndPlayerCommandsPhaseAction(
gameState: GameState,
actionResultApplier: ActionResultApplier
applier: ActionResultTApplier
) extends ProtolessRandomSequentialResultsAction {
private val gameStateProto: GameStateProto = GameStateConverter.toProto(gameState)
private def endPhaseResult(gs: GameState): ActionResultT = {
private def endPhaseResult(gs: GameStateProto): ActionResultT = {
val abandonedProvinces: Vector[ChangedProvinceT] = gs.provinces.values
.map(ProvinceConverter.fromProto)
.filter(_.rulingFactionId.isDefined)
.flatMap(ProvinceUtils.checkedForAbandonment)
.toVector
@@ -57,22 +66,27 @@ case class EndPlayerCommandsPhaseAction(
)
)
},
removedNotifications = gs.deferredNotifications.map(_.withDeferred(true)),
newNotifications = gs.deferredNotifications.map(_.withDeferred(false))
removedNotifications = gameStateProto.deferredNotifications.map { note =>
NotificationConverter.fromProto(note, deferred = true)
}.toVector,
newNotifications = gameStateProto.deferredNotifications.map { note =>
NotificationConverter.fromProto(note, deferred = false)
}.toVector
)
)
}
private def onePrisonerMovedChange(
pid: ProvinceId,
prisonerMoved: DeferredChange.PrisonerMoved,
gs: GameState,
prisonerMoved: PrisonerMoved,
gs: GameStateProto,
fr: FunctionalRandom
): RandomState[ActionResultT] = {
val uh = gs
.provinces(pid)
.unaffiliatedHeroes
.find(_.heroId == prisonerMoved.heroId)
.map(UnaffiliatedHeroConverter.fromProto)
fr.nextDouble.map { doubleValue =>
val escaped = doubleValue < PrisonerEscapeChance.doubleValue
@@ -112,8 +126,8 @@ case class EndPlayerCommandsPhaseAction(
private def onePrisonerReturnedChange(
pid: ProvinceId,
prisonerReturned: DeferredChange.PrisonerReturned,
gs: GameState,
prisonerReturned: PrisonerReturned,
gs: GameStateProto,
fr: FunctionalRandom
): RandomState[ActionResultT] =
RandomState(
@@ -145,28 +159,28 @@ case class EndPlayerCommandsPhaseAction(
private def notPrisonerChange(
pid: ProvinceId,
deferredChange: DeferredChangeT,
gs: GameState,
deferredChange: DeferredChange,
gs: GameStateProto,
fr: FunctionalRandom
): RandomState[ActionResultT] = {
val currentDate = gs.currentDate.get
val province = gs.provinces(pid)
): RandomState[ActionResultT] =
RandomState(
VigorXPApplier.withVigorXp(
ActionResultC(
actionResultType = deferredChange match {
case _: DeferredChange.EpidemicStarted => EpidemicTookEffectResultType
case _: DeferredChange.BlizzardEnded => WeatherTookEffectResultType
case _: DeferredChange.BlizzardStarted => WeatherTookEffectResultType
case _: DeferredChange.DroughtStarted => WeatherTookEffectResultType
case _: DeferredChange.DroughtEnded => WeatherTookEffectResultType
case _: DeferredChange.PrisonerMoved | _: DeferredChange.PrisonerReturned |
_: DeferredChange.CapturedHeroImprisoned | _: DeferredChange.CapturedHeroExecuted |
_: DeferredChange.CapturedHeroExiled | _: DeferredChange.CapturedHeroReturned =>
case _: EpidemicStarted => EpidemicTookEffectResultType
case _: BlizzardEnded => WeatherTookEffectResultType
case _: BlizzardStarted => WeatherTookEffectResultType
case _: DroughtStarted => WeatherTookEffectResultType
case _: DroughtEnded => WeatherTookEffectResultType
case _: PrisonerMoved | _: PrisonerReturned | _: CapturedHeroImprisoned | _: CapturedHeroExecuted |
_: CapturedHeroExiled | _: CapturedHeroReturned =>
throw new EagleInternalException(
"Prisoner management changes should not be here"
)
case DeferredChange.Empty =>
throw new EagleInternalException(
"Empty deferred change should not be here"
)
},
provinceId = Some(pid),
changedProvinces = Vector(
@@ -174,31 +188,70 @@ case class EndPlayerCommandsPhaseAction(
provinceId = pid,
newProvinceEvents = Some(
deferredChange match {
case DeferredChange.BlizzardStarted(_, durationMonths) =>
province.activeEvents :+ BlizzardEvent(
startDate = currentDate,
endDate = currentDate.addMonths(durationMonths)
case BlizzardStarted(
_,
durationMonths,
_ /* unknownFieldSet */
) =>
gs.provinces(pid)
.activeEvents
.map(ProvinceEventConverter.fromProto)
.toVector :+ BlizzardEvent(
startDate = DateConverter.fromProto(gs.currentDate),
endDate = DateConverter
.fromProto(gs.currentDate)
.addMonths(
durationMonths
)
)
case _: DeferredChange.BlizzardEnded =>
province.activeEvents.filter { case _: BlizzardEvent => false; case _ => true }
case BlizzardEnded(_, _ /* unknownFieldSet */ ) =>
gs.provinces(pid)
.activeEvents
.filterNot(ProvinceEventUtils.isBlizzardEvent)
.map(ProvinceEventConverter.fromProto)
.toVector
case DeferredChange.DroughtStarted(_, durationMonths) =>
province.activeEvents :+ DroughtEvent(
startDate = currentDate,
endDate = currentDate.addMonths(durationMonths)
case DroughtStarted(
_,
durationMonths,
_ /* unknownFieldSet */
) =>
gs.provinces(pid)
.activeEvents
.map(ProvinceEventConverter.fromProto)
.toVector :+ DroughtEvent(
startDate = DateConverter.fromProto(gs.currentDate),
endDate = DateConverter
.fromProto(gs.currentDate)
.addMonths(durationMonths)
)
case _: DeferredChange.DroughtEnded =>
province.activeEvents.filter { case _: DroughtEvent => false; case _ => true }
case _: DeferredChange.EpidemicStarted =>
province.activeEvents :+ EpidemicEvent(startDate = currentDate)
case DroughtEnded(_, _ /* unknownFieldSet */ ) =>
gs.provinces(pid)
.activeEvents
.filterNot(ProvinceEventUtils.isDroughtEvent)
.map(ProvinceEventConverter.fromProto)
.toVector
case _: DeferredChange.PrisonerMoved => Vector()
case _: DeferredChange.PrisonerReturned => Vector()
case _: DeferredChange.CapturedHeroImprisoned => Vector()
case _: DeferredChange.CapturedHeroExecuted => Vector()
case _: DeferredChange.CapturedHeroExiled => Vector()
case _: DeferredChange.CapturedHeroReturned => Vector()
case EpidemicStarted(_, _ /* unknownFieldSet */ ) =>
gs.provinces(pid)
.activeEvents
.map(ProvinceEventConverter.fromProto)
.toVector :+ EpidemicEvent(
startDate = DateConverter.fromProto(gs.currentDate)
)
case _: PrisonerMoved => Vector()
case _: PrisonerReturned => Vector()
case _: CapturedHeroImprisoned => Vector()
case _: CapturedHeroExecuted => Vector()
case _: CapturedHeroExiled => Vector()
case _: CapturedHeroReturned => Vector()
case Empty =>
throw new EagleInternalException(
"Empty deferred change should not be here"
)
}
),
removedDeferredChangeIndex = Some(0)
@@ -208,27 +261,26 @@ case class EndPlayerCommandsPhaseAction(
),
fr
)
}
private def oneDeferredProvinceChange(
pid: ProvinceId,
deferredChange: DeferredChangeT,
gs: GameState,
deferredChange: DeferredChange,
gs: GameStateProto,
fr: FunctionalRandom
): RandomState[ActionResultT] =
deferredChange match {
case prisonerMoved: DeferredChange.PrisonerMoved =>
case prisonerMoved: PrisonerMoved =>
onePrisonerMovedChange(pid, prisonerMoved, gs, fr)
case prisonerReturned: DeferredChange.PrisonerReturned =>
case prisonerReturned: PrisonerReturned =>
onePrisonerReturnedChange(pid, prisonerReturned, gs, fr)
case _ => notPrisonerChange(pid, deferredChange, gs, fr)
case _ => notPrisonerChange(pid, deferredChange, gs, fr)
}
private def deferredProvinceChangesResultsForProvince(
pid: ProvinceId,
sequencer: RandomStateSequencer
): RandomStateSequencer =
sequencer.lastState.provinces(pid).deferredChanges.foldLeft(sequencer) {
arsRS: RandomStateTSequencer
): RandomStateTSequencer =
arsRS.lastStateProto.provinces(pid).deferredChanges.foldLeft(arsRS) {
case (acc, dc) =>
acc.withRandomActionResult {
case (gs, fr) =>
@@ -237,40 +289,41 @@ case class EndPlayerCommandsPhaseAction(
}
private def deferredProvinceChangesResults(
sequencer: RandomStateSequencer
): RandomStateSequencer =
sequencer.lastState.provinces.keys
.foldLeft(sequencer) {
case (newSequencer, pid) =>
deferredProvinceChangesResultsForProvince(pid, newSequencer)
ars: RandomStateTSequencer
): RandomStateTSequencer =
ars.lastStateProto.provinces.keys
.foldLeft(ars) {
case (newArs, pid) =>
deferredProvinceChangesResultsForProvince(pid, newArs)
}
override def randomResults(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
gameState.provinces.values.foreach { p =>
gameStateProto.provinces.values.foreach { p =>
internalRequire(
!p.rulerIsTraveling,
s"Leader is traveling at the end of the PlayerCommandsPhase in province ${p.id}"
)
}
RandomStateSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
functionalRandom = functionalRandom
)
RandomStateTSequencer
.fromProto(
initialStateProto = gameStateProto,
actionResultApplier = applier,
functionalRandom = functionalRandom
)
.withRandomActionResults((gs, fr) =>
CheckForFactionChangesAction(
gameId = gs.gameId,
factions = gs.factions.values.toVector,
provinces = gs.provinces.values.toVector,
heroes = gs.heroes.values.toVector,
factions = gs.factions.values.toVector.map(FactionConverter.fromProto),
provinces = gs.provinces.values.toVector.map(ProvinceConverter.fromProto),
heroes = gs.heroes.values.toVector.map(HeroConverter.fromProto),
killedHeroIds = gs.killedHeroes.keys.toVector
).randomResults(fr)
)
.withContinuance(deferredProvinceChangesResults)
.withActionResults(gs => HeroBackstoryUpdateActionGenerator(gs).results)
.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator.fromGameState(gs))
.withActionResult(endPhaseResult)
.actionResults
}
@@ -1,23 +1,24 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.common.{RandomStateTSequencer, TRandomSequentialResultsAction}
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.EndVassalCommandsPhaseResultType
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
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.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.RoundPhase
case class EndVassalCommandsPhaseAction(
gameState: GameState,
actionResultApplier: ActionResultApplier
) extends ProtolessRandomSequentialResultsAction {
case class EndVassalCommandsPhaseAction(gameState: GameState) extends TRandomSequentialResultsAction(gameState) {
override protected def randomResults(
functionalRandom: FunctionalRandom
functionalRandom: FunctionalRandom,
actionResultTApplier: ActionResultTApplier
): RandomState[Vector[ActionResultT]] = {
gameState.provinces.values.foreach { p =>
internalRequire(
@@ -26,53 +27,56 @@ case class EndVassalCommandsPhaseAction(
)
}
RandomStateSequencer(
RandomStateTSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
actionResultApplier = actionResultTApplier,
functionalRandom = functionalRandom
)
.withActionResults(gs =>
.withProtolessSequentialResultsAction(gs =>
CheckForFulfilledQuestsAction(
gameId = gs.gameId,
currentDate = gs.currentDate.get,
currentDate = DateConverter.fromProto(gs.currentDate),
currentRoundId = gs.currentRoundId,
provinces = gs.provinces.values.toVector,
factions = gs.factions.values.toVector,
battalions = gs.battalions.values.toVector,
getHero = hid => gs.heroes.get(hid),
battalionTypes = gs.battalionTypes.map(BattalionTypeConverter.toProto),
hid => gs.heroes(hid).backstoryTextId
).results
provinces = gs.provinces.values.map(ProvinceConverter.fromProto).toVector,
factions = gs.factions.values
.map(FactionConverter.fromProto)
.toVector,
battalions = gs.battalions.values.toVector.map(BattalionConverter.fromProto),
getHero = hid => gs.heroes.get(hid).map(HeroConverter.fromProto),
battalionTypes = gs.battalionTypes.toVector,
hid => gs.heroes(hid).backstoryVersions.last.textId
)
)
.withActionResults(gs =>
.withProtolessSequentialResultsAction(gs =>
CheckForFailedQuestsAction(
gameId = gs.gameId,
currentDate = gs.currentDate.get,
currentDate = DateConverter.fromProto(gs.currentDate),
currentRoundId = gs.currentRoundId,
provinces = gs.provinces.values.toVector,
factions = gs.factions.values.toVector,
hid => gs.heroes(hid).backstoryTextId
).results
provinces = gs.provinces.values.map(ProvinceConverter.fromProto).toVector,
factions = gs.factions.values
.map(FactionConverter.fromProto)
.toVector,
hid => gs.heroes(hid).backstoryVersions.last.textId
)
)
.withRandomActionResults((gs, fr) =>
CheckForFactionChangesAction(
gameId = gs.gameId,
factions = gs.factions.values.toVector,
provinces = gs.provinces.values.toVector,
heroes = gs.heroes.values.toVector,
factions = gs.factions.values.map(FactionConverter.fromProto).toVector,
provinces = gs.provinces.values.map(ProvinceConverter.fromProto).toVector,
heroes = gs.heroes.values.map(HeroConverter.fromProto).toVector,
killedHeroIds = gs.killedHeroes.keys.toVector
).randomResults(fr)
)
.withActionResults(gs => HeroBackstoryUpdateActionGenerator(gs).results)
.withActionResult { gs =>
// Use current state's deferred notifications, not the initial gameState
.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator.fromGameState(gs))
.withActionResultT(_ =>
ActionResultC(
actionResultType = EndVassalCommandsPhaseResultType,
newRoundPhase = Some(RoundPhase.PlayerCommands),
removedNotifications = gs.deferredNotifications.map(_.withDeferred(true)),
newNotifications = gs.deferredNotifications.map(_.withDeferred(false))
removedNotifications = gameState.deferredNotifications,
newNotifications = gameState.deferredNotifications.map(_.withDeferred(false))
)
}
)
.actionResults
}
}
@@ -2,9 +2,9 @@ package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.{HeroId, ProvinceId, RoundId}
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.common.{RandomStateTSequencer, TRandomSequentialResultsAction}
import net.eagle0.eagle.library.settings.{
EmptyProvinceMonthlyDevastationDelta,
FactionBiasMinimumAdjustmentPerRound,
@@ -15,7 +15,8 @@ import net.eagle0.eagle.library.settings.{
OverResourceLimitLoss,
TrustDeltaPerRound
}
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.library.util.DateProtoUtils._Date
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.util.PriceIndexUtils
import net.eagle0.eagle.library.GameHistory
@@ -31,34 +32,37 @@ import net.eagle0.eagle.model.action_result.concrete.{
import net.eagle0.eagle.model.action_result.generated_text_request.{ChronicleUpdatePreviousEntry, LlmRequestT}
import net.eagle0.eagle.model.action_result.types.NewRoundActionResultType
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.generated_text_request.chronicle_event.ChronicleEventConverter
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.FactionRelationship
import net.eagle0.eagle.model.state.faction.FactionRelationship.RelationshipLevel
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroT
import net.eagle0.eagle.model.state.RoundPhase
case class NewRoundAction(
gameState: GameState,
gameHistory: GameHistory,
actionResultApplier: ActionResultApplier
) extends ProtolessRandomSequentialResultsAction {
case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
extends TRandomSequentialResultsAction(gameState) {
private val startingState: GameStateProto = GameStateConverter.toProto(gameState)
private val chronicleMonths = Vector(11)
private def isChronicleMonth(newDate: Date): Boolean =
chronicleMonths.contains(newDate.month.value)
override def randomResults(
functionalRandom: FunctionalRandom
override protected def randomResults(
functionalRandom: FunctionalRandom,
actionResultTApplier: ActionResultTApplier
): RandomState[Vector[ActionResultT]] = {
val newRoundId = gameState.currentRoundId + 1
val newRoundId = startingState.currentRoundId + 1
// Verify no old incoming armies
gameState.provinces.foreach {
startingState.provinces.foreach {
case (pid, province) =>
internalRequire(
province.incomingArmies.forall(_.arrivalRound >= newRoundId),
@@ -70,26 +74,28 @@ case class NewRoundAction(
)
}
val oldDate = gameState.currentDate.get
val oldDate = DateConverter.fromProto(startingState.currentDate)
val newDate = oldDate.addMonths(1)
// Build sequencer starting from starting state
val initialSequencer = RandomStateSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
val initialSequencer = RandomStateTSequencer.fromProto(
initialStateProto = startingState,
actionResultApplier = actionResultTApplier,
functionalRandom = functionalRandom
)
// Optionally add NewYearAction
val afterNewYearSequencer =
if newDate.month == Date.Month.January then
initialSequencer.withActionResult(_ => NewYearAction(gameState).immediateExecute)
initialSequencer.withActionResultT(_ =>
NewYearAction(GameStateConverter.fromProto(startingState)).immediateExecute
)
else initialSequencer
// Add the main new round result
val afterNewRoundSequencer = afterNewYearSequencer.withActionResult { currentState =>
val afterNewRoundSequencer = afterNewYearSequencer.withActionResultT { currentState =>
newRoundResult(
gs = currentState,
startingState = currentState,
newRoundId = newRoundId,
newDate = newDate
)
@@ -97,31 +103,34 @@ case class NewRoundAction(
// Add stat gain checks (and profession gain for stats that newly cross the threshold)
afterNewRoundSequencer.withRandomActionResults { (latestState, nextRandom) =>
HeroStatGainAction(latestState.heroes.values, latestState.gameId, newDate).randomResultsWithState(nextRandom)
val scalaHeroes = latestState.heroes.values.map(HeroConverter.fromProto)
val scalaDate = DateConverter.fromProto(Some(DateConverter.toProto(newDate)))
HeroStatGainAction(scalaHeroes, latestState.gameId, scalaDate).randomResultsWithState(nextRandom)
}.actionResults
}
private def chronicleLlmRequests(
newDate: Date,
gs: GameState
startingState: GameStateProto
): Vector[LlmRequestT] =
if isChronicleMonth(newDate) then
Vector(
LlmRequestT.ChronicleUpdateMessage(
requestId = s"chronicle_update_${newDate.year}_${newDate.month.value}",
eagleGameId = gs.gameId,
eagleGameId = startingState.gameId,
current_date = newDate,
previous_entries = gs.chronicleEntries.map { entry =>
previous_entries = startingState.chronicleEntries.map { entry =>
ChronicleUpdatePreviousEntry(
date = entry.date,
date = DateConverter.fromProto(entry.date),
generatedTextId = entry.generatedTextId
)
},
}.toVector,
new_entries = ChronicleEventGenerator
.eventTextEntries(
gameHistory = gameHistory,
since = gs.chronicleEntries.lastOption
.map(_.date)
since = startingState.chronicleEntries.lastOption
.flatMap(_.date)
.map(d => DateConverter.fromProto(Some(d)))
.getOrElse(Date(year = 0, month = Date.Month.January))
)
.map(ChronicleEventConverter.fromProto)
@@ -129,11 +138,8 @@ case class NewRoundAction(
)
else Vector()
private def dateBefore(a: Date, b: Date): Boolean =
a.year < b.year || (a.year == b.year && a.month.value < b.month.value)
private def newRoundResult(
gs: GameState,
startingState: GameStateProto,
newRoundId: RoundId,
newDate: Date
): ActionResultT = {
@@ -143,28 +149,29 @@ case class NewRoundAction(
)
val changes: Iterable[Changes] =
gs.provinces.values.map { p =>
startingState.provinces.values.map { p =>
val changedUHs: Vector[UnaffiliatedHeroT] =
p.unaffiliatedHeroes.map { uh =>
uh.copy(
val tUh = UnaffiliatedHeroConverter.fromProto(uh)
tUh.copy(
recruitmentAttempted = false,
roundsInType = uh.roundsInType + 1,
factionBiases = uh.factionBiases.map {
roundsInType = tUh.roundsInType + 1,
factionBiases = tUh.factionBiases.map {
case (fid, v) =>
fid -> modifiedFactionBiasValue(v)
}
)
}
val changedHeroes = changedHeroesAfterOverage(gs, p.id)
}.toVector
val changedHeroes = changedHeroesAfterOverage(startingState, p.id)
// apply caps
val capGoldLoss = (OverResourceLimitLoss.doubleValue * Math.max(
0,
p.gold - ProvinceUtils.goldCap(p)
p.gold - LegacyProvinceUtils.goldCap(p)
)).ceil.toInt
val capFoodLoss = (OverResourceLimitLoss.doubleValue * Math.max(
0,
p.food - ProvinceUtils.foodCap(p)
p.food - LegacyProvinceUtils.foodCap(p)
)).ceil.toInt
val newPriceIndex = PriceIndexUtils.shiftedTowardSteadyState(
@@ -221,7 +228,7 @@ case class NewRoundAction(
val heroesAfterStipend: Map[HeroId, ChangedHeroC] =
changes.flatMap(_.changedHeroes).map(h => h.heroId -> h).toMap
val uppedVigorHeroes: Iterable[ChangedHeroC] = gs.heroes.map {
val uppedVigorHeroes: Iterable[ChangedHeroC] = startingState.heroes.map {
case (hid, h) =>
val baseChange = heroesAfterStipend.getOrElse(hid, ChangedHeroC(heroId = hid))
if h.vigor >= h.constitution then baseChange
@@ -236,9 +243,10 @@ case class NewRoundAction(
val changedHeroes =
uppedVigorHeroes.filter(ch => ch.hasChanges).toVector
val changedFactions = gs.factions.values.map { f =>
val newDateProto = DateConverter.toProto(newDate)
val changedFactions = startingState.factions.values.map { f =>
val removedTruces =
f.factionRelationships.filter(fr => fr.resetDate.exists(dateBefore(_, newDate)))
f.factionRelationships.filter(fr => fr.resetDate.exists(_ < newDateProto))
ChangedFactionC(
factionId = f.id,
@@ -249,15 +257,15 @@ case class NewRoundAction(
resetDate = None,
trustValue = fr.trustValue
)
},
trustLevelUpdates = gs.factions.keys.filterNot(_ == f.id).toVector.map { targetFid =>
}.toVector,
trustLevelUpdates = startingState.factions.keys.filterNot(_ == f.id).toVector.map { targetFid =>
TrustLevelUpdate(targetFid, TrustDeltaPerRound.intValue)
},
clearLastActedProvinceId = true
)
}.toVector
val llmRequests = chronicleLlmRequests(newDate, gs)
val llmRequests = chronicleLlmRequests(newDate, startingState)
ActionResultC(
actionResultType = NewRoundActionResultType,
@@ -278,21 +286,21 @@ case class NewRoundAction(
}
private def changedHeroesAfterOverage(
gs: GameState,
gameState: GameStateProto,
provinceId: ProvinceId
): Vector[ChangedHeroC] = {
val province = gs.provinces(provinceId)
val province = gameState.provinces(provinceId)
val notRecentHeroCount = province.rulingFactionHeroIds
.map(gs.heroes)
.map(gameState.heroes)
.flatMap(_.roundIdJoined)
.count(roundId => gs.currentRoundId - roundId > MinimumRoundsBeforeLoyaltyDegrades.intValue)
.count(roundId => gameState.currentRoundId - roundId > MinimumRoundsBeforeLoyaltyDegrades.intValue)
if province.heroCap >= notRecentHeroCount then Vector.empty
else {
val loyaltyHit =
OverHeroCapLoyaltyDelta.doubleValue * (notRecentHeroCount - province.heroCap)
province.rulingFactionHeroIds
.map(gs.heroes)
.map(gameState.heroes)
.map(h =>
ChangedHeroC(
heroId = h.id,
@@ -301,6 +309,7 @@ case class NewRoundAction(
else StatDelta(loyaltyHit)
)
)
.toVector
}
end if
}
@@ -2,9 +2,9 @@ package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.internal.province.IncomingEndTurnAction as IncomingEndTurnActionProto
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.common.{RandomStateTSequencer, TRandomSequentialResultsAction}
import net.eagle0.eagle.library.util.view_filters.ProvinceViewFilter
import net.eagle0.eagle.library.util.ReturningHeroes
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
@@ -12,34 +12,33 @@ import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFact
import net.eagle0.eagle.model.action_result.types.{EndReconResolutionPhaseResultType, ReconSucceededResultType}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.proto_converters.IncomingEndTurnActionConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.{IncomingEndTurnAction, IncomingRecon}
import net.eagle0.eagle.model.state.RoundPhase
import net.eagle0.eagle.ProvinceId
case class PerformReconResolutionAction(
gameState: GameState,
actionResultApplier: ActionResultApplier
) extends ProtolessRandomSequentialResultsAction {
gameState: GameState
) extends TRandomSequentialResultsAction(gameState) {
// Proto GameState still needed for ProvinceViewFilter.filteredProvinceView
private lazy val startingGameStateProto: GameStateProto = GameStateConverter.toProto(gameState)
private val startingGameState: GameStateProto = GameStateConverter.toProto(gameState)
override protected def randomResults(
functionalRandom: FunctionalRandom
functionalRandom: FunctionalRandom,
actionResultTApplier: ActionResultTApplier
): RandomState[Vector[ActionResultT]] =
gameState.provinces.values
startingGameState.provinces.values
.foldLeft(
RandomStateSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
RandomStateTSequencer.fromProto(
initialStateProto = startingGameState,
actionResultApplier = actionResultTApplier,
functionalRandom = functionalRandom
)
) {
case (sequencer, province) =>
province.incomingEndTurnActions.collect {
case action @ IncomingEndTurnAction(_, _, _: IncomingRecon) => action
}
province.incomingEndTurnActions
.filter(_.action.isRecon)
.foldLeft(sequencer) {
case (innerSequencer, action) =>
innerSequencer.withRandomActionResult {
@@ -56,25 +55,26 @@ case class PerformReconResolutionAction(
)
}
// Package-private for testing
private[action] def oneResult(
def oneResult(
provinceId: ProvinceId,
incomingEndTurnAction: IncomingEndTurnAction,
incomingEndTurnAction: IncomingEndTurnActionProto,
functionalRandom: FunctionalRandom
): RandomState[ActionResultT] = {
val originProvince = gameState.provinces(incomingEndTurnAction.fromProvinceId)
val originProvince =
startingGameState.provinces(incomingEndTurnAction.fromProvinceId)
val fromFactionId = incomingEndTurnAction.fromFactionId
val targetProvince = gameState.provinces(provinceId)
val targetProvince = startingGameState.provinces(provinceId)
// Extract heroId from the recon action details
val heroId = incomingEndTurnAction.details.asInstanceOf[IncomingRecon].heroId
val recon = incomingEndTurnAction.getRecon
ReturningHeroes
.heroesReturningToFaction(
hids = Vector(heroId),
hids = Vector(recon.heroId),
factionId = fromFactionId,
originProvince = originProvince,
provinces = gameState.provinces.values.toVector,
originProvince = ProvinceConverter.fromProto(originProvince),
provinces = startingGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector,
functionalRandom = functionalRandom
)
.map { returningHeroes =>
@@ -85,29 +85,35 @@ case class PerformReconResolutionAction(
// the CP for the destination province
ChangedProvinceC(
provinceId = provinceId,
removedIncomingEndTurnActions = Vector(incomingEndTurnAction)
removedIncomingEndTurnActions = Vector(
IncomingEndTurnActionConverter.fromProto(incomingEndTurnAction)
)
),
// the CP for the acting hero returning
returningHeroes.changedProvince
),
changedFactions = Vector(
ChangedFactionC(
factionId = fromFactionId,
factionId = incomingEndTurnAction.fromFactionId,
updatedReconnedProvinces = Vector(
ProvinceViewFilter
.filteredProvinceView(
startingGameStateProto.provinces(provinceId),
startingGameStateProto
targetProvince,
startingGameState
// FIXME: setting factionId to None right now to grab the full info. This
// probably isn't exactly what we want.
)
.withAsOf(startingGameStateProto.currentDate.get)
.withAsOf(startingGameState.currentDate.get)
)
)
),
clientTextVisibilityExtensions = targetProvince.rulingFactionHeroIds.map { hid =>
ClientTextVisibilityExtensionC(
textId = gameState.heroes(hid).backstoryTextId,
textId = startingGameState
.heroes(hid)
.backstoryVersions
.last
.textId,
recipientFactionIds = Vector(fromFactionId)
)
}.toVector
@@ -2,10 +2,9 @@ package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.{HeroId, ProvinceId}
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.common.{RandomStateTSequencer, TRandomSequentialResultsAction}
import net.eagle0.eagle.library.actions.name_generation_request.NameGenerationRequestCreator
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.settings.{
FreeHeroMoveVigorCost,
MinVigorForFreeHeroMove,
@@ -27,6 +26,9 @@ import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedHero
import net.eagle0.eagle.model.action_result.types.{HeroChangedResultType, HeroMovedResultType, NewQuestsResultType}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.action_result.NotificationDetails
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.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.province.ProvinceT
@@ -103,31 +105,31 @@ object PerformUnaffiliatedHeroesAction {
case class PerformUnaffiliatedHeroesAction(
gameState: GameState,
heroGenerator: HeroGenerator,
actionResultApplier: ActionResultApplier
) extends ProtolessRandomSequentialResultsAction {
heroGenerator: HeroGenerator
) extends TRandomSequentialResultsAction(gameState) {
private val currentDate = gameState.currentDate.get
private val factions = gameState.factions.values.toVector
override protected def randomResults(
functionalRandom: FunctionalRandom
functionalRandom: FunctionalRandom,
actionResultTApplier: ActionResultTApplier
): RandomState[Vector[ActionResultT]] =
RandomStateSequencer(
RandomStateTSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
actionResultApplier = actionResultTApplier,
functionalRandom = functionalRandom
).withRandomActionResults((_, fr) => statusChangeResults(fr))
.withRandomActionResults { (gs, fr) =>
heroAppearsResults(gs, fr)
heroAppearsResults(GameStateConverter.fromProto(gs), fr)
}
.withRandomContinuance { (randomSequencer: RandomStateSequencer) =>
.withRandomContinuance { (randomSequencer: RandomStateTSequencer) =>
val newHeroesMap =
randomSequencer.actionResults.newValue
.flatMap(_.newHeroes)
.map(h => h.id -> h)
.toMap
val currentGs = randomSequencer.lastState
val currentGs = GameStateConverter.fromProto(randomSequencer.lastStateProto)
val qcpsOpt = questChangedProvinces(
currentGs = currentGs,
newHeroesMap = newHeroesMap,
@@ -142,14 +144,15 @@ case class PerformUnaffiliatedHeroesAction(
}
randomSequencer.withRandomActionResults((_, _) => qcpsOpt)
}
.withActionResults { gs =>
.withActionResultTs { gs =>
EndUnaffiliatedHeroActionsPhaseAction(
gameId = gs.gameId,
gameId = GameStateConverter.fromProto(gs).gameId,
currentDate = currentDate,
currentRoundId = gs.currentRoundId,
provinces = gs.provinces.values.toVector,
heroes = gs.heroes.values.toVector,
factions = gs.factions.values.toVector
provinces = gs.provinces.values.map(ProvinceConverter.fromProto).toVector,
heroes = gs.heroes.values.map(HeroConverter.fromProto).toVector,
factions =
gs.factions.values.map(net.eagle0.eagle.model.proto_converters.faction.FactionConverter.fromProto).toVector
).results
}
.actionResults
@@ -1,6 +1,6 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.{BattalionId, FactionId, GameId, HeroId, ProvinceId, RoundId}
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
@@ -13,21 +13,19 @@ import net.eagle0.eagle.model.action_result.types.{
}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.state.{HostileArmyGroup, RoundPhase}
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.date.Date as DateT
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
case class PerformUncontestedConquestAction(
gameId: GameId,
currentRoundId: RoundId,
currentDate: DateT,
provinces: Map[ProvinceId, ProvinceT],
factions: Map[FactionId, FactionT],
heroes: Map[HeroId, HeroT],
battalions: Map[BattalionId, BattalionT]
gameState: GameState
) extends ProtolessSequentialResultsAction {
private val gameId = gameState.gameId
private val currentRoundId = gameState.currentRoundId
private val currentDate = gameState.currentDate.get
private val provinces = gameState.provinces
private val factions = gameState.factions
private val heroes = gameState.heroes
private val battalions = gameState.battalions
private val endPhaseAction: ActionResultT = ActionResultC(
actionResultType = EndUncontestedConquestPhaseResultType,
@@ -6,107 +6,89 @@ import net.eagle0.eagle.api.available_command.{AvailableCommand, RestAvailableCo
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType.*
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.command.TCommandFactory
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, TCommand}
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.command.CommandFactory
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, RandomStateTSequencer}
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooser
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.library.util.CommandSelection
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
case class PerformVassalCommandsPhaseAction(
gameState: GameState,
commandsForProvince: ProvinceId => Option[OneProvinceAvailableCommands],
commandFactory: TCommandFactory,
actionResultApplier: ActionResultApplier
commandFactory: CommandFactory,
applier: ActionResultTApplier
) extends ProtolessRandomSequentialResultsAction {
private val gameStateProto: GameStateProto = GameStateConverter.toProto(gameState)
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.*
private def vassalProvinces(gs: GameState): Vector[ProvinceT] =
gs.provinces.values
.filter(_.rulingFactionId.isDefined)
.filterNot(ProvinceUtils.ruledByFactionLeader(_, gs.factions.values.toVector))
.filterNot(_.hasActed)
.toVector
override def randomResults(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] =
vassalProvinces(gameState)
gameStateProto.provinces.values
.filter(_.rulingFactionId.isDefined)
.filterNot(LegacyProvinceUtils.ruledByFactionLeader(_, gameStateProto))
.filterNot(_.hasActed)
.foldLeft(
RandomStateSequencer(
RandomStateTSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
actionResultApplier = applier,
functionalRandom = functionalRandom
)
) {
case (sequencer, province) =>
commandsForProvince(province.id).map { opac =>
sequencer.withRandomActionResults { (gs, fr) =>
chooseCommand(gs, province.id, fr).continue {
case (Some(cs), nextFr) =>
val cmd = commandFactory.makeTCommand(
actingFactionId = cs.actingFactionId,
gameState = gs,
availableCommand = cs.available,
selectedCommand = cs.selected
)
cmd match {
case TCommand.Simple(action) =>
RandomState(Vector(action.immediateExecute), nextFr)
case TCommand.RandomSimple(action) =>
action.immediateExecute(nextFr).map(ar => Vector(ar))
case TCommand.Sequential(action) =>
RandomState(action.results, nextFr)
}
case (None, nextFr) =>
RandomState(Vector.empty[ActionResultT], nextFr)
sequencer.withOptionalRandomTCommand { (gs, fr) =>
chooseCommand(province.id, fr).map { maybeCS =>
maybeCS.map { cs =>
commandFactory.makeTCommand(
actingFactionId = cs.actingFactionId,
gameState = GameStateConverter.fromProto(gs),
availableCommand = cs.available,
selectedCommand = cs.selected
)
}
}
}
.getOrElse(sequencer)
}
.actionResults
private def maybeRestCommand(
actingFactionId: FactionId,
gsProto: GameStateProto,
gs: GameStateProto,
commandOptions: Vector[AvailableCommand],
provinceId: ProvinceId,
reason: String
): Option[CommandSelection] = {
val heroes =
gsProto
.provinces(provinceId)
gs.provinces(provinceId)
.rulingFactionHeroIds
.map(gsProto.heroes)
.map(gs.heroes)
MoreOption.flatWhen(
commandOptions.collectFirst {
case ac: RestAvailableCommand =>
ac
}.isDefined && shouldRest(heroes)
) {
chosenRestCommand(actingFactionId, gsProto, commandOptions, reason)
chosenRestCommand(actingFactionId, gs, commandOptions, reason)
}
}
private def selectedCommandFromOrders(
actingFactionId: FactionId,
gsProto: GameStateProto,
gs: GameStateProto,
commandOptions: Vector[AvailableCommand],
functionalRandom: FunctionalRandom,
provinceId: ProvinceId
): RandomState[Option[CommandSelection]] =
gsProto.provinces(provinceId).provinceOrders match {
gs.provinces(provinceId).provinceOrders match {
case ENTRUST =>
chosenEntrustCommand(
actingFactionId = actingFactionId,
gameState = gsProto,
gameState = gs,
availableCommands = commandOptions,
actingProvinceId = provinceId,
functionalRandom = functionalRandom
@@ -114,21 +96,21 @@ case class PerformVassalCommandsPhaseAction(
case DEVELOP =>
chosenDevelopCommand(
actingFactionId = actingFactionId,
gameState = gsProto,
gameState = gs,
availableCommands = commandOptions,
functionalRandom = functionalRandom
)
case MOBILIZE =>
chosenMobilizeCommand(
actingFactionId = actingFactionId,
gameState = gsProto,
gameState = gs,
availableCommands = commandOptions,
functionalRandom = functionalRandom
)
case EXPAND =>
chosenExpandCommand(
actingFactionId,
gsProto,
gs,
commandOptions,
functionalRandom
)
@@ -137,7 +119,7 @@ case class PerformVassalCommandsPhaseAction(
RandomState(
chosenRestCommand(
actingFactionId,
gsProto,
gs,
commandOptions,
"bad orders"
),
@@ -146,17 +128,13 @@ case class PerformVassalCommandsPhaseAction(
}
def chooseCommand(
gs: GameState,
provinceId: ProvinceId,
functionalRandom: FunctionalRandom
): RandomState[Option[CommandSelection]] = {
// Convert to proto for CommandChoiceHelpers which expects proto GameState
val gsProto = GameStateConverter.toProto(gs)
): RandomState[Option[CommandSelection]] =
commandsForProvince(provinceId).map { oneProvinceAvailableCommands =>
CommandChooser.choose(
gs.provinces(provinceId).rulingFactionId.get,
gsProto,
gameStateProto.provinces(provinceId).getRulingFactionId,
gameStateProto,
oneProvinceAvailableCommands.commands.toVector,
Vector[CommandChooser](
resolveTributeSelectedCommand,
@@ -164,14 +142,14 @@ case class PerformVassalCommandsPhaseAction(
handleRiotSelectedCommand,
(
fid: FactionId,
gsP: GameStateProto,
gs: GameStateProto,
acs: Vector[AvailableCommand],
fr: FunctionalRandom
) =>
RandomState(
maybeRestCommand(
fid,
gsP,
gs,
acs,
provinceId,
"chosen vassal command: rest"
@@ -180,13 +158,12 @@ case class PerformVassalCommandsPhaseAction(
),
(
fid: FactionId,
gsP: GameStateProto,
gs: GameStateProto,
acs: Vector[AvailableCommand],
fr: FunctionalRandom
) => selectedCommandFromOrders(fid, gsP, acs, fr, provinceId)
) => selectedCommandFromOrders(fid, gs, acs, fr, provinceId)
),
functionalRandom
)
}.get
}
}
@@ -2,94 +2,79 @@ package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.command.TCommandFactory
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, TCommand}
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.command.CommandFactory
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, RandomStateTSequencer}
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.library.util.CommandSelection
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.ProvinceId
case class PerformVassalDefenseDecisionsAction(
gameState: GameState,
commandsForProvince: ProvinceId => Option[OneProvinceAvailableCommands],
commandFactory: TCommandFactory,
actionResultApplier: ActionResultApplier
commandFactory: CommandFactory,
applier: ActionResultTApplier
) extends ProtolessRandomSequentialResultsAction {
private val gameStateProto: GameStateProto = GameStateConverter.toProto(gameState)
private def vassalProvinces(gs: GameState): Vector[ProvinceT] =
gs.provinces.values
.filter(_.rulingFactionId.isDefined)
.filterNot(ProvinceUtils.ruledByFactionLeader(_, gs.factions.values.toVector))
.toVector
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.*
override def randomResults(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] =
vassalProvinces(gameState)
gameStateProto.provinces.values
.filter(_.rulingFactionId.isDefined)
.filterNot(LegacyProvinceUtils.ruledByFactionLeader(_, gameStateProto))
.toVector
.foldLeft(
RandomStateSequencer(
RandomStateTSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
actionResultApplier = applier,
functionalRandom = functionalRandom
)
) {
case (sequencer, province) =>
commandsForProvince(province.id).map { opac =>
sequencer.withRandomActionResults { (gs, fr) =>
chooseCommand(gs, province.id, opac.commands.toVector, fr).continue {
case (Some(cs), nextFr) =>
val cmd = commandFactory.makeTCommand(
actingFactionId = cs.actingFactionId,
gameState = gs,
availableCommand = cs.available,
selectedCommand = cs.selected
)
cmd match {
case TCommand.Simple(action) =>
RandomState(Vector(action.immediateExecute), nextFr)
case TCommand.RandomSimple(action) =>
action.immediateExecute(nextFr).map(ar => Vector(ar))
case TCommand.Sequential(action) =>
RandomState(action.results, nextFr)
}
case (None, nextFr) =>
RandomState(Vector.empty[ActionResultT], nextFr)
sequencer.withOptionalRandomTCommand { (gs, fr) =>
chooseCommand(province.id, fr).map { maybeCS =>
maybeCS.map { cs =>
commandFactory.makeTCommand(
actingFactionId = cs.actingFactionId,
gameState = GameStateConverter.fromProto(gs),
availableCommand = cs.available,
selectedCommand = cs.selected
)
}
}
}
.getOrElse(sequencer)
}
.actionResults
private def chooseCommand(
gs: GameState,
def chooseCommand(
provinceId: ProvinceId,
commandOptions: Seq[net.eagle0.eagle.api.available_command.AvailableCommand],
functionalRandom: FunctionalRandom
): RandomState[Option[CommandSelection]] = {
// Convert to proto for CommandChoiceHelpers which expects proto GameState
val gsProto = GameStateConverter.toProto(gs)
): RandomState[Option[CommandSelection]] =
commandsForProvince(provinceId).map { oneProvinceAvailableCommands =>
val commandOptions = oneProvinceAvailableCommands.commands
CommandChoiceHelpers.resolveTributeSelectedCommand(
actingFactionId = gs.provinces(provinceId).rulingFactionId.get,
gameState = gsProto,
availableCommands = commandOptions.toVector,
functionalRandom = functionalRandom
) match {
case rss @ RandomState(Some(_), _) => rss
case RandomState(None, fr) =>
CommandChoiceHelpers.defendSelectedCommand(
gs.provinces(provinceId).rulingFactionId.get,
gsProto,
commandOptions.toVector,
fr
)
resolveTributeSelectedCommand(
actingFactionId = gameStateProto.provinces(provinceId).getRulingFactionId,
gameState = gameStateProto,
availableCommands = commandOptions.toVector,
functionalRandom = functionalRandom
) match {
case rss @ RandomState(Some(_), _) => rss
case RandomState(None, fr) =>
defendSelectedCommand(
gameStateProto.provinces(provinceId).getRulingFactionId,
gameStateProto,
commandOptions.toVector,
fr
)
}
}
}
.getOrElse(RandomState(None, functionalRandom))
}
@@ -2,7 +2,7 @@ package net.eagle0.eagle.library.actions.impl.action
import scala.util.hashing.MurmurHash3
import net.eagle0.eagle.{BattalionId, FactionId, GameId, HeroId, ProvinceId, RoundId}
import net.eagle0.eagle.{FactionId, GameId, ProvinceId, RoundId}
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
import net.eagle0.eagle.library.util.BattalionUtils
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
@@ -10,10 +10,8 @@ import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedPro
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.{SentSuppliesResultType, StartBattleResultType}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.state.{Army, BattalionType, BattalionTypeId, HostileArmyGroup, MovingArmy, Supplies}
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.{Army, BattalionType, HostileArmyGroup, MovingArmy, Supplies}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.shardok_battle.{BattleType, ShardokBattle, ShardokPlayer, VictoryCondition}
import net.eagle0.eagle.model.state.unit_status.UnitStatus
@@ -21,16 +19,17 @@ import net.eagle0.eagle.model.state.HostileArmyGroupStatus.Attacking
import net.eagle0.eagle.shardok_interface.ResolvedEagleUnit
case class RequestBattlesAction(
gameId: GameId,
currentRoundId: RoundId,
currentDate: net.eagle0.eagle.model.state.date.Date,
battleCounter: Int,
heroes: Map[HeroId, HeroT],
battalions: Map[BattalionId, BattalionT],
provinces: Map[ProvinceId, ProvinceT],
factions: Map[FactionId, FactionT],
battalionTypes: Map[BattalionTypeId, BattalionType]
gameState: GameState
) extends ProtolessSequentialResultsAction {
private val gameId = gameState.gameId
private val currentRoundId = gameState.currentRoundId
private val currentDate = gameState.currentDate.get
private val battleCounter = gameState.battleCounter
private val heroes = gameState.heroes
private val battalions = gameState.battalions
private val provinces = gameState.provinces
private val factions = gameState.factions
private val battalionTypes = gameState.battalionTypes.map(bt => bt.typeId -> bt).toMap
override def results: Vector[ActionResultT] =
provincesWithAttackingArmies.zipWithIndex.flatMap { case (pwaas, idx) => handleOneProvince(pwaas, idx) }
@@ -2,24 +2,21 @@ package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.common.{RandomStateTSequencer, TRandomSequentialResultsAction}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.model.action_result.changed_province.concrete.{ChangedProvinceC, HostileArmyStatusChange}
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.{EndTruceTurnBackPhaseResultType, WithdrawalForTruceResultType}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.HostileArmyGroupStatus
import net.eagle0.eagle.model.state.RoundPhase
case class TruceTurnBackPhaseAction(
gameState: GameState,
actionResultApplier: ActionResultApplier
) extends ProtolessRandomSequentialResultsAction {
case class TruceTurnBackPhaseAction(gameState: GameState) extends TRandomSequentialResultsAction(gameState) {
private val factions: Vector[FactionT] = gameState.factions.values.toVector
private def checkOneAttackingArmyGroup(
@@ -63,21 +60,22 @@ case class TruceTurnBackPhaseAction(
.getOrElse(Vector())
override protected def randomResults(
functionalRandom: FunctionalRandom
functionalRandom: FunctionalRandom,
actionResultTApplier: ActionResultTApplier
): RandomState[Vector[ActionResultT]] =
RandomStateSequencer(
RandomStateTSequencer(
initialState = gameState,
actionResultApplier = actionResultApplier,
actionResultApplier = actionResultTApplier,
functionalRandom = functionalRandom
)
.withActionResults(_ => gameState.provinces.values.toVector.flatMap(checkOneProvince))
.withActionResults(gs =>
.withProtolessSequentialResultsAction(gs =>
WithdrawnArmiesReturnHomeAction(
gs.currentRoundId,
gs.provinces.values.toVector
).results
gs.provinces.values.map(ProvinceConverter.fromProto).toVector
)
)
.withActionResult(_ =>
.withActionResultT(_ =>
ActionResultC(
actionResultType = EndTruceTurnBackPhaseResultType,
newRoundPhase = Some(RoundPhase.BattleRequest)
@@ -1,21 +1,23 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "t_command_factory",
srcs = ["TCommandFactory.scala"],
name = "command_base",
srcs = ["Command.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
#"@maven//:com_thesamet_scalapb_lenses_3",
],
)
@@ -27,13 +29,14 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
],
exports = [
":t_command_factory",
":command_base",
],
deps = [
":alms_command",
":apprehend_outlaw_command",
":arm_troops_command",
":attack_decision_command",
":command_base",
":control_weather_command",
":decline_quest_command",
":defend_command",
@@ -53,6 +56,9 @@ scala_library(
":march_command",
":organize_troops_command",
":please_recruit_me_command",
":protoless_random_simple_action_wrapper",
":protoless_sequential_results_action_wrapper",
":protoless_simple_action_wrapper",
":recon_command",
":recruit_heroes_command",
":resolve_alliance_offer_command",
@@ -67,7 +73,6 @@ scala_library(
":start_epidemic_command",
":suppress_beasts_command",
":swear_brotherhood_command",
":t_command_factory",
":trade_command",
":train_command",
":travel_command",
@@ -908,6 +913,98 @@ scala_library(
],
)
scala_library(
name = "protoless_random_simple_action_wrapper",
srcs = ["ProtolessRandomSimpleActionWrapper.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
deps = [
":command_base",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_simple_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:vigor_xp_applier",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
)
scala_library(
name = "protoless_sequential_results_action_wrapper",
srcs = ["ProtolessSequentialResultsActionWrapper.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
deps = [
":command_base",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:vigor_xp_applier",
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_runtime_validator",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
],
)
scala_library(
name = "protoless_simple_action_wrapper",
srcs = ["ProtolessSimpleActionWrapper.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
deps = [
":command_base",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:vigor_xp_applier",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
)
scala_library(
name = "alms_command",
srcs = ["AlmsCommand.scala"],
@@ -0,0 +1,40 @@
package net.eagle0.eagle.library.actions.impl.command
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.changed_faction.ChangedFaction
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
import net.eagle0.eagle.library.actions.impl.common.{Action, ActionWithResultingState}
import net.eagle0.eagle.FactionId
object Command {
private def newChangedFaction(
result: ActionResult,
factionId: Option[FactionId]
): Option[ChangedFaction] =
for {
pid <- result.province
fid <- factionId
} yield ChangedFaction(
id = fid,
newLastActedProvinceId = Some(pid)
)
def resultWithLastCommand(
result: ActionResult,
selectedCommand: SelectedCommand,
factionId: Option[FactionId]
): ActionResult =
result.update(
_.lastCommandTypeForActingProvince := result.province
.map(_ => selectedCommand)
.getOrElse(SelectedCommand.Empty),
_.changedFactions :++= newChangedFaction(result, factionId)
)
}
trait Command extends Action {
override def execute(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionWithResultingState]
}
@@ -66,7 +66,7 @@ import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.BattalionTypeId
class CommandFactory extends TCommandFactory {
class CommandFactory {
private def allProvinces(gameState: GameState): Vector[ProvinceT] =
gameState.provinces.values.toVector
@@ -114,6 +114,34 @@ class CommandFactory extends TCommandFactory {
previousBackstoryTextIdLookup = hid => gameState.heroes(hid).backstoryTextId
)
def makeCommand(
actingFactionId: FactionId,
gameState: GameState,
availableCommand: AvailableCommand,
selectedCommand: SelectedCommand
): Command =
makeCommandInternal(actingFactionId, gameState, availableCommand, selectedCommand) match {
case protolessSimpleAction: ProtolessSimpleAction =>
new ProtolessSimpleActionWrapper(
startingState = gameState,
protolessSimpleAction = protolessSimpleAction,
selectedCommand = selectedCommand
)
case protolessRandomSimpleAction: ProtolessRandomSimpleAction =>
new ProtolessRandomSimpleActionWrapper(
startingState = gameState,
protolessRandomSimpleAction = protolessRandomSimpleAction,
selectedCommand = selectedCommand
)
case protolessSequentialResultsAction: ProtolessSequentialResultsAction =>
new ProtolessSequentialResultsActionWrapper(
startingState = gameState,
protolessSequentialResultsAction = protolessSequentialResultsAction
)
case c: Command => c
}
/**
* Creates a T-type command action directly, without proto wrapping.
*
@@ -133,6 +161,10 @@ class CommandFactory extends TCommandFactory {
TCommand.RandomSimple(protolessRandomSimpleAction)
case protolessSequentialResultsAction: ProtolessSequentialResultsAction =>
TCommand.Sequential(protolessSequentialResultsAction)
case _: Command =>
throw new EagleInternalException(
"makeTCommand called on a command type that is not yet converted to T-types"
)
}
private def makeCommandInternal(
@@ -140,7 +172,7 @@ class CommandFactory extends TCommandFactory {
gameState: GameState,
availableCommand: AvailableCommand,
selectedCommand: SelectedCommand
): ProtolessSimpleAction | ProtolessRandomSimpleAction | ProtolessSequentialResultsAction =
): ProtolessSimpleAction | ProtolessRandomSimpleAction | ProtolessSequentialResultsAction | Command =
(availableCommand, selectedCommand) match {
case (
aoac: ApprehendOutlawAvailableCommand,
@@ -0,0 +1,54 @@
package net.eagle0.eagle.library.actions.impl.command
import net.eagle0.common.{RandomState, SeededRandom}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.common.action_result_type.ActionResultType.NEW_RANDOM_SEED
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
import net.eagle0.eagle.library.actions.impl.common.{
ActionWithResultingState,
ProtolessRandomSimpleAction,
RandomStateProtoSequencer,
VigorXPApplier
}
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
class ProtolessRandomSimpleActionWrapper(
startingState: GameState,
protolessRandomSimpleAction: ProtolessRandomSimpleAction,
selectedCommand: SelectedCommand
) extends Command {
override def execute(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionWithResultingState] =
RandomStateProtoSequencer(
initialState = startingState,
actionResultProtoApplier = actionResultProtoApplier,
functionalRandom = SeededRandom(startingState.randomSeed)
).withRandomActionResult {
case (gs, fr) =>
protolessRandomSimpleAction
.immediateExecute(fr)
.map(ar => VigorXPApplier.withVigorXp(ActionResultProtoConverter.toProto(ar)))
.map { resultWithVigorXp =>
Command.resultWithLastCommand(
result = resultWithVigorXp,
selectedCommand = selectedCommand,
factionId = resultWithVigorXp.province
.map(startingState.provinces)
.flatMap(_.rulingFactionId)
)
}
}.withRandomActionResult {
case (gs, fr) =>
RandomState(
ActionResult(
`type` = NEW_RANDOM_SEED,
newRandomSeed = Some(fr.seed)
),
fr
)
}.results.newValue
}
@@ -0,0 +1,38 @@
package net.eagle0.eagle.library.actions.impl.command
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.library.actions.applier.{ActionResultProtoApplier, ActionResultTApplierImpl}
import net.eagle0.eagle.library.actions.impl.common.{
ActionWithResultingState,
ProtolessSequentialResultsAction,
RandomStateTSequencer,
VigorXPApplier
}
import net.eagle0.eagle.library.util.validations.ScalaRuntimeValidator
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.state.game_state.GameState
class ProtolessSequentialResultsActionWrapper(
startingState: GameState,
protolessSequentialResultsAction: ProtolessSequentialResultsAction
) extends Command {
override def execute(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionWithResultingState] = {
val arts = protolessSequentialResultsAction.results match {
case items if items.isEmpty =>
throw new EagleInternalException(
"ProtolessSequentialResultsActionWrapper must have at least one result"
)
case h +: t =>
VigorXPApplier.withVigorXp(h) +: t
case _ => ??? // above cases should cover
}
RandomStateTSequencer(
initialState = startingState,
actionResultApplier = ActionResultTApplierImpl(ScalaRuntimeValidator),
functionalRandom = SeededRandom(startingState.randomSeed)
).withActionResultTs(_ => arts).actionsWithResultingStates.newValue
}
}
@@ -0,0 +1,36 @@
package net.eagle0.eagle.library.actions.impl.command
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
import net.eagle0.eagle.library.actions.impl.common.{ActionWithResultingState, ProtolessSimpleAction, VigorXPApplier}
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
class ProtolessSimpleActionWrapper(
startingState: GameState,
protolessSimpleAction: ProtolessSimpleAction,
selectedCommand: SelectedCommand
) extends Command {
override def execute(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionWithResultingState] =
VigorXPApplier.withVigorXp(
ActionResultProtoConverter.toProto(protolessSimpleAction.immediateExecute)
) match {
case result =>
Vector(
actionResultProtoApplier.applyActionResult(
startingState = GameStateConverter.toProto(startingState),
result = Command.resultWithLastCommand(
result = result,
selectedCommand = selectedCommand,
factionId = result.province
.map(startingState.provinces)
.flatMap(_.rulingFactionId)
)
)
)
}
}
@@ -0,0 +1,51 @@
package net.eagle0.eagle.library.actions.impl.command
import net.eagle0.common.{FunctionalRandom, RandomState, SeededRandom}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.common.action_result_type.ActionResultType.NEW_RANDOM_SEED
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
import net.eagle0.eagle.library.actions.impl.common.{
ActionWithResultingState,
RandomStateProtoSequencer,
VigorXPApplier
}
abstract class RandomSingleResultCommand(
startingState: GameState,
val selectedCommand: SelectedCommand
) extends Command {
def immediateExecute(
functionalRandom: FunctionalRandom
): RandomState[ActionResult]
override def execute(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionWithResultingState] =
RandomStateProtoSequencer(
initialStateProto = startingState,
actionResultProtoApplier = actionResultProtoApplier,
functionalRandom = SeededRandom(startingState.randomSeed)
).withRandomActionResult {
case (gs, fr) =>
immediateExecute(fr).map(VigorXPApplier.withVigorXp).map { resultWithVigorXp =>
Command.resultWithLastCommand(
result = resultWithVigorXp,
selectedCommand = selectedCommand,
factionId = resultWithVigorXp.province
.map(startingState.provinces)
.flatMap(_.rulingFactionId)
)
}
}.withRandomActionResult {
case (gs, fr) =>
RandomState(
ActionResult(
`type` = NEW_RANDOM_SEED,
newRandomSeed = Some(fr.seed)
),
fr
)
}.results.newValue
}
@@ -0,0 +1,31 @@
package net.eagle0.eagle.library.actions.impl.command
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
import net.eagle0.eagle.library.actions.impl.common.{ActionWithResultingState, SimpleAction, VigorXPApplier}
class SimpleActionWrapper(
startingState: GameState,
simpleAction: SimpleAction,
selectedCommand: SelectedCommand
) extends Command {
override def execute(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionWithResultingState] =
VigorXPApplier.withVigorXp(simpleAction.immediateExecute) match {
case result =>
Vector(
actionResultProtoApplier.applyActionResult(
startingState = startingState,
result = Command.resultWithLastCommand(
result = result,
selectedCommand = selectedCommand,
factionId = result.province
.map(startingState.provinces)
.flatMap(_.rulingFactionId)
)
)
)
}
}
@@ -1,17 +0,0 @@
package net.eagle0.eagle.library.actions.impl.command
import net.eagle0.eagle.api.available_command.AvailableCommand
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.library.actions.impl.common.TCommand
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.FactionId
/** Trait for creating T-type command actions. Extracted from CommandFactory to allow lightweight mocking in tests. */
trait TCommandFactory {
def makeTCommand(
actingFactionId: FactionId,
gameState: GameState,
availableCommand: AvailableCommand,
selectedCommand: SelectedCommand
): TCommand
}
@@ -33,6 +33,28 @@ scala_library(
],
)
scala_library(
name = "deterministic_sequential_results_action",
srcs = ["DeterministicSequentialResultsAction.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
exports = [
":action",
":action_with_resulting_state",
],
deps = [
":action",
":action_with_resulting_state",
":vigor_xp_applier",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
],
)
scala_library(
name = "t_random_sequential_results_action",
srcs = ["TRandomSequentialResultsAction.scala"],
@@ -78,7 +100,6 @@ scala_library(
":action_with_resulting_state",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
],
deps = [
":action_with_resulting_state",
@@ -178,6 +199,80 @@ scala_library(
],
)
scala_library(
name = "random_state_proto_sequencer",
srcs = ["RandomStateProtoSequencer.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
exports = [
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
deps = [
":action",
":action_with_resulting_state",
":protoless_sequential_results_action",
":protoless_simple_action",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
scala_library(
name = "random_state_trait_sequencer",
srcs = ["RandomStateTSequencer.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
exports = [
":t_command",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
deps = [
":action",
":action_with_resulting_state",
":protoless_random_simple_action",
":protoless_sequential_results_action",
":protoless_simple_action",
":t_command",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier_impl",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
scala_library(
name = "vigor_xp_applier",
srcs = ["VigorXPApplier.scala"],
@@ -0,0 +1,32 @@
package net.eagle0.eagle.library.actions.impl.common
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
import net.eagle0.eagle.library.actions.impl.common
abstract class DeterministicSequentialResultsAction(startingState: GameState) extends Action {
def results(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionResult]
override def execute(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionWithResultingState] =
if results(actionResultProtoApplier).isEmpty then Vector.empty
else
results(actionResultProtoApplier)
.map(VigorXPApplier.withVigorXp)
.foldLeft(
Vector(
common.ActionWithResultingState(
gameState = startingState,
actionResult = null
)
)
)((aws, far) =>
aws :+ actionResultProtoApplier
.applyActionResult(aws.last.gameState, far)
)
.drop(1)
}
@@ -27,7 +27,7 @@ abstract class ProtolessRandomSequentialResultsAction {
}
/**
* Execute this action with an explicit starting state and convert results to proto format.
* Execute this action and convert results to proto format.
*
* This is the bridge between T-type actions and the proto-based engine interface.
*/
@@ -0,0 +1,286 @@
package net.eagle0.eagle.library.actions.impl.common
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.internal.action_result.ActionResult as ActionResultProto
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.state.game_state.GameState
trait RandomStateProtoSequencer {
def lastStateProto: GameStateProto
def results: RandomState[Vector[ActionWithResultingState]]
def actionResults: RandomState[Vector[ActionResultProto]]
def withRandomActionResult(
actionResultGen: (
GameStateProto,
FunctionalRandom
) => RandomState[ActionResultProto]
): RandomStateProtoSequencer
def withRandomActionResults(
actionResultsGen: (
GameStateProto,
FunctionalRandom
) => RandomState[Vector[ActionResultProto]]
): RandomStateProtoSequencer
def withRandomAction(
actionGen: (GameStateProto, FunctionalRandom) => RandomState[Action]
): RandomStateProtoSequencer
// ActionResultSequencer passthroughs
def withAction(action: GameStateProto => Action): RandomStateProtoSequencer
def withProtolessSimpleAction(
action: GameStateProto => ProtolessSimpleAction
): RandomStateProtoSequencer
def withProtolessSequentialResultsAction(
action: GameStateProto => ProtolessSequentialResultsAction
): RandomStateProtoSequencer
def withRandomActionResultT(
actionResultGen: (
GameStateProto,
FunctionalRandom
) => RandomState[ActionResultT]
): RandomStateProtoSequencer
def withRandomActionResultTs(
actionResultGen: (
GameStateProto,
FunctionalRandom
) => RandomState[Vector[ActionResultT]]
): RandomStateProtoSequencer
def withActionResult(
actionResultGen: GameStateProto => ActionResultProto
): RandomStateProtoSequencer
def withActionResultT(
actionResultGen: GameStateProto => ActionResultT
): RandomStateProtoSequencer
def withActionResultTs(
actionResultGen: GameStateProto => Iterable[ActionResultT]
): RandomStateProtoSequencer
def withActionResults(
actionResultsGen: GameStateProto => Iterable[ActionResultProto]
): RandomStateProtoSequencer
def withActionWithResultingState(
awrsGen: GameStateProto => ActionWithResultingState
): RandomStateProtoSequencer
def withContinuance(
continuance: RandomStateProtoSequencer => RandomStateProtoSequencer
): RandomStateProtoSequencer
def withRandomContinuance(
randomContinuance: RandomStateProtoSequencer => RandomStateProtoSequencer
): RandomStateProtoSequencer
def foldIn[T](ts: Iterable[T])(
f: (T, GameStateProto, FunctionalRandom) => RandomState[ActionResultProto]
): RandomStateProtoSequencer
}
object RandomStateProtoSequencer {
def apply(
initialState: GameState,
actionResultProtoApplier: ActionResultProtoApplier,
functionalRandom: FunctionalRandom
): RandomStateProtoSequencer =
apply(
initialStateProto = GameStateConverter.toProto(initialState),
actionResultProtoApplier = actionResultProtoApplier,
functionalRandom = functionalRandom
)
def apply(
initialStateProto: GameStateProto,
actionResultProtoApplier: ActionResultProtoApplier,
functionalRandom: FunctionalRandom
): RandomStateProtoSequencer =
RandomStateProtoSequencerImpl(
initialStateProto = initialStateProto,
actionResultProtoApplier = actionResultProtoApplier,
results = RandomState(
Vector(),
functionalRandom
)
)
}
private case class RandomStateProtoSequencerImpl(
initialStateProto: GameStateProto,
actionResultProtoApplier: ActionResultProtoApplier,
results: RandomState[Vector[ActionWithResultingState]]
) extends RandomStateProtoSequencer {
override def actionResults: RandomState[Vector[ActionResultProto]] =
results.map(_.map(_.actionResult))
override val lastStateProto: GameStateProto =
results.newValue.lastOption.map(_.gameState).getOrElse(initialStateProto)
override def withAction(
action: GameStateProto => Action
): RandomStateProtoSequencer =
copy(
results = results.map(
_ ++ action(lastStateProto).execute(actionResultProtoApplier)
)
)
override def withProtolessSimpleAction(
action: GameStateProto => ProtolessSimpleAction
): RandomStateProtoSequencer =
withActionResultT(gs => action(gs).immediateExecute)
override def withProtolessSequentialResultsAction(
action: GameStateProto => ProtolessSequentialResultsAction
): RandomStateProtoSequencer =
withActionResults(gs => action(gs).results.map(ActionResultProtoConverter.toProto))
override def withActionResultT(
action: GameStateProto => ActionResultT
): RandomStateProtoSequencer =
withActionResult(gs =>
ActionResultProtoConverter.toProto(
action(gs)
)
)
override def withActionResultTs(
action: GameStateProto => Iterable[ActionResultT]
): RandomStateProtoSequencer =
withActionResults(gs => action(gs).map(ActionResultProtoConverter.toProto))
override def withActionResult(
actionResultGen: GameStateProto => ActionResultProto
): RandomStateProtoSequencer =
copy(
results = results.map(
_ :+ actionResultProtoApplier
.applyActionResult(lastStateProto, actionResultGen(lastStateProto))
)
)
override def withActionResults(
actionResultsGen: GameStateProto => Iterable[ActionResultProto]
): RandomStateProtoSequencer =
copy(
results = results.map(
_ ++ actionResultProtoApplier
.applyActionResults(lastStateProto, actionResultsGen(lastStateProto))
)
)
override def foldIn[T](ts: Iterable[T])(
f: (T, GameStateProto, FunctionalRandom) => RandomState[ActionResultProto]
): RandomStateProtoSequencer =
ts.foldLeft(this) {
case (sequencer, t) =>
sequencer.withRandomActionResult { case (gs, fr) => f(t, gs, fr) }
}
override def withActionWithResultingState(
awrsGen: GameStateProto => ActionWithResultingState
): RandomStateProtoSequencer = copy(
results = results.map(_ :+ awrsGen(lastStateProto))
)
override def withContinuance(
continuance: RandomStateProtoSequencer => RandomStateProtoSequencer
): RandomStateProtoSequencer = continuance(this)
override def withRandomActionResult(
actionResultGen: (
GameStateProto,
FunctionalRandom
) => RandomState[ActionResultProto]
): RandomStateProtoSequencerImpl =
results match {
case RandomState(ars, fr) =>
copy(
results = actionResultGen(lastStateProto, fr).map { ar =>
ars :+ actionResultProtoApplier
.applyActionResult(lastStateProto, ar)
}
)
}
override def withRandomActionResultT(
actionResultGen: (
GameStateProto,
FunctionalRandom
) => RandomState[ActionResultT]
): RandomStateProtoSequencerImpl =
results match {
case RandomState(ars, fr) =>
copy(
results = actionResultGen(lastStateProto, fr).map { ar =>
ars :+ actionResultProtoApplier
.applyActionResult(
lastStateProto,
ActionResultProtoConverter.toProto(ar)
)
}
)
}
override def withRandomActionResults(
actionResultsGen: (
GameStateProto,
FunctionalRandom
) => RandomState[Vector[ActionResultProto]]
): RandomStateProtoSequencer =
results match {
case RandomState(ars, fr) =>
copy(results = actionResultsGen(lastStateProto, fr).map { newArs =>
ars ++ actionResultProtoApplier.applyActionResults(
lastStateProto,
newArs
)
})
}
override def withRandomActionResultTs(
actionResultGen: (
GameStateProto,
FunctionalRandom
) => RandomState[Vector[ActionResultT]]
): RandomStateProtoSequencerImpl =
results match {
case RandomState(ars, fr) =>
copy(
results = actionResultGen(lastStateProto, fr).map { newArs =>
ars ++ actionResultProtoApplier
.applyActionResults(
lastStateProto,
newArs.map(ActionResultProtoConverter.toProto)
)
}
)
}
override def withRandomAction(
actionGen: (GameStateProto, FunctionalRandom) => RandomState[Action]
): RandomStateProtoSequencer =
results match {
case RandomState(ars, fr) =>
copy(results = actionGen(lastStateProto, fr).map { action =>
ars ++ action.execute(actionResultProtoApplier)
})
}
override def withRandomContinuance(
randomContinuance: RandomStateProtoSequencer => RandomStateProtoSequencer
): RandomStateProtoSequencer = randomContinuance(this)
}
@@ -0,0 +1,356 @@
package net.eagle0.eagle.library.actions.impl.common
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.library.actions.applier.{ActionResultTApplier, ActionResultTWithResultingState}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.state.game_state.GameState
trait RandomStateTSequencer {
def lastStateProto: GameStateProto
def actionResults: RandomState[Vector[ActionResultT]]
def actionResultTsWithResultingStates: RandomState[
Vector[ActionResultTWithResultingState]
]
def actionsWithResultingStates: RandomState[
Vector[ActionWithResultingState]
] = actionResultTsWithResultingStates.map {
_.map {
case ActionResultTWithResultingState(art, gs) =>
ActionWithResultingState(ActionResultProtoConverter.toProto(art), gs)
}
}
def withRandomActionResult(
actionResultGen: (
GameStateProto,
FunctionalRandom
) => RandomState[ActionResultT]
): RandomStateTSequencer
def withRandomActionResults(
actionResultsGen: FunctionalRandom => RandomState[Vector[ActionResultT]]
): RandomStateTSequencer =
withRandomActionResults((_, fr) => actionResultsGen(fr))
def withRandomActionResults(
actionResultsGen: (
GameStateProto,
FunctionalRandom
) => RandomState[Vector[ActionResultT]]
): RandomStateTSequencer
def withProtolessSimpleAction(
action: GameStateProto => ProtolessSimpleAction
): RandomStateTSequencer
def withProtolessRandomSimpleAction(
action: (GameStateProto, FunctionalRandom) => RandomState[ActionResultT]
): RandomStateTSequencer
def withProtolessSequentialResultsAction(
action: GameStateProto => ProtolessSequentialResultsAction
): RandomStateTSequencer
/**
* Execute a T-type command and add its results to the sequencer.
*
* This handles all three T-type command types:
* - Simple: single immediate result
* - RandomSimple: single result requiring randomness
* - Sequential: multiple results
*/
def withTCommand(
command: GameStateProto => TCommand
): RandomStateTSequencer
/**
* Execute a T-type command that may or may not exist.
*
* If the command generator returns None, no action is taken.
*/
def withOptionalTCommand(
command: GameStateProto => Option[TCommand]
): RandomStateTSequencer
/**
* Execute a T-type command with access to the FunctionalRandom.
*
* The command generator receives both the game state and the random state, useful when command selection itself
* requires randomness.
*/
def withRandomTCommand(
command: (GameStateProto, FunctionalRandom) => RandomState[TCommand]
): RandomStateTSequencer
/**
* Execute an optional T-type command with access to the FunctionalRandom.
*/
def withOptionalRandomTCommand(
command: (GameStateProto, FunctionalRandom) => RandomState[Option[TCommand]]
): RandomStateTSequencer
def withActionResult(
actionResultGen: GameStateProto => ActionResultT
): RandomStateTSequencer
def withActionResultT(
actionResultGen: GameStateProto => ActionResultT
): RandomStateTSequencer
def withActionResultTs(
actionResultGen: GameStateProto => Iterable[ActionResultT]
): RandomStateTSequencer
def withActionResults(
actionResultsGen: GameStateProto => Iterable[ActionResultT]
): RandomStateTSequencer
def withContinuance(
continuance: RandomStateTSequencer => RandomStateTSequencer
): RandomStateTSequencer
def withRandomContinuance(
randomContinuance: RandomStateTSequencer => RandomStateTSequencer
): RandomStateTSequencer
def foldIn[T](ts: Iterable[T])(
f: (T, GameStateProto, FunctionalRandom) => RandomState[ActionResultT]
): RandomStateTSequencer
}
object RandomStateTSequencer {
def apply(
initialState: GameState,
actionResultApplier: ActionResultTApplier,
functionalRandom: FunctionalRandom
): RandomStateTSequencer =
fromProto(
initialStateProto = GameStateConverter.toProto(initialState),
actionResultApplier = actionResultApplier,
functionalRandom = functionalRandom
)
def fromProto(
initialStateProto: GameStateProto,
actionResultApplier: ActionResultTApplier,
functionalRandom: FunctionalRandom
): RandomStateTSequencer =
RandomStateTSequencerImpl(
initialStateProto = initialStateProto,
actionResultApplier = actionResultApplier,
actionResultTsWithResultingStates = RandomState(
Vector(),
functionalRandom
)
)
}
private case class RandomStateTSequencerImpl(
initialStateProto: GameStateProto,
actionResultApplier: ActionResultTApplier,
actionResultTsWithResultingStates: RandomState[
Vector[ActionResultTWithResultingState]
]
) extends RandomStateTSequencer {
override def actionResults: RandomState[Vector[ActionResultT]] =
actionResultTsWithResultingStates.map(_.map(_.actionResult))
override def lastStateProto: GameStateProto =
actionResultTsWithResultingStates.newValue.lastOption
.map(_.resultingState)
.getOrElse(initialStateProto)
override def withProtolessSimpleAction(
action: GameStateProto => ProtolessSimpleAction
): RandomStateTSequencer =
withActionResultT(gs => action(gs).immediateExecute)
override def withProtolessRandomSimpleAction(
action: (GameStateProto, FunctionalRandom) => RandomState[ActionResultT]
): RandomStateTSequencer =
withRandomActionResult(action)
override def withProtolessSequentialResultsAction(
action: GameStateProto => ProtolessSequentialResultsAction
): RandomStateTSequencer =
withActionResults(gs => action(gs).results)
override def withTCommand(
command: GameStateProto => TCommand
): RandomStateTSequencer =
command(lastStateProto) match {
case TCommand.Simple(action) => withActionResultT(_ => action.immediateExecute)
case TCommand.RandomSimple(action) =>
withRandomActionResult((_, fr) => action.immediateExecute(fr))
case TCommand.Sequential(action) => withActionResults(_ => action.results)
}
override def withOptionalTCommand(
command: GameStateProto => Option[TCommand]
): RandomStateTSequencer =
command(lastStateProto) match {
case Some(cmd) => withTCommand(_ => cmd)
case None => this
}
override def withRandomTCommand(
command: (GameStateProto, FunctionalRandom) => RandomState[TCommand]
): RandomStateTSequencer =
actionResultTsWithResultingStates match {
case RandomState(_, fr) =>
command(lastStateProto, fr) match {
case RandomState(TCommand.Simple(action), newFr) =>
copy(
actionResultTsWithResultingStates = RandomState(
actionResultTsWithResultingStates.newValue,
newFr
)
).withActionResultT(_ => action.immediateExecute)
case RandomState(TCommand.RandomSimple(action), newFr) =>
copy(
actionResultTsWithResultingStates = RandomState(
actionResultTsWithResultingStates.newValue,
newFr
)
).withRandomActionResult((_, fr2) => action.immediateExecute(fr2))
case RandomState(TCommand.Sequential(action), newFr) =>
copy(
actionResultTsWithResultingStates = RandomState(
actionResultTsWithResultingStates.newValue,
newFr
)
).withActionResults(_ => action.results)
}
}
override def withOptionalRandomTCommand(
command: (GameStateProto, FunctionalRandom) => RandomState[Option[TCommand]]
): RandomStateTSequencer =
actionResultTsWithResultingStates match {
case RandomState(_, fr) =>
command(lastStateProto, fr) match {
case RandomState(Some(cmd), newFr) =>
copy(
actionResultTsWithResultingStates = RandomState(
actionResultTsWithResultingStates.newValue,
newFr
)
).withTCommand(_ => cmd)
case RandomState(None, newFr) =>
copy(
actionResultTsWithResultingStates = RandomState(
actionResultTsWithResultingStates.newValue,
newFr
)
)
}
}
override def withActionResultT(
action: GameStateProto => ActionResultT
): RandomStateTSequencer =
withActionResult(gs => action(gs))
override def withActionResultTs(
action: GameStateProto => Iterable[ActionResultT]
): RandomStateTSequencer =
withActionResults(gs => action(gs))
override def withActionResult(
actionResultGen: GameStateProto => ActionResultT
): RandomStateTSequencer = actionResultGen(lastStateProto) match {
case art =>
copy(
actionResultTsWithResultingStates = actionResultTsWithResultingStates.map(
_ :+ ActionResultTWithResultingState(art, lastStateProto)
)
)
}
override def withActionResults(
actionResultsGen: GameStateProto => Iterable[ActionResultT]
): RandomStateTSequencer = actionResultsGen(lastStateProto).foldLeft(this) {
case (sequencer, ar) =>
sequencer.copy(actionResultTsWithResultingStates =
sequencer.actionResultTsWithResultingStates.map(
_ :+ actionResultApplier
.applyActionResult(
sequencer.lastStateProto,
ar
)
)
)
}
override def foldIn[T](ts: Iterable[T])(
f: (T, GameStateProto, FunctionalRandom) => RandomState[ActionResultT]
): RandomStateTSequencer =
ts.foldLeft(this) {
case (sequencer, t) =>
sequencer.withRandomActionResult { case (gs, fr) => f(t, gs, fr) }
}
override def withContinuance(
continuance: RandomStateTSequencer => RandomStateTSequencer
): RandomStateTSequencer = continuance(this)
override def withRandomActionResult(
actionResultGen: (
GameStateProto,
FunctionalRandom
) => RandomState[ActionResultT]
): RandomStateTSequencerImpl =
actionResultTsWithResultingStates match {
case RandomState(awrs, fr) =>
actionResultGen(lastStateProto, fr) match {
case RandomState(art, newFr) =>
copy(actionResultTsWithResultingStates =
RandomState(
awrs :+ actionResultApplier
.applyActionResult(
lastStateProto,
art
),
newFr
)
)
}
}
override def withRandomActionResults(
actionResultsGen: (
GameStateProto,
FunctionalRandom
) => RandomState[Vector[ActionResultT]]
): RandomStateTSequencer =
actionResultTsWithResultingStates match {
case RandomState(awrs, fr) =>
actionResultsGen(lastStateProto, fr) match {
case RandomState(Vector(), newFr) =>
copy(
actionResultTsWithResultingStates = RandomState(awrs, newFr)
)
case RandomState(arts, newFr) =>
copy(
actionResultTsWithResultingStates = RandomState(
awrs ++ actionResultApplier
.applyActionResults(
lastStateProto,
arts
),
newFr
)
)
}
}
override def withRandomContinuance(
randomContinuance: RandomStateTSequencer => RandomStateTSequencer
): RandomStateTSequencer = randomContinuance(this)
}
@@ -1,26 +0,0 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "random_state_sequencer",
srcs = ["RandomStateSequencer.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
deps = [
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_simple_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -1,179 +0,0 @@
package net.eagle0.eagle.library.actions.random_state_sequencer
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.library.actions.applier.{ActionResultApplier, ActionResultWithResultingState}
import net.eagle0.eagle.library.actions.impl.common.{
ProtolessRandomSimpleAction,
ProtolessSequentialResultsAction,
ProtolessSimpleAction,
TCommand
}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.state.game_state.GameState
/**
* A fully protoless sequencer for chaining action results.
*
* - Uses Scala GameState throughout (no proto conversions)
* - All callbacks receive Scala GameState
* - Returns ActionResultWithResultingState (Scala GameState, not proto)
*/
trait RandomStateSequencer {
def lastState: GameState
def actionResults: RandomState[Vector[ActionResultT]]
def resultsWithStates: RandomState[Vector[ActionResultWithResultingState]]
def withActionResult(
actionResultGen: GameState => ActionResultT
): RandomStateSequencer
def withActionResults(
actionResultsGen: GameState => Iterable[ActionResultT]
): RandomStateSequencer
def withRandomActionResult(
actionResultGen: (GameState, FunctionalRandom) => RandomState[ActionResultT]
): RandomStateSequencer
def withRandomActionResults(
actionResultsGen: FunctionalRandom => RandomState[Vector[ActionResultT]]
): RandomStateSequencer =
withRandomActionResults((_, fr) => actionResultsGen(fr))
def withRandomActionResults(
actionResultsGen: (GameState, FunctionalRandom) => RandomState[Vector[ActionResultT]]
): RandomStateSequencer
def withContinuance(
continuance: RandomStateSequencer => RandomStateSequencer
): RandomStateSequencer
def withRandomContinuance(
randomContinuance: RandomStateSequencer => RandomStateSequencer
): RandomStateSequencer
def foldIn[T](ts: Iterable[T])(
f: (T, GameState, FunctionalRandom) => RandomState[ActionResultT]
): RandomStateSequencer
def withProtolessSimpleAction(
actionGen: GameState => ProtolessSimpleAction
): RandomStateSequencer =
withActionResult(gs => actionGen(gs).immediateExecute)
def withProtolessRandomSimpleAction(
actionGen: GameState => ProtolessRandomSimpleAction
): RandomStateSequencer =
withRandomActionResult((gs, fr) => actionGen(gs).immediateExecute(fr))
def withProtolessSequentialResultsAction(
actionGen: GameState => ProtolessSequentialResultsAction
): RandomStateSequencer =
withActionResults(gs => actionGen(gs).results)
def withTCommand(commandGen: GameState => TCommand): RandomStateSequencer =
commandGen(lastState) match {
case TCommand.Simple(action) => withProtolessSimpleAction(_ => action)
case TCommand.RandomSimple(action) => withProtolessRandomSimpleAction(_ => action)
case TCommand.Sequential(action) => withProtolessSequentialResultsAction(_ => action)
}
}
object RandomStateSequencer {
def apply(
initialState: GameState,
actionResultApplier: ActionResultApplier,
functionalRandom: FunctionalRandom
): RandomStateSequencer =
RandomStateSequencerImpl(
initialState = initialState,
actionResultApplier = actionResultApplier,
resultsWithStates = RandomState(Vector(), functionalRandom)
)
}
private case class RandomStateSequencerImpl(
initialState: GameState,
actionResultApplier: ActionResultApplier,
resultsWithStates: RandomState[Vector[ActionResultWithResultingState]]
) extends RandomStateSequencer {
override def lastState: GameState =
resultsWithStates.newValue.lastOption
.map(_.resultingState)
.getOrElse(initialState)
override def actionResults: RandomState[Vector[ActionResultT]] =
resultsWithStates.map(_.map(_.actionResult))
override def withActionResult(
actionResultGen: GameState => ActionResultT
): RandomStateSequencer = {
val result = actionResultGen(lastState)
copy(
resultsWithStates = resultsWithStates.map(
_ :+ actionResultApplier.applyActionResult(lastState, result)
)
)
}
override def withActionResults(
actionResultsGen: GameState => Iterable[ActionResultT]
): RandomStateSequencer =
actionResultsGen(lastState).foldLeft[RandomStateSequencer](this) {
case (sequencer, result) =>
sequencer.withActionResult(_ => result)
}
override def withRandomActionResult(
actionResultGen: (GameState, FunctionalRandom) => RandomState[ActionResultT]
): RandomStateSequencer =
resultsWithStates match {
case RandomState(currentResults, fr) =>
actionResultGen(lastState, fr) match {
case RandomState(result, newFr) =>
copy(
resultsWithStates = RandomState(
currentResults :+ actionResultApplier.applyActionResult(lastState, result),
newFr
)
)
}
}
override def withRandomActionResults(
actionResultsGen: (GameState, FunctionalRandom) => RandomState[Vector[ActionResultT]]
): RandomStateSequencer =
resultsWithStates match {
case RandomState(currentResults, fr) =>
actionResultsGen(lastState, fr) match {
case RandomState(Vector(), newFr) =>
copy(resultsWithStates = RandomState(currentResults, newFr))
case RandomState(results, newFr) =>
copy(
resultsWithStates = RandomState(
currentResults ++ actionResultApplier.applyActionResults(lastState, results),
newFr
)
)
}
}
override def withContinuance(
continuance: RandomStateSequencer => RandomStateSequencer
): RandomStateSequencer = continuance(this)
override def withRandomContinuance(
randomContinuance: RandomStateSequencer => RandomStateSequencer
): RandomStateSequencer = randomContinuance(this)
override def foldIn[T](ts: Iterable[T])(
f: (T, GameState, FunctionalRandom) => RandomState[ActionResultT]
): RandomStateSequencer =
ts.foldLeft[RandomStateSequencer](this) {
case (sequencer, t) =>
sequencer.withRandomActionResult((gs, fr) => f(t, gs, fr))
}
}
@@ -22,7 +22,6 @@ import net.eagle0.eagle.model.state.province.{
EpidemicEvent,
FestivalEvent,
FloodEvent,
ImminentRiotEvent,
ProvinceEvent,
ProvinceT
}
@@ -115,9 +114,6 @@ object ProvinceUtils {
def hasEpidemic(p: ProvinceT): Boolean =
p.activeEvents.collectFirst { case _: EpidemicEvent => () }.nonEmpty
def hasImminentRiot(p: ProvinceT): Boolean =
p.activeEvents.collectFirst { case _: ImminentRiotEvent => () }.nonEmpty
def hasBeasts(p: ProvinceT): Boolean =
p.activeEvents.collectFirst { case _: BeastsEvent => () }.nonEmpty
@@ -27,32 +27,4 @@ object RoundPhase {
case object BattleAftermath extends RoundPhase { val value = 23 }
case object DiplomacyResolution extends RoundPhase { val value = 24 }
case object ReconResolution extends RoundPhase { val value = 26 }
val allValues: Vector[RoundPhase] = Vector(
NewRound,
PrisonerExchange,
ProvinceEvents,
ForcedTurnBack,
ProvinceMoveResolution,
HandleRiot,
HeroDepartures,
PleaseRecruitMe,
UnaffiliatedHeroActions,
VassalCommands,
PlayerCommands,
HostileArmySetup,
FreeForAllDecision,
FreeForAllBattleRequest,
FreeForAllBattleResolution,
UncontestedConquest,
AttackDecision,
DefenseDecision,
TruceTurnBack,
BattleRequest,
FoodConsumption,
BattleResolution,
BattleAftermath,
DiplomacyResolution,
ReconResolution
)
}
@@ -13,9 +13,6 @@ import net.eagle0.eagle.library.util.hero_name_fetcher.HeroNameFetcher
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
class UnrequestedTextHandler(llmResolver: LlmResolver) {
// 3 minutes in milliseconds
private val StalledThresholdMillis: Long = 3 * 60 * 1000
def clientTextStoreWithHandledUnrequestedTexts(
clientTextStore: ClientTextStore,
gameHistory: GameHistory,
@@ -156,40 +153,20 @@ class UnrequestedTextHandler(llmResolver: LlmResolver) {
gameHistory: GameHistory,
randomLong: Long
): ClientTextStore = {
// Check for stalled texts and log warnings
val stalledTexts = clientTextStore.stalledIncompleteTexts(StalledThresholdMillis)
if stalledTexts.nonEmpty then {
val stalledDetails = stalledTexts.map { ict =>
val waitingSeconds = (System.currentTimeMillis() - ict.requestedAtMillis) / 1000
s"${ict.id} (waiting ${waitingSeconds}s, partial: ${ict.partialText.take(50)}...)"
}.mkString("\n ")
println(
s"WARNING: ${stalledTexts.size} incomplete text(s) have been waiting > 3 minutes:\n $stalledDetails"
)
}
val results = llmResolver
.resolveLlmRequests(
llmRequests = clientTextStore.incompleteTexts.values
.map((ict: IncompleteClientText) =>
LlmResolver.LlmRequestWithGameState(
llmRequest = ict.llmRequest,
gameState = GameStateConverter.toProto(gameHistory.stateAfter(ict.requestedAfterHistoryCount)),
partialCompletion = Some(ict.partialText)
)
llmResolver.resolveLlmRequests(
llmRequests = clientTextStore.incompleteTexts.values
.map((ict: IncompleteClientText) =>
LlmResolver.LlmRequestWithGameState(
llmRequest = ict.llmRequest,
gameState = GameStateConverter.toProto(gameHistory.stateAfter(ict.requestedAfterHistoryCount)),
partialCompletion = Some(ict.partialText)
)
.toVector,
clientTextStore = clientTextStore,
functionalRandom = SeededRandom(randomLong)
)
.newValue
)
.toVector,
clientTextStore = clientTextStore,
functionalRandom = SeededRandom(randomLong)
)
// Move texts that couldn't be submitted back to unrequested so they get retried
results.foldLeft(clientTextStore) {
case (cts, (llmRequest, LlmResolverTooManyRequestsInFlight)) =>
cts.withMovedBackToUnrequested(llmRequest.id)
case (cts, _) =>
cts
}
clientTextStore
}
}
@@ -9,7 +9,6 @@ import io.grpc.{Status, StatusRuntimeException}
import net.eagle0.common.{RandomState, SeededRandom, SimpleTimedLogger}
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
import net.eagle0.eagle.ai.AIClient
import net.eagle0.eagle.api.eagle.ServerGameStatus
import net.eagle0.eagle.api.eagle.ShardokActionResultResponse.SingleShardokGameResultResponse
import net.eagle0.eagle.api.eagle.UpdateStreamRequest.StreamGameRequest.{ShardokViewStatus, StreamingTextStatus}
import net.eagle0.eagle.api.selected_command.SelectedCommand
@@ -143,34 +142,10 @@ object GameController {
Some {
val availableCommands =
engine.getAvailablePlayerCommands(client.factionId)
// Calculate server game status based on available information
val hasCommands = availableCommands.exists(_.commandsByProvince.nonEmpty)
// Check if any other human player has commands
val otherHumanFactionIds = humanClients.map(_.factionId).filterNot(_ == client.factionId)
val otherPlayersHaveCommands = otherHumanFactionIds.exists { fid =>
engine.getAvailablePlayerCommands(fid).exists(_.commandsByProvince.nonEmpty)
}
// Check if there are incomplete LLM texts accessible to this player
val hasIncompleteTexts = clientTextStore.incompleteTexts.values.exists { incompleteText =>
clientTextStore.accessibleTo
.get(incompleteText.id)
.exists(fids => fids.isEmpty || fids.contains(client.factionId))
}
val serverGameStatus =
if hasCommands then Some(ServerGameStatus(status = ServerGameStatus.Status.YOUR_TURN))
else if hasIncompleteTexts then Some(ServerGameStatus(status = ServerGameStatus.Status.GENERATING_TEXT))
else if otherPlayersHaveCommands then
Some(ServerGameStatus(status = ServerGameStatus.Status.WAITING_FOR_PLAYERS))
else None
val note =
val note =
s"fid ${client.factionId}: Updating client with ${updates.filteredResults.length} new results, " +
s"${shardokUpdates.length} new Shardok results, "
val note2 = availableCommands.map { acs =>
val note2 = availableCommands.map { acs =>
s"available commands with token ${acs.token}"
}
.getOrElse("no available commands")
@@ -183,8 +158,7 @@ object GameController {
availableCommands = engine.getAvailablePlayerCommands(client.factionId),
filteredResults = updates.filteredResults,
unfilteredCountAfter = updates.unfilteredCountAfter,
knownShardokResults = shardokUpdates.toVector,
serverGameStatus = serverGameStatus
knownShardokResults = shardokUpdates.toVector
)
) {
case (client, clientTextUpdate) =>
@@ -7,13 +7,7 @@ import scala.annotation.tailrec
import net.eagle0.eagle.{FactionId, GameId, ShardokGameId}
import net.eagle0.eagle.api.command.AvailableCommands
import net.eagle0.eagle.api.eagle.{
ActionResultResponse,
GameUpdate,
ServerGameStatus,
ShardokActionResultResponse,
UpdateStreamResponse
}
import net.eagle0.eagle.api.eagle.{ActionResultResponse, GameUpdate, ShardokActionResultResponse, UpdateStreamResponse}
import net.eagle0.eagle.api.eagle.GameUpdate.GameUpdateDetails
import net.eagle0.eagle.api.eagle.ShardokActionResultResponse.SingleShardokGameResultResponse
import net.eagle0.eagle.api.eagle.UpdateStreamRequest.StreamGameRequest.{ShardokViewStatus, StreamingTextStatus}
@@ -179,8 +173,7 @@ case class HumanPlayerClientConnectionState(
availableCommands: Option[AvailableCommands],
filteredResults: Vector[ActionResultView],
unfilteredCountAfter: Int,
knownShardokResults: Vector[SingleShardokGameResultResponse],
serverGameStatus: Option[ServerGameStatus]
knownShardokResults: Vector[SingleShardokGameResultResponse]
): HumanPlayerClientConnectionState = {
// Send Shardok results FIRST, before Eagle results that may contain date
// changes which clear ShardokGameModels on the client
@@ -199,8 +192,7 @@ case class HumanPlayerClientConnectionState(
availableCommands = availableCommands,
startingState = None,
filteredResults = filteredResults,
unfilteredCountAfter = unfilteredCountAfter,
serverGameStatus = serverGameStatus
unfilteredCountAfter = unfilteredCountAfter
)
}
@@ -276,16 +268,6 @@ case class HumanPlayerClientConnectionState(
// starting state and filter results from position 1 onwards.
val effectiveStartCount = if unfilteredCount == 0 && history.count > 0 then 1 else unfilteredCount
// For initial connection, we only know about this player's commands.
// Report YOUR_TURN if they have commands, otherwise don't report a status
// (GameController.humanClientsAfterPostingResults has more context for updates).
val serverGameStatus = availableCommands match {
case Some(cmds) if cmds.commandsByProvince.nonEmpty =>
Some(ServerGameStatus(status = ServerGameStatus.Status.YOUR_TURN))
case _ =>
None
}
if history.count <= effectiveStartCount + maxInitialResults then {
val fr = filteredResultsFrom(history, effectiveStartCount)
val startingState =
@@ -302,8 +284,7 @@ case class HumanPlayerClientConnectionState(
availableCommands = availableCommands,
startingState = startingState,
filteredResults = fr.filteredResults,
unfilteredCountAfter = fr.unfilteredCountAfter,
serverGameStatus = serverGameStatus
unfilteredCountAfter = fr.unfilteredCountAfter
)
.withNewCount(fr.unfilteredCountAfter)
.enqueueShardokResults(filteredShardokResults = sfr)
@@ -322,8 +303,7 @@ case class HumanPlayerClientConnectionState(
)
),
filteredResults = fr.filteredResults,
unfilteredCountAfter = fr.unfilteredCountAfter,
serverGameStatus = serverGameStatus
unfilteredCountAfter = fr.unfilteredCountAfter
)
.withNewCount(fr.unfilteredCountAfter)
.enqueueShardokResults(filteredShardokResults = sfr)
@@ -350,23 +330,20 @@ case class HumanPlayerClientConnectionState(
availableCommands: Option[AvailableCommands],
startingState: Option[GameStateView],
filteredResults: Vector[ActionResultView],
unfilteredCountAfter: Int,
serverGameStatus: Option[ServerGameStatus]
unfilteredCountAfter: Int
): HumanPlayerClientConnectionState =
enqueueResults(
startingState = startingState,
filteredResults = filteredResults,
unfilteredCountAfter = unfilteredCountAfter,
availableCommands = availableCommands,
serverGameStatus = serverGameStatus
availableCommands = availableCommands
)
private final def enqueueResults(
startingState: Option[GameStateView],
filteredResults: Vector[ActionResultView],
unfilteredCountAfter: Int,
availableCommands: Option[AvailableCommands],
serverGameStatus: Option[ServerGameStatus]
availableCommands: Option[AvailableCommands]
): HumanPlayerClientConnectionState = {
@tailrec
def go(
@@ -391,8 +368,7 @@ case class HumanPlayerClientConnectionState(
ActionResultResponse(
actionResultViews = filteredResults,
unfilteredResultCountAfter = unfilteredCountAfter,
availableCommands = availableCommands,
serverGameStatus = serverGameStatus
availableCommands = availableCommands
)
)
)
@@ -126,8 +126,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
id = "existingId",
partialText = "firstText",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest,
requestedAtMillis = 12345L
llmRequest = genericLlmRequest
)
),
unrequestedTexts = Map(),
@@ -207,8 +206,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
id = "existingId",
partialText = "firstText",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest,
requestedAtMillis = 12345L
llmRequest = genericLlmRequest
)
),
unrequestedTexts = Map(),
@@ -233,8 +231,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
id = "existingId",
partialText = "firstText",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest,
requestedAtMillis = 12345L
llmRequest = genericLlmRequest
)
)
updatedStore.unrequestedTexts should contain theSameElementsAs Map(
@@ -300,8 +297,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
id = "id",
partialText = "text",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest,
requestedAtMillis = 12345L
llmRequest = genericLlmRequest
)
),
unrequestedTexts = Map(),
@@ -360,8 +356,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
id = "id",
partialText = "text",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest,
requestedAtMillis = 12345L
llmRequest = genericLlmRequest
)
),
unrequestedTexts = Map(),
@@ -378,7 +373,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
}
"withMarkedRequested" should "move unrequested to incomplete" in {
val store =
val store =
ClientTextStoreImpl(
pregenerated = pregeneratedClientTextStore,
persister = mockPersister,
@@ -397,14 +392,16 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
incompleteTextsAreSaved = false,
accessibleToIsSaved = false
)
val updatedStore = store.withMarkedRequested("id")
updatedStore.incompleteTexts.size shouldBe 1
val incompleteText = updatedStore.incompleteTexts("id")
incompleteText.id shouldBe "id"
incompleteText.partialText shouldBe ""
incompleteText.requestedAfterHistoryCount shouldBe requestedAfterHistoryCount
incompleteText.llmRequest shouldBe genericLlmRequest
incompleteText.requestedAtMillis should be > 0L
val updatedStore = store.withMarkedRequested("id")
updatedStore.incompleteTexts should contain theSameElementsAs Map(
"id" ->
IncompleteClientText(
id = "id",
partialText = "",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest
)
)
updatedStore.unrequestedTexts shouldBe empty
}
@@ -420,8 +417,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
id = "id",
partialText = "text",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest,
requestedAtMillis = 12345L
llmRequest = genericLlmRequest
)
),
unrequestedTexts = Map(),
@@ -439,8 +435,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
id = "id",
partialText = "textnew text",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest,
requestedAtMillis = 12345L
llmRequest = genericLlmRequest
)
)
}
@@ -477,8 +472,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
id = "id",
partialText = "text",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest,
requestedAtMillis = 12345L
llmRequest = genericLlmRequest
)
),
unrequestedTexts = Map(),
@@ -539,8 +533,7 @@ class ClientTextStoreImplTest extends AnyFlatSpec with MockFactory with Matchers
id = "id",
partialText = "text",
requestedAfterHistoryCount = requestedAfterHistoryCount,
llmRequest = genericLlmRequest,
requestedAtMillis = 12345L
llmRequest = genericLlmRequest
)
),
unrequestedTexts = Map(),
@@ -93,8 +93,10 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/quest/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero/concrete",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
@@ -108,8 +110,10 @@ scala_test(
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:end_battle_aftermath_phase_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_against_former_on_exile",
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_from_exile",
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_from_imprisonment",
@@ -134,7 +138,6 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction:faction_relationship",
"//src/main/scala/net/eagle0/eagle/model/state/province:deferred_change_trait",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero/concrete",
@@ -189,14 +192,21 @@ scala_test(
deps = [
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:end_diplomacy_resolution_phase_action",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_runtime_validator",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_diplomacy_resolution_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
],
)
@@ -206,30 +216,47 @@ scala_test(
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:end_handle_riots_phase_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:perform_province_events_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:attack_decision_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:free_for_all_decision_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:handle_captured_heroes_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:handle_riot_do_nothing_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:manage_prisoners_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:protoless_simple_action_wrapper",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:vigor_xp_applier",
"//src/main/scala/net/eagle0/eagle/library/settings:riot_economy_devastation_delta",
"//src/main/scala/net/eagle0/eagle/library/settings:riot_infrastructure_devastation_delta",
"//src/main/scala/net/eagle0/eagle/library/settings:riot_max_food",
"//src/main/scala/net/eagle0/eagle/library/settings:riot_max_gold",
"//src/main/scala/net/eagle0/eagle/library/settings:riot_support_delta",
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:quest_fulfillment_utils",
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_runtime_validator",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_handle_riots_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:riot_aversion_failed_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:riot_occurred_result_type",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
"//src/test/scala/net/eagle0/common:proto_matchers",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
"//src/test/scala/net/eagle0/eagle/library/util/validations:testing_noop_validator",
"@maven//:org_scalamock_scalamock_3",
],
)
@@ -241,8 +268,8 @@ scala_test(
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:end_player_commands_phase_action",
"//src/main/scala/net/eagle0/eagle/library/settings:prisoner_escape_chance",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
@@ -334,11 +361,9 @@ scala_test(
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:supplies_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library:game_history",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:new_round_action",
"//src/main/scala/net/eagle0/eagle/library/settings:base_resource_limit",
@@ -356,11 +381,8 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/library/settings:percent_degradation_per_year",
"//src/main/scala/net/eagle0/eagle/library/settings:trust_delta_per_round",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
"//src/test/scala/net/eagle0/eagle/library/util/validations:testing_noop_validator",
"@maven//:org_scalamock_scalamock_3",
@@ -602,7 +624,7 @@ scala_test(
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:perform_recon_resolution_action",
"//src/main/scala/net/eagle0/eagle/library/settings:base_resource_limit",
"//src/main/scala/net/eagle0/eagle/library/settings:per_development_resource_limit",
@@ -632,8 +654,9 @@ scala_test(
name = "perform_unaffiliated_heroes_action_test",
srcs = ["PerformUnaffiliatedHeroesActionTest.scala"],
deps = [
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:perform_unaffiliated_heroes_action",
"//src/main/scala/net/eagle0/eagle/library/settings:free_hero_move_vigor_cost",
"//src/main/scala/net/eagle0/eagle/library/settings:min_odds_for_please_recruit_me",
@@ -656,25 +679,14 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/library/settings:traveler_to_resident_chance",
"//src/main/scala/net/eagle0/eagle/library/settings:vigor_to_constitution_xp_multiplier",
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_unaffiliated_hero_actions_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:hero_changed_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:hero_moved_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:hero_rejoined_result_type",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//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/faction/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero/concrete",
"//src/test/scala/net/eagle0/common:proto_matchers",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
],
)
@@ -699,15 +711,20 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state:supplies",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//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/faction/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
],
)
@@ -728,13 +745,13 @@ scala_test(
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:perform_vassal_commands_phase_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:attack_decision_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:free_for_all_decision_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:handle_captured_heroes_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:manage_prisoners_command",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/library/settings:action_vigor_cost",
"//src/main/scala/net/eagle0/eagle/library/settings:desired_loyalty_over_threshold",
@@ -758,6 +775,7 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/library/settings:vigor_gain_from_feast",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers",
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:quest_fulfillment_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
@@ -910,11 +928,18 @@ scala_test(
"//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/state:battalion_type",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//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/faction/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
],
)
@@ -978,21 +1003,14 @@ scala_test(
name = "truce_turn_back_phase_action_test",
srcs = ["TruceTurnBackPhaseActionTest.scala"],
deps = [
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:truce_turn_back_phase_action",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_truce_turn_back_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:withdrawal_for_truce_result_type",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
"//src/test/scala/net/eagle0/common:proto_matchers",
"//src/test/scala/net/eagle0/eagle/library/util/validations:testing_noop_validator",
],
)
@@ -20,9 +20,11 @@ import net.eagle0.eagle.model.state.{
Supplies
}
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.quest.concrete.TruceWithFactionQuest
import net.eagle0.eagle.model.state.run_status.RunStatus
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroT}
import net.eagle0.eagle.model.state.unaffiliated_hero.concrete.UnaffiliatedHeroC
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroType.{Outlaw, Resident, Traveler}
@@ -38,6 +40,30 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
private val currentRoundId: RoundId = 921
private val currentDate = Date(year = 221, month = Date.Month.April)
private def makeGameState(provinces: Vector[ProvinceT]): GameState = GameState(
gameId = gameId,
currentRoundId = currentRoundId,
currentPhase = RoundPhase.AttackDecision,
currentDate = Some(currentDate),
actionResultCount = 0,
provinces = provinces.map(p => p.id -> p).toMap,
heroes = Map.empty,
battalions = Map.empty,
destroyedBattalions = Map.empty,
factions = Map.empty,
factionCommandCounts = Map.empty,
killedHeroes = Map.empty,
destroyedFactions = Map.empty,
outstandingBattles = Vector.empty,
battleCounter = 0,
deferredNotifications = Vector.empty,
runStatus = RunStatus.Running,
victor = None,
battalionTypes = Vector.empty,
randomSeed = 12345L,
chronicleEntries = Vector.empty
)
private val provincesWithWithdrawingArmy: Vector[ProvinceT] = Vector(
ProvinceC(
id = 4,
@@ -121,10 +147,7 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
"execute" should "set the basics" in {
val results =
EndAttackDecisionPhaseAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
provinces = Vector.empty
makeGameState(Vector.empty)
).results
results.head.actionResultType shouldBe EndAttackDecisionPhaseResultType
@@ -149,10 +172,7 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
val results =
EndAttackDecisionPhaseAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
provinces = provincesWithTruceQuest
makeGameState(provincesWithTruceQuest)
).results
forExactly(1, results) { result =>
@@ -175,10 +195,7 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
"incoming armies still present for this round" should "set the basics" in {
val results =
EndAttackDecisionPhaseAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
provinces = provincesWithWithdrawingArmy
makeGameState(provincesWithWithdrawingArmy)
).results
results should have size 3
@@ -190,10 +207,7 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
it should "generate outlaws" in {
val result =
EndAttackDecisionPhaseAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
provinces = provincesWithWithdrawingArmy
makeGameState(provincesWithWithdrawingArmy)
)
.results(1)
@@ -222,10 +236,7 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
it should "remove the relevant hostile army" in {
val result =
EndAttackDecisionPhaseAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
provinces = provincesWithWithdrawingArmy
makeGameState(provincesWithWithdrawingArmy)
).results.head
inside(result.changedProvinces.loneElement) {
@@ -237,10 +248,7 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
it should "destroy the battalions" in {
val result =
EndAttackDecisionPhaseAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
provinces = provincesWithWithdrawingArmy
makeGameState(provincesWithWithdrawingArmy)
)
.results(1)
@@ -250,10 +258,7 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
it should "generate a notification" in {
val result =
EndAttackDecisionPhaseAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
provinces = provincesWithWithdrawingArmy
makeGameState(provincesWithWithdrawingArmy)
)
.results(1)
@@ -18,7 +18,7 @@ import net.eagle0.eagle.internal.faction.Faction
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.actions.applier.ActionResultApplierImpl
import net.eagle0.eagle.library.actions.applier.ActionResultTApplierImpl
import net.eagle0.eagle.library.actions.impl.action.EndBattleAftermathPhaseAction.RevelationChange
import net.eagle0.eagle.library.settings.{
FactionBiasAgainstFormerOnExile,
@@ -66,7 +66,7 @@ class EndBattleAftermathPhaseActionTest extends AnyFlatSpec with Matchers with B
TruceMonthsFromReturningLeader.setIntValue(12)
}
private val actionResultApplier = ActionResultApplierImpl(validator = None)
private val actionResultApplier = ActionResultTApplierImpl()
private val staticFr: FunctionalRandom = SeededRandom(0xcafe)
@@ -132,7 +132,7 @@ class EndBattleAftermathPhaseActionTest extends AnyFlatSpec with Matchers with B
val rcs =
EndBattleAftermathPhaseAction(GameStateConverter.fromProto(gameState), actionResultApplier)
.revelationChanges(GameStateConverter.fromProto(gameState))
.revelationChanges(gameState)
inside(rcs.loneElement) {
case RevelationChange(changedFaction, changedProvince) =>
@@ -728,12 +728,11 @@ class EndBattleAftermathPhaseActionTest extends AnyFlatSpec with Matchers with B
factions = mapifyFactions(Faction(id = 3))
)
val gs = GameStateConverter.fromProto(gameState)
inside(
EndBattleAftermathPhaseAction
.deferredChangeAR(
deferredChange = EndBattleAftermathPhaseAction.allDeferredChanges(gs).head,
gameState = gs,
deferredChange = EndBattleAftermathPhaseAction.allDeferredChanges(gameState).head,
gameStateProto = gameState,
functionalRandom = SeededRandom(0xfeed)
)
.newValue
@@ -803,12 +802,11 @@ class EndBattleAftermathPhaseActionTest extends AnyFlatSpec with Matchers with B
factions = mapifyFactions(Faction(id = 3))
)
val gs = GameStateConverter.fromProto(gameState)
inside(
EndBattleAftermathPhaseAction
.deferredChangeAR(
deferredChange = EndBattleAftermathPhaseAction.allDeferredChanges(gs).head,
gameState = gs,
deferredChange = EndBattleAftermathPhaseAction.allDeferredChanges(gameState).head,
gameStateProto = gameState,
functionalRandom = SeededRandom(0xfeed)
)
.newValue
@@ -856,12 +854,11 @@ class EndBattleAftermathPhaseActionTest extends AnyFlatSpec with Matchers with B
factions = mapifyFactions(Faction(id = 3))
)
val gs = GameStateConverter.fromProto(gameState)
inside(
EndBattleAftermathPhaseAction
.deferredChangeAR(
deferredChange = EndBattleAftermathPhaseAction.allDeferredChanges(gs).head,
gameState = gs,
deferredChange = EndBattleAftermathPhaseAction.allDeferredChanges(gameState).head,
gameStateProto = gameState,
functionalRandom = SeededRandom(0xface)
)
.newValue
@@ -1,10 +1,9 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.library.actions.applier.ActionResultApplierImpl
import net.eagle0.eagle.library.actions.applier.ActionResultTApplierImpl
import net.eagle0.eagle.library.util.validations.ScalaRuntimeValidator
import net.eagle0.eagle.model.action_result.types.EndDiplomacyResolutionPhaseResultType
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.run_status.RunStatus
import net.eagle0.eagle.model.state.RoundPhase
@@ -21,7 +20,7 @@ class EndDiplomacyResolutionPhaseActionTest extends AnyFlatSpec with Matchers wi
gameId = 1L,
currentRoundId = 1,
currentPhase = RoundPhase.DiplomacyResolution,
currentDate = Some(Date(year = 1, month = Date.Month.January)),
currentDate = None,
actionResultCount = 0,
provinces = Map.empty,
heroes = Map.empty,
@@ -41,7 +40,7 @@ class EndDiplomacyResolutionPhaseActionTest extends AnyFlatSpec with Matchers wi
chronicleEntries = Vector.empty
)
private val applier = ActionResultApplierImpl(Some(ScalaRuntimeValidator))
private val applier = ActionResultTApplierImpl(ScalaRuntimeValidator)
private val functionalRandom = SeededRandom(282)
override def beforeEach(): Unit = {}
@@ -65,7 +64,7 @@ class EndDiplomacyResolutionPhaseActionTest extends AnyFlatSpec with Matchers wi
// as Unresolved in the stale initial game state
// The fix ensures that each action in the sequence sees the updated game state
// from previous actions via the RandomStateSequencer
// from previous actions via the RandomStateTSequencer
// Create a game state with diplomacy offers
val testGameState = gameState.copy(
@@ -74,7 +73,7 @@ class EndDiplomacyResolutionPhaseActionTest extends AnyFlatSpec with Matchers wi
)
// This should not throw an exception even if offers get invalidated
// The RandomStateSequencer ensures later actions see updated state
// The RandomStateTSequencer ensures later actions see updated state
val results = EndDiplomacyResolutionPhaseAction(testGameState, applier)
.randomResults(functionalRandom)
.newValue
@@ -87,6 +86,6 @@ class EndDiplomacyResolutionPhaseActionTest extends AnyFlatSpec with Matchers wi
// The key improvement is that this no longer throws exceptions
// when invalidation occurs, because later actions now receive
// the updated game state via RandomStateSequencer
// the updated game state via RandomStateTSequencer
}
}
@@ -1,10 +1,19 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.SeededRandom
import net.eagle0.common.ProtoMatchers.equalProto
import net.eagle0.eagle.api.available_command.HandleRiotDoNothingAvailableCommand
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
import net.eagle0.eagle.library.actions.applier.ActionResultApplierImpl
import net.eagle0.eagle.library.actions.impl.command.{HandleRiotDoNothingCommand, TCommandFactory}
import net.eagle0.eagle.common.action_result_type.ActionResultType
import net.eagle0.eagle.common.action_result_type.ActionResultType.{RIOT_AVERSION_FAILED, RIOT_AVERTED}
import net.eagle0.eagle.common.date.Date
import net.eagle0.eagle.common.province_event.ImminentRiotEvent
import net.eagle0.eagle.common.round_phase.{NewRoundPhase, RoundPhase}
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.actions.applier.{ActionResultProtoApplierImpl, ActionResultTApplierImpl}
import net.eagle0.eagle.library.actions.impl.command.{CommandFactory, HandleRiotDoNothingCommand}
import net.eagle0.eagle.library.actions.impl.common.TCommand
import net.eagle0.eagle.library.settings.{
RiotEconomyDevastationDelta,
@@ -13,20 +22,9 @@ import net.eagle0.eagle.library.settings.{
RiotMaxGold,
RiotSupportDelta
}
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.types.{
EndHandleRiotsPhaseResultType,
RiotAversionFailedResultType,
RiotOccurredResultType
}
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.province.{ImminentRiotEvent, ProvinceOrderType}
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.state.run_status.RunStatus
import net.eagle0.eagle.model.state.RoundPhase
import net.eagle0.eagle.library.util.validations.{ScalaRuntimeValidator, TestingNoopValidator}
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
@@ -34,43 +32,31 @@ import org.scalatest.BeforeAndAfterEach
class EndHandleRiotsPhaseActionTest extends AnyFlatSpec with BeforeAndAfterEach with Matchers with MockFactory {
private val applier = ActionResultApplierImpl(None)
private val commandFactory = mock[TCommandFactory]
private val actionResultProtoApplier = new ActionResultProtoApplierImpl(
validator = TestingNoopValidator
)
private val applier = ActionResultTApplierImpl(ScalaRuntimeValidator)
private val commandFactory = mock[CommandFactory]
val fid = 19
val hid = 25
val pid = 7
private val province = ProvinceC(
private val provinceProto = Province(
id = pid,
rulingFactionId = Some(fid),
rulingHeroId = Some(hid),
rulingFactionHeroIds = Vector(hid),
provinceOrders = ProvinceOrderType.UnknownOrderType
rulingHeroId = Some(hid)
)
private val province = ProvinceConverter.fromProto(provinceProto)
private val gameState = GameState(
gameId = 1,
currentDate = Some(Date(year = 1501, month = Date.Month.June)),
currentRoundId = 1,
currentPhase = RoundPhase.HandleRiot,
actionResultCount = 0,
provinces = Map(pid -> province),
factions = Map(fid -> FactionC(id = fid, factionHeadId = 99, name = "Test Faction", leaderIds = Vector(99))),
heroes = Map(hid -> HeroC(id = hid, factionId = Some(fid))),
battalions = Map.empty,
destroyedBattalions = Map.empty,
factionCommandCounts = Map.empty,
killedHeroes = Map.empty,
destroyedFactions = Map.empty,
outstandingBattles = Vector.empty,
battleCounter = 0,
deferredNotifications = Vector.empty,
runStatus = RunStatus.Running,
victor = None,
battalionTypes = Vector.empty,
private val gameState: GameState = GameState(
currentDate = Some(Date(year = 1501, month = 6)),
randomSeed = 1L,
chronicleEntries = Vector.empty
currentPhase = RoundPhase.HANDLE_RIOT,
provinces = Map(
pid -> provinceProto
),
factions = Map(fid -> Faction(id = fid, factionHeadId = 99, leaders = Vector(99)))
)
override def beforeEach(): Unit = {
@@ -90,17 +76,23 @@ class EndHandleRiotsPhaseActionTest extends AnyFlatSpec with BeforeAndAfterEach
)
)
"results" should "include the end phase result" in {
val results = EndHandleRiotsPhaseAction(
gameState,
commandsForProvince.get,
commandFactory,
applier
).results(SeededRandom(gameState.randomSeed))
results.last.actionResultType shouldBe EndHandleRiotsPhaseResultType
results.last.newRoundPhase shouldBe Some(RoundPhase.HeroDepartures)
}
"results" should "include the end phase result" in
exactly(
1,
EndHandleRiotsPhaseAction(
GameStateConverter.fromProto(gameState),
commandsForProvince.get,
commandFactory,
applier
).execute(gameState, actionResultProtoApplier).map(_.actionResult)
).should(
equalProto(
ActionResult(
`type` = ActionResultType.END_HANDLE_RIOTS_PHASE,
newRoundPhase = Some(NewRoundPhase(RoundPhase.HERO_DEPARTURES))
)
)
)
it should "include a handle riot result for a province led by a vassal with an imminent riot" in {
commandFactory.makeTCommand
@@ -114,25 +106,21 @@ class EndHandleRiotsPhaseActionTest extends AnyFlatSpec with BeforeAndAfterEach
)
)
val gsWithRiot = gameState.copy(
provinces = Map(
pid -> province.copy(activeEvents = Vector(ImminentRiotEvent()))
)
val gsWithRiot = gameState.update(
_.provinces(pid).activeEvents := Vector(ImminentRiotEvent())
)
val results = EndHandleRiotsPhaseAction(
gsWithRiot,
GameStateConverter.fromProto(gsWithRiot),
commandsForProvince.get,
commandFactory,
applier
).results(SeededRandom(gsWithRiot.randomSeed))
val result = results.head
result.provinceId shouldBe Some(pid)
result.actingFactionId shouldBe Some(fid)
).execute(gsWithRiot, actionResultProtoApplier).map(_.actionResult)
val result = results.head
result.province.shouldBe(Some(pid))
result.player.shouldBe(Some(fid))
result.changedProvinces.should(have.size(1))
val changedProvince = result.changedProvinces.head.asInstanceOf[ChangedProvinceC]
changedProvince.setHasActed shouldBe Some(true)
result.actionResultType should (be(RiotAversionFailedResultType) or be(RiotOccurredResultType))
result.changedProvinces.head.setHasActed.should(contain(true))
result.`type`.should(be(RIOT_AVERSION_FAILED) or be(RIOT_AVERTED))
}
}
@@ -22,7 +22,7 @@ import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.{Neighbor, Province}
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
import net.eagle0.eagle.library.actions.applier.ActionResultApplierImpl
import net.eagle0.eagle.library.actions.applier.ActionResultTApplierImpl
import net.eagle0.eagle.library.settings.PrisonerEscapeChance
import net.eagle0.eagle.library.util.IDable.{mapifyFactions, mapifyHeroes, mapifyProvinces}
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
@@ -53,7 +53,7 @@ class EndPlayerCommandsPhaseActionTest extends AnyFlatSpec with Matchers with Be
PrisonerEscapeChance.setDoubleValue(0.05)
}
private val actionResultApplier = ActionResultApplierImpl(validator = None)
private val actionResultApplier = ActionResultTApplierImpl()
private val rulingFactionId = 5
private val enemyFactionId: FactionId = 15
@@ -1,6 +1,5 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.common.action_result_type.ActionResultType.{NEW_ROUND_ACTION, NEW_YEAR_ACTION}
import net.eagle0.eagle.common.date.Date as DateProto
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType.DEVELOP
@@ -12,7 +11,6 @@ import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
UNAFFILIATED_HERO_PRISONER,
UNAFFILIATED_HERO_TRAVELER
}
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.army.{Army, HostileArmyGroup, MovingArmy, Withdrawing}
import net.eagle0.eagle.internal.changed_faction.{ChangedFaction, TrustLevelUpdate}
import net.eagle0.eagle.internal.changed_hero.ChangedHero
@@ -28,7 +26,7 @@ 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.{EagleInternalException, GameHistory}
import net.eagle0.eagle.library.actions.applier.{ActionResultApplierImpl, ActionResultProtoApplierImpl}
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplierImpl
import net.eagle0.eagle.library.actions.impl.WaitingAction
import net.eagle0.eagle.library.settings.{
BaseResourceLimit,
@@ -49,9 +47,7 @@ import net.eagle0.eagle.library.settings.{
import net.eagle0.eagle.library.util.validations.TestingNoopValidator
import net.eagle0.eagle.library.util.IDable.{mapifyFactions, mapifyHeroes}
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.state.date.Date as ScalaDate
import net.eagle0.eagle.model.state.game_state.GameState as ScalaGameState
import org.scalamock.scalatest.MockFactory
import org.scalatest.{BeforeAndAfterEach, OneInstancePerTest}
import org.scalatest.flatspec.AnyFlatSpec
@@ -72,17 +68,6 @@ class NewRoundActionTest
validator = TestingNoopValidator
)
private val actionResultApplier = new ActionResultApplierImpl(
validator = None
)
// Helper to run NewRoundAction and convert results to proto for testing
private def runNewRoundAction(gameState: ScalaGameState): Vector[ActionResult] =
NewRoundAction(gameState, mockHistory, actionResultApplier)
.randomResults(SeededRandom(0))
.newValue
.map(ActionResultProtoConverter.toProto)
mockHistory.sinceDate
.expects(ScalaDate(year = 0, month = ScalaDate.Month.January))
.returning(Vector.empty)
@@ -119,7 +104,7 @@ class NewRoundActionTest
"execute" should "advance the round ID" in {
val results =
runNewRoundAction(GameStateConverter.fromProto(midyearGameState))
NewRoundAction(GameStateConverter.fromProto(midyearGameState), mockHistory).resultsOfExecute()
results.head.newRoundId shouldBe Some(8)
}
@@ -137,7 +122,7 @@ class NewRoundActionTest
)
val results =
runNewRoundAction(GameStateConverter.fromProto(gsWithDevastation))
NewRoundAction(GameStateConverter.fromProto(gsWithDevastation), mockHistory).resultsOfExecute()
inside(results.head.changedProvinces.head) {
case cp: ChangedProvince =>
@@ -162,7 +147,7 @@ class NewRoundActionTest
)
val results =
runNewRoundAction(GameStateConverter.fromProto(gsWithDevastation))
NewRoundAction(GameStateConverter.fromProto(gsWithDevastation), mockHistory).resultsOfExecute()
inside(results.head.changedProvinces.head) {
case cp: ChangedProvince =>
@@ -176,7 +161,7 @@ class NewRoundActionTest
it should "advance the round phase" in {
val results =
runNewRoundAction(GameStateConverter.fromProto(midyearGameState))
NewRoundAction(GameStateConverter.fromProto(midyearGameState), mockHistory).resultsOfExecute()
results.head.newRoundPhase shouldBe Some(
NewRoundPhase(RoundPhase.PRISONER_EXCHANGE)
@@ -185,14 +170,14 @@ class NewRoundActionTest
it should "advance the month" in {
val results =
runNewRoundAction(GameStateConverter.fromProto(midyearGameState))
NewRoundAction(GameStateConverter.fromProto(midyearGameState), mockHistory).resultsOfExecute()
results.head.newDate shouldBe Some(DateProto(year = 252, month = 8))
}
it should "not include a NewYearAction if the month is not 12" in {
val results =
runNewRoundAction(GameStateConverter.fromProto(midyearGameState))
NewRoundAction(GameStateConverter.fromProto(midyearGameState), mockHistory).resultsOfExecute()
results.map(_.`type`) should not contain NEW_YEAR_ACTION
}
@@ -201,7 +186,7 @@ class NewRoundActionTest
val endOfYearGameState =
midyearGameState.withCurrentDate(DateProto(year = 252, month = 12))
val results =
runNewRoundAction(GameStateConverter.fromProto(endOfYearGameState))
NewRoundAction(GameStateConverter.fromProto(endOfYearGameState), mockHistory).resultsOfExecute()
results.find(_.`type` == NEW_ROUND_ACTION).get.newDate shouldBe Some(
DateProto(year = 253, month = 1)
@@ -212,7 +197,7 @@ class NewRoundActionTest
val endOfYearGameState =
midyearGameState.withCurrentDate(DateProto(year = 252, month = 12))
val results =
runNewRoundAction(GameStateConverter.fromProto(endOfYearGameState))
NewRoundAction(GameStateConverter.fromProto(endOfYearGameState), mockHistory).resultsOfExecute()
results.head.`type` shouldBe NEW_YEAR_ACTION
}
@@ -226,7 +211,7 @@ class NewRoundActionTest
val gameState =
midyearGameState.withHeroes(Map(startingHero.id -> startingHero))
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
results.head.changedHeroes shouldBe Vector(
ChangedHero(id = startingHero.id, vigor = VigorDelta(7))
@@ -242,7 +227,7 @@ class NewRoundActionTest
val gameState =
midyearGameState.withHeroes(Map(startingHero.id -> startingHero))
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
results.head.changedHeroes shouldBe Vector(
ChangedHero(id = startingHero.id, vigor = VigorDelta(2))
@@ -264,7 +249,7 @@ class NewRoundActionTest
val gameState =
midyearGameState.withHeroes(Map(startingHero.id -> startingHero))
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
results.head.newHeroes shouldBe empty
}
@@ -320,7 +305,7 @@ class NewRoundActionTest
)
)
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
results.head.changedHeroes should contain theSameElementsAs (Vector(
ChangedHero(id = 1, loyalty = LoyaltyDelta(-2)),
@@ -379,7 +364,7 @@ class NewRoundActionTest
)
)
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
results.head.changedHeroes shouldBe empty
}
@@ -435,7 +420,7 @@ class NewRoundActionTest
)
)
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
results.head.changedHeroes should contain theSameElementsAs (1 to 12).toVector
.map(hid => ChangedHero(id = hid, loyalty = LoyaltyDelta(-1)))
@@ -458,7 +443,7 @@ class NewRoundActionTest
// overage is 15000
// loss is 15000 * 0.2 = 3000
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
results.head.changedProvinces.head.foodDelta shouldBe Some(-3000)
}
@@ -480,7 +465,7 @@ class NewRoundActionTest
// overage is 5500
// loss is 5500 * 0.2 = 1100
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
results.head.changedProvinces.head.goldDelta shouldBe Some(-1100)
}
@@ -525,7 +510,10 @@ class NewRoundActionTest
)
)
val cps = runNewRoundAction(GameStateConverter.fromProto(gameState)).head.changedProvinces
val cps = NewRoundAction(
GameStateConverter.fromProto(gameState),
mockHistory
).resultsOfExecute().head.changedProvinces
forExactly(1, cps) { cp =>
cp.id shouldBe 7
@@ -605,7 +593,10 @@ class NewRoundActionTest
)
)
val cps = runNewRoundAction(GameStateConverter.fromProto(gameState)).head.changedProvinces
val cps = NewRoundAction(
GameStateConverter.fromProto(gameState),
mockHistory
).resultsOfExecute().head.changedProvinces
inside(cps.head) {
case cp: ChangedProvince =>
@@ -707,7 +698,11 @@ class NewRoundActionTest
)
)
val changedFactions = runNewRoundAction(GameStateConverter.fromProto(gsWithTruces)).head.changedFactions
val changedFactions =
NewRoundAction(
GameStateConverter.fromProto(gsWithTruces),
mockHistory
).resultsOfExecute().head.changedFactions
changedFactions
.find(_.id == 3)
@@ -772,7 +767,11 @@ class NewRoundActionTest
)
)
val changedFactions = runNewRoundAction(GameStateConverter.fromProto(gsWithTruces)).head.changedFactions
val changedFactions =
NewRoundAction(
GameStateConverter.fromProto(gsWithTruces),
mockHistory
).resultsOfExecute().head.changedFactions
changedFactions should contain theSameElementsAs Vector(
ChangedFaction(
@@ -812,8 +811,9 @@ class NewRoundActionTest
)
val ex = the[EagleInternalException] thrownBy
NewRoundAction(GameStateConverter.fromProto(gameStateWithOldArmy), mockHistory, actionResultApplier)
.randomResults(SeededRandom(0))
NewRoundAction(GameStateConverter.fromProto(gameStateWithOldArmy), mockHistory).execute(
actionResultProtoApplier
)
ex.getMessage shouldBe "requirement failed: Province 9 has an army that should have already arrived"
}
@@ -832,8 +832,9 @@ class NewRoundActionTest
)
val ex = the[EagleInternalException] thrownBy
NewRoundAction(GameStateConverter.fromProto(gameStateWithHostileArmy), mockHistory, actionResultApplier)
.randomResults(SeededRandom(0))
NewRoundAction(GameStateConverter.fromProto(gameStateWithHostileArmy), mockHistory).execute(
actionResultProtoApplier
)
ex.getMessage shouldBe "requirement failed: Province 9 has hostile armies at the round start"
}
@@ -841,7 +842,7 @@ class NewRoundActionTest
it should "include a chronicle notification if in November" in {
val gameState =
midyearGameState.withCurrentDate(DateProto(year = 252, month = 10))
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
val relevantAction = results.find(_.`type` == NEW_ROUND_ACTION).get
relevantAction.newChronicleEntry should contain(
@@ -855,7 +856,7 @@ class NewRoundActionTest
it should "include a chronicle LLM request if in November" in {
val gameState =
midyearGameState.withCurrentDate(DateProto(year = 252, month = 10))
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
val relevantAction = results.find(_.`type` == NEW_ROUND_ACTION).get
relevantAction.newGeneratedTextRequests should have size 1
@@ -878,7 +879,7 @@ class NewRoundActionTest
)
)
val results = runNewRoundAction(GameStateConverter.fromProto(gameState))
val results = NewRoundAction(GameStateConverter.fromProto(gameState), mockHistory).resultsOfExecute()
forExactly(1, results) { result =>
result.changedProvinces.head.newPriceIndex shouldBe Some(
@@ -13,12 +13,12 @@ import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.{IncomingEndTurnAction, Province}
import net.eagle0.eagle.internal.province.IncomingEndTurnAction.IncomingRecon
import net.eagle0.eagle.library.actions.applier.ActionResultApplierImpl
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplierImpl
import net.eagle0.eagle.library.settings.{BaseResourceLimit, PerDevelopmentResourceLimit}
import net.eagle0.eagle.library.util.validations.TestingNoopValidator
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC, ClientTextVisibilityExtensionC}
import net.eagle0.eagle.model.action_result.types.{EndReconResolutionPhaseResultType, ReconSucceededResultType}
import net.eagle0.eagle.model.action_result.types.ReconSucceededResultType
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.state.province.{
IncomingEndTurnAction as IncomingEndTurnActionT,
@@ -30,7 +30,9 @@ import org.scalatest.BeforeAndAfterEach
class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEach with Matchers {
private val actionResultApplier = ActionResultApplierImpl(validator = None)
private val actionResultProtoApplier = new ActionResultProtoApplierImpl(
validator = TestingNoopValidator
)
private val actingFaction = 7
private val incomingRecon: IncomingEndTurnAction = IncomingEndTurnAction(
@@ -104,10 +106,10 @@ class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEa
}
"perform one" should "remove the incoming recon" in {
val oneResult = PerformReconResolutionAction(GameStateConverter.fromProto(gameState), actionResultApplier)
val oneResult = PerformReconResolutionAction(GameStateConverter.fromProto(gameState))
.oneResult(
provinceId = 3,
incomingEndTurnAction = incomingReconT,
incomingEndTurnAction = incomingRecon,
functionalRandom = functionalRandom
)
.newValue
@@ -122,10 +124,10 @@ class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEa
}
it should "return the hero to the origin province if it's still owned" in {
val oneResult = PerformReconResolutionAction(GameStateConverter.fromProto(gameState), actionResultApplier)
val oneResult = PerformReconResolutionAction(GameStateConverter.fromProto(gameState))
.oneResult(
provinceId = 3,
incomingEndTurnAction = incomingReconT,
incomingEndTurnAction = incomingRecon,
functionalRandom = functionalRandom
)
.newValue
@@ -144,10 +146,10 @@ class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEa
gameState.update(_.provinces(9).rulingFactionId := 2)
val oneResult =
PerformReconResolutionAction(GameStateConverter.fromProto(gsWithLostProvince), actionResultApplier)
PerformReconResolutionAction(GameStateConverter.fromProto(gsWithLostProvince))
.oneResult(
provinceId = 3,
incomingEndTurnAction = incomingReconT,
incomingEndTurnAction = incomingRecon,
functionalRandom = functionalRandom
)
.newValue
@@ -168,10 +170,10 @@ class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEa
)
val oneResult =
PerformReconResolutionAction(GameStateConverter.fromProto(gsWithLostProvinces), actionResultApplier)
PerformReconResolutionAction(GameStateConverter.fromProto(gsWithLostProvinces))
.oneResult(
provinceId = 3,
incomingEndTurnAction = incomingReconT,
incomingEndTurnAction = incomingRecon,
functionalRandom = functionalRandom
)
.newValue
@@ -186,10 +188,10 @@ class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEa
}
it should "update the faction with the info about the province" in {
val oneResult = PerformReconResolutionAction(GameStateConverter.fromProto(gameState), actionResultApplier)
val oneResult = PerformReconResolutionAction(GameStateConverter.fromProto(gameState))
.oneResult(
provinceId = 3,
incomingEndTurnAction = incomingReconT,
incomingEndTurnAction = incomingRecon,
functionalRandom = functionalRandom
)
.newValue
@@ -207,10 +209,10 @@ class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEa
}
it should "extend client text visibility for the heroes in the reconned province" in {
val oneResult = PerformReconResolutionAction(GameStateConverter.fromProto(gameState), actionResultApplier)
val oneResult = PerformReconResolutionAction(GameStateConverter.fromProto(gameState))
.oneResult(
provinceId = 3,
incomingEndTurnAction = incomingReconT,
incomingEndTurnAction = incomingRecon,
functionalRandom = functionalRandom
)
.newValue
@@ -260,11 +262,10 @@ class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEa
)
val allResults = PerformReconResolutionAction(
GameStateConverter.fromProto(gameStateWithMoreRecons),
actionResultApplier
).results(functionalRandom)
GameStateConverter.fromProto(gameStateWithMoreRecons)
).execute(actionResultProtoApplier).map(_.actionResult)
allResults.count(_.actionResultType == ReconSucceededResultType) shouldBe 3
allResults.count(_.actionResultType == EndReconResolutionPhaseResultType) shouldBe 1
allResults.count(_.`type` == RECON_SUCCEEDED) shouldBe 3
allResults.count(_.`type` == END_RECON_RESOLUTION_PHASE) shouldBe 1
}
}
@@ -1,7 +1,42 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.library.actions.applier.ActionResultApplierImpl
import net.eagle0.common.ProtoMatchers.equalProto
import net.eagle0.eagle.common.action_result_notification_details.{Notification, OutlawSpottedDetails}
import net.eagle0.eagle.common.action_result_notification_details.Notification.TargetFaction
import net.eagle0.eagle.common.action_result_type.ActionResultType.{
END_UNAFFILIATED_HERO_ACTIONS_PHASE,
HERO_CHANGED,
HERO_MOVED,
HERO_REJOINED
}
import net.eagle0.eagle.common.date.Date
import net.eagle0.eagle.common.gender.Gender.GENDER_FEMALE
import net.eagle0.eagle.common.profession.Profession.NO_PROFESSION
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType.ENTRUST
import net.eagle0.eagle.common.recruitment_info.RecruitmentInfo
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.{
RECRUITMENT_STATUS_NO_RULER_IN_PROVINCE,
RECRUITMENT_STATUS_OUTLAW,
RECRUITMENT_STATUS_PRISONER,
RECRUITMENT_STATUS_TRAVELER
}
import net.eagle0.eagle.common.round_phase.RoundPhase.UNAFFILIATED_HERO_ACTIONS
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
UNAFFILIATED_HERO_OUTLAW,
UNAFFILIATED_HERO_PRISONER,
UNAFFILIATED_HERO_RESIDENT,
UNAFFILIATED_HERO_TRAVELER
}
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.changed_hero.ChangedHero
import net.eagle0.eagle.internal.changed_hero.ChangedHero.Vigor.VigorDelta
import net.eagle0.eagle.internal.changed_province.ChangedProvince
import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.{Neighbor, Province}
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
import net.eagle0.eagle.library.actions.impl.WaitingAction
import net.eagle0.eagle.library.settings.{
FreeHeroMoveVigorCost,
MinOddsForPleaseRecruitMe,
@@ -25,59 +60,16 @@ import net.eagle0.eagle.library.settings.{
VigorToConstitutionXpMultiplier,
XpForStatBump
}
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.{ChangedHeroC, StatDelta}
import net.eagle0.eagle.model.action_result.types.{
EndUnaffiliatedHeroActionsPhaseResultType,
HeroChangedResultType,
HeroMovedResultType,
HeroRejoinedResultType
}
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.hero.Gender
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.{BlizzardEvent, Neighbor, ProvinceOrderType, ProvinceT}
import net.eagle0.eagle.library.util.IDable.{mapifyFactions, mapifyHeroes, mapifyProvinces}
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.state.date.Date as ScalaDate
import net.eagle0.eagle.model.state.province.{BlizzardEvent as ScalaBlizzardEvent, Neighbor as ScalaNeighbor, ProvinceT}
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.state.run_status.RunStatus
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
import net.eagle0.eagle.model.state.unaffiliated_hero.concrete.UnaffiliatedHeroC
import net.eagle0.eagle.model.state.RoundPhase
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfterEach
import org.scalatest.Inside.inside
class PerformUnaffiliatedHeroesActionTest extends AnyFlatSpec with Matchers with BeforeAndAfterEach {
private val applier = ActionResultApplierImpl(None)
private val baseGameState: GameState = GameState(
gameId = 1L,
currentRoundId = 1,
currentPhase = RoundPhase.UnaffiliatedHeroActions,
currentDate = Some(Date(year = 3, month = Date.Month.October)),
actionResultCount = 0,
provinces = Map.empty,
heroes = Map.empty,
battalions = Map.empty,
destroyedBattalions = Map.empty,
factions = Map.empty,
factionCommandCounts = Map.empty,
killedHeroes = Map.empty,
destroyedFactions = Map.empty,
outstandingBattles = Vector.empty,
battleCounter = 0,
deferredNotifications = Vector.empty,
runStatus = RunStatus.Running,
victor = None,
battalionTypes = Vector.empty,
randomSeed = 12345L,
chronicleEntries = Vector.empty
)
override def beforeEach(): Unit = {
RestVigorGain.setDoubleValue(17.2)
MinVigorForFreeHeroMove.setDoubleValue(60)
@@ -104,22 +96,23 @@ class PerformUnaffiliatedHeroesActionTest extends AnyFlatSpec with Matchers with
}
"movableNeighbors" should "include exactly neighbors without a blizzard" in {
// Use Scala types directly to avoid proto conversion issues with BlizzardEvent
val provinces: Map[Int, ProvinceT] = Map(
3 -> ProvinceC(id = 3),
6 -> ProvinceC(
id = 6,
neighbors = Vector(
Neighbor(provinceId = 3, startingPositionIndex = 0),
Neighbor(provinceId = 7, startingPositionIndex = 1),
Neighbor(provinceId = 12, startingPositionIndex = 2)
ScalaNeighbor(provinceId = 3, startingPositionIndex = 0),
ScalaNeighbor(provinceId = 7, startingPositionIndex = 1),
ScalaNeighbor(provinceId = 12, startingPositionIndex = 2)
)
),
7 -> ProvinceC(
id = 7,
activeEvents = Vector(
BlizzardEvent(
startDate = Date(1, Date.Month.January),
endDate = Date(1, Date.Month.March)
ScalaBlizzardEvent(
startDate = ScalaDate(1, ScalaDate.Month.January),
endDate = ScalaDate(1, ScalaDate.Month.March)
)
)
),
@@ -134,410 +127,512 @@ class PerformUnaffiliatedHeroesActionTest extends AnyFlatSpec with Matchers with
}
"an unaffiliated hero with low vigor" should "rest" in {
val heroes: Map[Int, HeroT] = Map(
19 -> HeroC(id = 19, vigor = 33, pronounGender = Gender.Female)
)
val provinces: Map[Int, ProvinceT] = Map(
4 -> ProvinceC(
id = 4,
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 19,
unaffiliatedHeroType = UnaffiliatedHeroType.Traveler,
recruitmentInfo = RecruitmentInfo.Traveler
val gs = GameState(
currentPhase = UNAFFILIATED_HERO_ACTIONS,
currentDate = Some(Date(3, 273)),
heroes = Map(
19 -> Hero(
id = 19,
vigor = 33,
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE
)
),
provinces = Map(
4 -> Province(
id = 4,
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
`type` = UNAFFILIATED_HERO_TRAVELER,
heroId = 19,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_TRAVELER))
)
)
)
)
)
val gs = baseGameState.copy(heroes = heroes, provinces = provinces)
val results = PerformUnaffiliatedHeroesAction(
gameState = gs,
heroGenerator = null,
actionResultApplier = applier
).results(SeededRandom(gs.randomSeed))
gameState = GameStateConverter.fromProto(gs),
heroGenerator = null
).resultsOfExecute()
results should have size 4
val firstResult = results.head
firstResult.actionResultType shouldBe HeroChangedResultType
firstResult.changedHeroes should have size 1
inside(firstResult.changedHeroes.head) {
case changedHero: ChangedHeroC =>
changedHero.heroId shouldBe 19
changedHero.vigorChange shouldBe StatDelta(17.2)
}
// The batched ActionResult contains only changedHeroes for vigor changes
// (no province field since vigor changes are keyed by hero ID)
results.head should equalProto(
ActionResult(
`type` = HERO_CHANGED,
changedHeroes = Vector(
ChangedHero(id = 19, vigor = VigorDelta(17.2))
)
)
)
}
"an unaffiliated hero attached to a faction" should "return to the faction and rejoin" in {
val provinces: Map[Int, ProvinceT] = Map(
3 -> ProvinceC(
id = 3,
neighbors = Vector(Neighbor(provinceId = 8, startingPositionIndex = 0)),
rulingFactionId = Some(6),
rulingFactionHeroIds = Vector(9, 11),
provinceOrders = ProvinceOrderType.Entrust
val gs = GameState(
currentPhase = UNAFFILIATED_HERO_ACTIONS,
currentDate = Some(Date(3, 273)),
provinces = mapifyProvinces(
Province(
id = 3,
neighbors = Vector(Neighbor(provinceId = 8)),
rulingFactionId = Some(6),
rulingFactionHeroIds = Vector(9, 11),
provinceOrders = ENTRUST
),
Province(
id = 8,
neighbors = Vector(Neighbor(provinceId = 3)),
rulingFactionId = Some(23),
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_OUTLAW,
lastFaction = Some(6),
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_OUTLAW))
)
)
)
),
8 -> ProvinceC(
id = 8,
neighbors = Vector(Neighbor(provinceId = 3, startingPositionIndex = 0)),
rulingFactionId = Some(23),
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 17,
unaffiliatedHeroType = UnaffiliatedHeroType.Outlaw,
lastFactionId = Some(6),
recruitmentInfo = RecruitmentInfo.Outlaw
heroes = mapifyHeroes(
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 17,
vigor = 90
),
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 9,
factionId = Some(6)
),
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 11,
factionId = Some(6)
)
),
factions = mapifyFactions(Faction(id = 6, factionHeadId = 9))
)
val results = PerformUnaffiliatedHeroesAction(
gameState = GameStateConverter.fromProto(gs),
heroGenerator = null
).resultsOfExecute()
results should have size 5
results.head should equalProto(
ActionResult(
`type` = HERO_MOVED,
changedProvinces = Vector(
ChangedProvince(
id = 8,
removedUnaffiliatedHeroIds = Vector(17)
),
ChangedProvince(
id = 3,
newUnaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_OUTLAW,
lastFaction = Some(6),
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_OUTLAW))
)
)
)
),
changedHeroes = Vector(
ChangedHero(
id = 17,
vigor = VigorDelta(-10),
constitutionXpDelta = Some(1)
)
),
notificationsToDeliver = Vector(
Notification(
details = OutlawSpottedDetails(outlawHeroId = 17, provinceId = 3),
targetFactions = Vector(TargetFaction(23), TargetFaction(6))
)
)
)
)
val heroes: Map[Int, HeroT] = Map(
17 -> HeroC(id = 17, pronounGender = Gender.Female, vigor = 90),
9 -> HeroC(id = 9, pronounGender = Gender.Female, factionId = Some(6)),
11 -> HeroC(id = 11, pronounGender = Gender.Female, factionId = Some(6))
results(2) should equalProto(
ActionResult(
`type` = HERO_REJOINED,
changedProvinces = Vector(
ChangedProvince(
id = 3,
removedUnaffiliatedHeroIds = Vector(17),
addedRulingPlayerHeroIds = Vector(17)
)
),
changedHeroes = Vector(
ChangedHero(
id = 17,
newFactionId = Some(6)
)
)
)
)
val factions: Map[Int, FactionT] = Map(
6 -> FactionC(id = 6, factionHeadId = 9, name = "", leaderIds = Vector())
)
val gs = baseGameState.copy(provinces = provinces, heroes = heroes, factions = factions)
val results = PerformUnaffiliatedHeroesAction(
gameState = gs,
heroGenerator = null,
actionResultApplier = applier
).results(SeededRandom(gs.randomSeed))
results should have size 5
val moveResult = results.head
moveResult.actionResultType shouldBe HeroMovedResultType
moveResult.changedProvinces should have size 2
inside(moveResult.changedProvinces.find(_.provinceId == 8).get) {
case moveFromProvince: ChangedProvinceC =>
moveFromProvince.removedUnaffiliatedHeroIds should contain(17)
}
inside(moveResult.changedProvinces.find(_.provinceId == 3).get) {
case moveToProvince: ChangedProvinceC =>
moveToProvince.newUnaffiliatedHeroes.head.heroId shouldBe 17
}
inside(moveResult.changedHeroes.head) {
case movedHero: ChangedHeroC =>
movedHero.heroId shouldBe 17
movedHero.vigorChange shouldBe StatDelta(-10.0)
}
moveResult.newNotifications should have size 1
moveResult.newNotifications.head.targetFactionIds should contain allOf (23, 6)
val rejoinResult = results(2)
rejoinResult.actionResultType shouldBe HeroRejoinedResultType
inside(rejoinResult.changedProvinces.head) {
case rejoinProvince: ChangedProvinceC =>
rejoinProvince.provinceId shouldBe 3
rejoinProvince.removedUnaffiliatedHeroIds should contain(17)
rejoinProvince.newRulingFactionHeroIds should contain(17)
}
inside(rejoinResult.changedHeroes.head) {
case rejoinedHero: ChangedHeroC =>
rejoinedHero.heroId shouldBe 17
rejoinedHero.newFactionId shouldBe Some(6)
}
results(3).actionResultType shouldBe EndUnaffiliatedHeroActionsPhaseResultType
results(3).`type` shouldBe END_UNAFFILIATED_HERO_ACTIONS_PHASE
}
"a prisoner in an owned province" should "not generate a result" in {
val provinces: Map[Int, ProvinceT] = Map(
8 -> ProvinceC(
id = 8,
rulingFactionId = Some(23),
rulingFactionHeroIds = Vector(9, 11),
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 17,
unaffiliatedHeroType = UnaffiliatedHeroType.Prisoner,
lastFactionId = Some(6),
recruitmentInfo = RecruitmentInfo.Prisoner
)
val gs = GameState(
currentPhase = UNAFFILIATED_HERO_ACTIONS,
currentDate = Some(Date(3, 273)),
provinces = mapifyProvinces(
Province(
id = 8,
rulingFactionId = Some(23),
rulingFactionHeroIds = Vector(9, 11),
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_PRISONER,
lastFaction = Some(6),
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_PRISONER))
)
),
provinceOrders = ENTRUST
)
),
heroes = mapifyHeroes(
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 17,
vigor = 90
),
provinceOrders = ProvinceOrderType.Entrust
)
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 9,
factionId = Some(23)
),
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 11,
factionId = Some(23)
)
),
factions = mapifyFactions(Faction(id = 23, factionHeadId = 9), Faction(id = 6))
)
val heroes: Map[Int, HeroT] = Map(
17 -> HeroC(id = 17, pronounGender = Gender.Female, vigor = 90),
9 -> HeroC(id = 9, pronounGender = Gender.Female, factionId = Some(23)),
11 -> HeroC(id = 11, pronounGender = Gender.Female, factionId = Some(23))
)
val factions: Map[Int, FactionT] = Map(
23 -> FactionC(id = 23, factionHeadId = 9, name = "", leaderIds = Vector()),
6 -> FactionC(id = 6, factionHeadId = 0, name = "", leaderIds = Vector())
)
val gs = baseGameState.copy(provinces = provinces, heroes = heroes, factions = factions)
val results = PerformUnaffiliatedHeroesAction(
gameState = gs,
heroGenerator = null,
actionResultApplier = applier
).results(SeededRandom(gs.randomSeed))
gameState = GameStateConverter.fromProto(gs),
heroGenerator = null
).resultsOfExecute()
results
.flatMap(_.changedProvinces)
.collect { case cp: ChangedProvinceC => cp }
.flatMap(_.changedUnaffiliatedHeroes) shouldBe empty
}
"a prisoner in an unowned province" should "become a resident" in {
val provinces: Map[Int, ProvinceT] = Map(
8 -> ProvinceC(
id = 8,
rulingFactionId = None,
rulingFactionHeroIds = Vector(),
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 17,
unaffiliatedHeroType = UnaffiliatedHeroType.Prisoner,
lastFactionId = Some(6),
recruitmentInfo = RecruitmentInfo.Prisoner
)
)
)
)
val heroes: Map[Int, HeroT] = Map(
17 -> HeroC(id = 17, pronounGender = Gender.Female, vigor = 90),
9 -> HeroC(id = 9, pronounGender = Gender.Female, factionId = Some(23)),
11 -> HeroC(id = 11, pronounGender = Gender.Female, factionId = Some(23))
)
val factions: Map[Int, FactionT] = Map(
23 -> FactionC(id = 23, factionHeadId = 0, name = "", leaderIds = Vector()),
6 -> FactionC(id = 6, factionHeadId = 0, name = "", leaderIds = Vector())
)
val gs = baseGameState.copy(provinces = provinces, heroes = heroes, factions = factions)
val results = PerformUnaffiliatedHeroesAction(
gameState = gs,
heroGenerator = null,
actionResultApplier = applier
).results(SeededRandom(gs.randomSeed))
val changedUHs = results
.flatMap(_.changedProvinces)
.collect { case cp: ChangedProvinceC => cp }
.flatMap(_.changedUnaffiliatedHeroes)
changedUHs.find(_.heroId == 17) shouldBe defined
changedUHs.find(_.heroId == 17).get.unaffiliatedHeroType shouldBe UnaffiliatedHeroType.Resident
}
"a faction leader prisoner in an unowned province" should "become an outlaw" in {
val provinces: Map[Int, ProvinceT] = Map(
8 -> ProvinceC(
id = 8,
rulingFactionId = None,
rulingFactionHeroIds = Vector(),
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 17,
unaffiliatedHeroType = UnaffiliatedHeroType.Prisoner,
lastFactionId = Some(6),
recruitmentInfo = RecruitmentInfo.Prisoner
)
)
)
)
val heroes: Map[Int, HeroT] = Map(
17 -> HeroC(id = 17, pronounGender = Gender.Female, vigor = 90),
9 -> HeroC(id = 9, pronounGender = Gender.Female, factionId = Some(23)),
11 -> HeroC(id = 11, pronounGender = Gender.Female, factionId = Some(23))
)
val factions: Map[Int, FactionT] = Map(
23 -> FactionC(id = 23, factionHeadId = 0, name = "", leaderIds = Vector()),
6 -> FactionC(id = 6, factionHeadId = 0, name = "", leaderIds = Vector(17))
)
val gs = baseGameState.copy(provinces = provinces, heroes = heroes, factions = factions)
val results = PerformUnaffiliatedHeroesAction(
gameState = gs,
heroGenerator = null,
actionResultApplier = applier
).results(SeededRandom(gs.randomSeed))
val changedUHs = results
.flatMap(_.changedProvinces)
.collect { case cp: ChangedProvinceC => cp }
.flatMap(_.changedUnaffiliatedHeroes)
changedUHs.head.heroId shouldBe 17
changedUHs.head.unaffiliatedHeroType shouldBe UnaffiliatedHeroType.Outlaw
}
"multiple prisoners in different provinces" should "be batched into a single HERO_CHANGED result" in {
val provinces: Map[Int, ProvinceT] = Map(
4 -> ProvinceC(
id = 4,
rulingFactionId = None,
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 17,
unaffiliatedHeroType = UnaffiliatedHeroType.Prisoner,
recruitmentInfo = RecruitmentInfo.Prisoner
val gs = GameState(
currentPhase = UNAFFILIATED_HERO_ACTIONS,
currentDate = Some(Date(3, 273)),
provinces = mapifyProvinces(
Province(
id = 8,
rulingFactionId = None,
rulingFactionHeroIds = Vector(),
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_PRISONER,
lastFaction = Some(6),
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_PRISONER))
)
)
)
),
8 -> ProvinceC(
id = 8,
rulingFactionId = None,
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 19,
unaffiliatedHeroType = UnaffiliatedHeroType.Prisoner,
recruitmentInfo = RecruitmentInfo.Prisoner
)
heroes = mapifyHeroes(
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 17,
vigor = 90
),
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 9,
factionId = Some(23)
),
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 11,
factionId = Some(23)
)
),
factions = mapifyFactions(Faction(id = 23), Faction(id = 6))
)
val results = PerformUnaffiliatedHeroesAction(
gameState = GameStateConverter.fromProto(gs),
heroGenerator = null
).resultsOfExecute()
results
.flatMap(_.changedProvinces)
.flatMap(_.changedUnaffiliatedHeroes) should contain(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_RESIDENT,
lastFaction = Some(6),
recruitmentInfo = Some(
RecruitmentInfo(status = RECRUITMENT_STATUS_NO_RULER_IN_PROVINCE)
)
)
)
val heroes: Map[Int, HeroT] = Map(
17 -> HeroC(id = 17, vigor = 90, pronounGender = Gender.Female),
19 -> HeroC(id = 19, vigor = 90, pronounGender = Gender.Female)
}
"a faction leader prisoner in an unowned province" should "become an outlaw" in {
val gs = GameState(
currentPhase = UNAFFILIATED_HERO_ACTIONS,
currentDate = Some(Date(3, 273)),
provinces = mapifyProvinces(
Province(
id = 8,
rulingFactionId = None,
rulingFactionHeroIds = Vector(),
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_PRISONER,
lastFaction = Some(6),
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_PRISONER))
)
)
)
),
heroes = mapifyHeroes(
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 17,
vigor = 90
),
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 9,
factionId = Some(23)
),
Hero(
profession = NO_PROFESSION,
pronounGender = GENDER_FEMALE,
id = 11,
factionId = Some(23)
)
),
factions = mapifyFactions(Faction(id = 23), Faction(id = 6, leaders = Vector(17)))
)
val factions: Map[Int, FactionT] = Map(
6 -> FactionC(id = 6, factionHeadId = 0, name = "", leaderIds = Vector())
)
val gs = baseGameState.copy(provinces = provinces, heroes = heroes, factions = factions)
val results = PerformUnaffiliatedHeroesAction(
gameState = gs,
heroGenerator = null,
actionResultApplier = applier
).results(SeededRandom(gs.randomSeed))
gameState = GameStateConverter.fromProto(gs),
heroGenerator = null
).resultsOfExecute()
val heroChangedResults = results.filter(_.actionResultType == HeroChangedResultType)
results
.flatMap(_.changedProvinces)
.flatMap(_.changedUnaffiliatedHeroes)
.head should equalProto(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_OUTLAW,
lastFaction = Some(6),
recruitmentInfo = Some(
RecruitmentInfo(status = RECRUITMENT_STATUS_OUTLAW)
)
)
)
}
"multiple prisoners in different provinces" should "be batched into a single HERO_CHANGED result" in {
val gs = GameState(
currentPhase = UNAFFILIATED_HERO_ACTIONS,
currentDate = Some(Date(3, 273)),
provinces = mapifyProvinces(
Province(
id = 4,
rulingFactionId = None,
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_PRISONER,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_PRISONER))
)
)
),
Province(
id = 8,
rulingFactionId = None,
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 19,
`type` = UNAFFILIATED_HERO_PRISONER,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_PRISONER))
)
)
)
),
heroes = mapifyHeroes(
Hero(id = 17, vigor = 90, profession = NO_PROFESSION, pronounGender = GENDER_FEMALE),
Hero(id = 19, vigor = 90, profession = NO_PROFESSION, pronounGender = GENDER_FEMALE)
),
factions = mapifyFactions(Faction(id = 6))
)
val results = PerformUnaffiliatedHeroesAction(
gameState = GameStateConverter.fromProto(gs),
heroGenerator = null
).resultsOfExecute()
// Should have exactly one HERO_CHANGED result containing both status changes
val heroChangedResults = results.filter(_.`type` == HERO_CHANGED)
heroChangedResults should have size 1
val changedProvinces = heroChangedResults.head.changedProvinces.collect { case cp: ChangedProvinceC => cp }
val changedProvinces = heroChangedResults.head.changedProvinces
changedProvinces should have size 2
changedProvinces.map(_.provinceId) should contain allOf (4, 8)
changedProvinces.map(_.id) should contain allOf (4, 8)
// Both prisoners should become residents
changedProvinces
.flatMap(_.changedUnaffiliatedHeroes)
.map(_.heroId) should contain allOf (17, 19)
changedProvinces
.flatMap(_.changedUnaffiliatedHeroes)
.foreach(_.unaffiliatedHeroType shouldBe UnaffiliatedHeroType.Resident)
.foreach(_.`type` shouldBe UNAFFILIATED_HERO_RESIDENT)
}
"a resident" should "become a traveler when random chance triggers" in {
ResidentToTravelerChance.setDoubleValue(1.0)
ResidentToTravelerChance.setDoubleValue(1.0) // Always trigger
val provinces: Map[Int, ProvinceT] = Map(
4 -> ProvinceC(
id = 4,
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 17,
unaffiliatedHeroType = UnaffiliatedHeroType.Resident,
recruitmentInfo = RecruitmentInfo.NoRulerInProvince
val gs = GameState(
currentPhase = UNAFFILIATED_HERO_ACTIONS,
currentDate = Some(Date(3, 273)),
provinces = mapifyProvinces(
Province(
id = 4,
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_RESIDENT,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_NO_RULER_IN_PROVINCE))
)
)
)
),
heroes = mapifyHeroes(
Hero(id = 17, vigor = 90, profession = NO_PROFESSION, pronounGender = GENDER_FEMALE)
)
)
val heroes: Map[Int, HeroT] = Map(
17 -> HeroC(id = 17, vigor = 90, pronounGender = Gender.Female)
)
val gs = baseGameState.copy(provinces = provinces, heroes = heroes)
val results = PerformUnaffiliatedHeroesAction(
gameState = gs,
heroGenerator = null,
actionResultApplier = applier
).results(SeededRandom(gs.randomSeed))
gameState = GameStateConverter.fromProto(gs),
heroGenerator = null
).resultsOfExecute()
val changedUHs = results
results
.flatMap(_.changedProvinces)
.collect { case cp: ChangedProvinceC => cp }
.flatMap(_.changedUnaffiliatedHeroes)
changedUHs.find(_.heroId == 17) shouldBe defined
changedUHs.find(_.heroId == 17).get.unaffiliatedHeroType shouldBe UnaffiliatedHeroType.Traveler
.flatMap(_.changedUnaffiliatedHeroes) should contain(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_TRAVELER,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_TRAVELER))
)
)
}
"a traveler" should "become a resident when random chance triggers" in {
TravelerMoveChance.setDoubleValue(0)
TravelerToResidentChance.setDoubleValue(1.0)
TravelerMoveChance.setDoubleValue(0) // Don't move
TravelerToResidentChance.setDoubleValue(1.0) // Always become resident
val provinces: Map[Int, ProvinceT] = Map(
4 -> ProvinceC(
id = 4,
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 17,
unaffiliatedHeroType = UnaffiliatedHeroType.Traveler,
recruitmentInfo = RecruitmentInfo.Traveler
val gs = GameState(
currentPhase = UNAFFILIATED_HERO_ACTIONS,
currentDate = Some(Date(3, 273)),
provinces = mapifyProvinces(
Province(
id = 4,
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_TRAVELER,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_TRAVELER))
)
)
)
),
heroes = mapifyHeroes(
Hero(id = 17, vigor = 90, profession = NO_PROFESSION, pronounGender = GENDER_FEMALE)
)
)
val heroes: Map[Int, HeroT] = Map(
17 -> HeroC(id = 17, vigor = 90, pronounGender = Gender.Female)
)
val gs = baseGameState.copy(provinces = provinces, heroes = heroes)
val results = PerformUnaffiliatedHeroesAction(
gameState = gs,
heroGenerator = null,
actionResultApplier = applier
).results(SeededRandom(gs.randomSeed))
gameState = GameStateConverter.fromProto(gs),
heroGenerator = null
).resultsOfExecute()
val changedHeroes = results
.flatMap(_.changedProvinces)
.collect { case cp: ChangedProvinceC => cp }
.flatMap(_.changedUnaffiliatedHeroes)
changedHeroes should have size 1
changedHeroes.head.heroId shouldBe 17
changedHeroes.head.unaffiliatedHeroType shouldBe UnaffiliatedHeroType.Resident
changedHeroes.head.`type` shouldBe UNAFFILIATED_HERO_RESIDENT
}
"multiple heroes in the same province" should "be batched into a single HERO_CHANGED result" in {
val provinces: Map[Int, ProvinceT] = Map(
4 -> ProvinceC(
id = 4,
rulingFactionId = None,
unaffiliatedHeroes = Vector(
UnaffiliatedHeroC(
heroId = 17,
unaffiliatedHeroType = UnaffiliatedHeroType.Prisoner,
recruitmentInfo = RecruitmentInfo.Prisoner
),
UnaffiliatedHeroC(
heroId = 19,
unaffiliatedHeroType = UnaffiliatedHeroType.Prisoner,
recruitmentInfo = RecruitmentInfo.Prisoner
val gs = GameState(
currentPhase = UNAFFILIATED_HERO_ACTIONS,
currentDate = Some(Date(3, 273)),
provinces = mapifyProvinces(
Province(
id = 4,
rulingFactionId = None,
unaffiliatedHeroes = Vector(
UnaffiliatedHero(
heroId = 17,
`type` = UNAFFILIATED_HERO_PRISONER,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_PRISONER))
),
UnaffiliatedHero(
heroId = 19,
`type` = UNAFFILIATED_HERO_PRISONER,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_PRISONER))
)
)
)
)
),
heroes = mapifyHeroes(
Hero(id = 17, vigor = 90, profession = NO_PROFESSION, pronounGender = GENDER_FEMALE),
Hero(id = 19, vigor = 90, profession = NO_PROFESSION, pronounGender = GENDER_FEMALE)
),
factions = mapifyFactions(Faction(id = 6))
)
val heroes: Map[Int, HeroT] = Map(
17 -> HeroC(id = 17, vigor = 90, pronounGender = Gender.Female),
19 -> HeroC(id = 19, vigor = 90, pronounGender = Gender.Female)
)
val factions: Map[Int, FactionT] = Map(
6 -> FactionC(id = 6, factionHeadId = 0, name = "", leaderIds = Vector())
)
val gs = baseGameState.copy(provinces = provinces, heroes = heroes, factions = factions)
val results = PerformUnaffiliatedHeroesAction(
gameState = gs,
heroGenerator = null,
actionResultApplier = applier
).results(SeededRandom(gs.randomSeed))
gameState = GameStateConverter.fromProto(gs),
heroGenerator = null
).resultsOfExecute()
val heroChangedResults = results.filter(_.actionResultType == HeroChangedResultType)
// Should have exactly one HERO_CHANGED result
val heroChangedResults = results.filter(_.`type` == HERO_CHANGED)
heroChangedResults should have size 1
val changedProvinces = heroChangedResults.head.changedProvinces.collect { case cp: ChangedProvinceC => cp }
// Should have one changedProvince entry with both heroes
val changedProvinces = heroChangedResults.head.changedProvinces
changedProvinces should have size 1
changedProvinces.head.provinceId shouldBe 4
changedProvinces.head.id shouldBe 4
val changedHeroes = changedProvinces.head.changedUnaffiliatedHeroes
changedHeroes should have size 2
@@ -18,15 +18,22 @@ import net.eagle0.eagle.model.state.{
Supplies
}
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.faction.FactionRelationship
import net.eagle0.eagle.model.state.faction.FactionRelationship.RelationshipLevel
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.{Gender, Profession}
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.state.province.ProvinceOrderType
import net.eagle0.eagle.model.state.province.ProvinceOrderType.Entrust
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.run_status.RunStatus
import net.eagle0.eagle.model.state.RoundPhase
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.Inside.inside
@@ -34,7 +41,39 @@ import org.scalatest.Inspectors.forExactly
import org.scalatest.LoneElement.convertToCollectionLoneElementWrapper
class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
private val attackerId = 1
private val attackerId = 1
private val gameId = 12345L
private val currentRoundId = 9
private val currentDate = Date(year = 1, month = Date.Month.January)
private def makeGameState(
provinces: Map[Int, ProvinceT],
factions: Map[Int, FactionT],
heroes: Map[Int, HeroT] = this.heroes,
battalions: Map[Int, BattalionT] = this.battalions
): GameState = GameState(
gameId = gameId,
currentRoundId = currentRoundId,
currentPhase = RoundPhase.UncontestedConquest,
currentDate = Some(currentDate),
actionResultCount = 0,
provinces = provinces,
heroes = heroes,
battalions = battalions,
destroyedBattalions = Map.empty,
factions = factions,
factionCommandCounts = Map.empty,
killedHeroes = Map.empty,
destroyedFactions = Map.empty,
outstandingBattles = Vector.empty,
battleCounter = 0,
deferredNotifications = Vector.empty,
runStatus = RunStatus.Running,
victor = None,
battalionTypes = Vector.empty,
randomSeed = 12345L,
chronicleEntries = Vector.empty
)
private val heroes: Map[Int, HeroC] = Map(
92 -> HeroC(
@@ -117,13 +156,7 @@ class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
)
val results = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
makeGameState(provinces, factions)
).results
results.loneElement.actionResultType.shouldBe(
@@ -176,13 +209,7 @@ class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
)
val results = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
makeGameState(provinces, factions)
).results
results.size.shouldBe(3)
@@ -240,13 +267,7 @@ class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
)
val results = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
makeGameState(provinces, factions)
).results
inside(results.head.changedProvinces.head) {
@@ -312,13 +333,7 @@ class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
)
val results = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
makeGameState(provinces, factions)
).results
results.size.shouldBe(3)
@@ -414,13 +429,7 @@ class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
val ex = the[EagleInternalException] thrownBy
PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
makeGameState(provinces, factions)
).results
ex.getMessage.shouldBe(
@@ -511,13 +520,7 @@ class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
)
val result = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
makeGameState(provinces, factions)
).results.head
result.actionResultType.shouldBe(WithdrawalForTruceResultType)
@@ -19,8 +19,8 @@ import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.{Neighbor, Province}
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.command.TCommandFactory
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
import net.eagle0.eagle.library.actions.impl.command.CommandFactory
import net.eagle0.eagle.library.settings.{
ActionVigorCost,
DesiredLoyaltyOverThreshold,
@@ -61,8 +61,8 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
private val functionalRandom = SeededRandom(new Random().nextLong())
private val commandFactory: TCommandFactory = mock[TCommandFactory]
private val actionResultApplier: ActionResultApplier = mock[ActionResultApplier]
private val commandFactory: CommandFactory = mock[CommandFactory]
private val applier: ActionResultTApplier = mock[ActionResultTApplier]
private val actingFactionId = 19
@@ -339,10 +339,9 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
val developingProvince = actingProvince.withProvinceOrders(DEVELOP)
val developingGS =
gameState.update(_.provinces(developingProvince.id) := developingProvince)
val scalaGs = GameStateConverter.fromProto(developingGS)
val selection = PerformVassalCommandsPhaseAction(
gameState = scalaGs,
gameState = GameStateConverter.fromProto(developingGS),
commandsForProvince = Map(
developingProvince.id -> Some(
OneProvinceAvailableCommands(
@@ -358,8 +357,8 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
)
),
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).chooseCommand(scalaGs, developingProvince.id, functionalRandom).newValue.get
applier = applier
).chooseCommand(developingProvince.id, functionalRandom).newValue.get
inside(selection.available) {
case FeastAvailableCommand(
@@ -382,10 +381,9 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
_.heroes(4).loyalty := 70,
_.heroes(5).loyalty := 70
)
val scalaGs = GameStateConverter.fromProto(developingGS)
val selection = PerformVassalCommandsPhaseAction(
gameState = scalaGs,
gameState = GameStateConverter.fromProto(developingGS),
commandsForProvince = Map(
developingProvince.id -> Some(
OneProvinceAvailableCommands(
@@ -401,8 +399,8 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
)
),
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).chooseCommand(scalaGs, developingProvince.id, functionalRandom).newValue.get
applier = applier
).chooseCommand(developingProvince.id, functionalRandom).newValue.get
inside(selection) {
case CommandSelection(
@@ -436,10 +434,9 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
_.heroes(4).loyalty := 70,
_.heroes(5).loyalty := 70
)
val scalaGs = GameStateConverter.fromProto(developingGS)
val selection = PerformVassalCommandsPhaseAction(
gameState = scalaGs,
gameState = GameStateConverter.fromProto(developingGS),
commandsForProvince = Map(
developingProvince.id -> Some(
OneProvinceAvailableCommands(
@@ -455,8 +452,8 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
)
),
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).chooseCommand(scalaGs, developingProvince.id, functionalRandom).newValue.get
applier = applier
).chooseCommand(developingProvince.id, functionalRandom).newValue.get
selection.available should not matchPattern {
case _: HeroGiftAvailableCommand =>
@@ -491,10 +488,9 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
val developingProvince = actingProvince.withProvinceOrders(DEVELOP)
val developingGS =
gameState.update(_.provinces(developingProvince.id) := developingProvince)
val scalaGs = GameStateConverter.fromProto(developingGS)
val selection = PerformVassalCommandsPhaseAction(
gameState = scalaGs,
gameState = GameStateConverter.fromProto(developingGS),
commandsForProvince = Map(
developingProvince.id -> Some(
OneProvinceAvailableCommands(
@@ -504,8 +500,8 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
)
),
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).chooseCommand(scalaGs, developingProvince.id, functionalRandom).newValue.get
applier = applier
).chooseCommand(developingProvince.id, functionalRandom).newValue.get
selection.available shouldBe improveCommand
}
@@ -514,10 +510,9 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
val developingProvince = actingProvince.withProvinceOrders(DEVELOP)
val developingGS =
gameState.update(_.provinces(developingProvince.id) := developingProvince)
val scalaGs = GameStateConverter.fromProto(developingGS)
val selection = PerformVassalCommandsPhaseAction(
gameState = scalaGs,
gameState = GameStateConverter.fromProto(developingGS),
commandsForProvince = Map(
developingProvince.id -> Some(
OneProvinceAvailableCommands(
@@ -527,8 +522,8 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
)
),
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).chooseCommand(scalaGs, developingProvince.id, functionalRandom).newValue.get
applier = applier
).chooseCommand(developingProvince.id, functionalRandom).newValue.get
selection.available shouldBe restCommand
}
@@ -538,7 +533,6 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
val marchingGS = marchGameState.update(
_.provinces(marchingProvince.id) := marchingProvince
)
val scalaGs = GameStateConverter.fromProto(marchingGS)
val commandsMap = Map(
marchingProvince.id -> Some(
@@ -554,11 +548,11 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
)
val selection = PerformVassalCommandsPhaseAction(
gameState = scalaGs,
gameState = GameStateConverter.fromProto(marchingGS),
commandsForProvince = commandsMap,
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).chooseCommand(scalaGs, actingProvince.id, functionalRandom).newValue.get
applier = applier
).chooseCommand(actingProvince.id, functionalRandom).newValue.get
inside(selection.available) {
case mac: MarchAvailableCommand =>
@@ -571,7 +565,6 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
val marchingGS = marchGameState.update(
_.provinces(marchingProvince.id) := marchingProvince
)
val scalaGs = GameStateConverter.fromProto(marchingGS)
val commandsMap = Map(
marchingProvince.id -> Some(
@@ -583,11 +576,11 @@ class PerformVassalCommandsPhaseActionTest extends AnyFlatSpec with Matchers wit
)
val selection = PerformVassalCommandsPhaseAction(
gameState = scalaGs,
gameState = GameStateConverter.fromProto(marchingGS),
commandsForProvince = commandsMap,
commandFactory = commandFactory,
actionResultApplier = actionResultApplier
).chooseCommand(scalaGs, actingProvince.id, functionalRandom).newValue.get
applier = applier
).chooseCommand(actingProvince.id, functionalRandom).newValue.get
selection.available shouldBe improveCommand
}
@@ -5,15 +5,22 @@ import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.state.*
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.hero.Gender.{Female, Male}
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.hero.Profession.NoProfession
import net.eagle0.eagle.model.state.province.{Neighbor, ProvinceOrderType}
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.run_status.RunStatus
import net.eagle0.eagle.model.state.shardok_battle.{BattleType, VictoryCondition}
import net.eagle0.eagle.model.state.HostileArmyGroupStatus.{Attacking, AwaitingDecision}
import net.eagle0.eagle.model.state.RoundPhase
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.Inside.inside
@@ -23,6 +30,40 @@ class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
private val attackerId = 1
private val defenderId = 2
private def makeGameState(
gameId: GameId,
currentRoundId: RoundId,
currentDate: Date,
battleCounter: Int,
provinces: Map[ProvinceId, ProvinceT],
factions: Map[FactionId, FactionT],
heroes: Map[HeroId, HeroT] = this.heroes,
battalions: Map[BattalionId, BattalionT] = this.battalions,
battalionTypes: Map[BattalionTypeId, BattalionType] = this.battalionTypes
): GameState = GameState(
gameId = gameId,
currentRoundId = currentRoundId,
currentPhase = RoundPhase.BattleRequest,
currentDate = Some(currentDate),
actionResultCount = 0,
provinces = provinces,
heroes = heroes,
battalions = battalions,
destroyedBattalions = Map.empty,
factions = factions,
factionCommandCounts = Map.empty,
killedHeroes = Map.empty,
destroyedFactions = Map.empty,
outstandingBattles = Vector.empty,
battleCounter = battleCounter,
deferredNotifications = Vector.empty,
runStatus = RunStatus.Running,
victor = None,
battalionTypes = battalionTypes.values.toVector,
randomSeed = 12345L,
chronicleEntries = Vector.empty
)
private val heroes = Map[HeroId, HeroC](
92 -> HeroC(
id = 92,
@@ -137,15 +178,14 @@ class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
)
val action = RequestBattlesAction(
gameId = 0xcafe,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0x17,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
battalionTypes = battalionTypes
makeGameState(
gameId = 0xcafe,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0x17,
provinces = provinces,
factions = factions
)
)
val results = action.results
@@ -226,15 +266,14 @@ class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
)
val results = RequestBattlesAction(
gameId = 0x123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
battalionTypes = battalionTypes
makeGameState(
gameId = 0x123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
provinces = provinces,
factions = factions
)
).results
results.size shouldBe 2
@@ -283,15 +322,14 @@ class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
val ex = the[EagleInternalException] thrownBy
RequestBattlesAction(
gameId = 0x123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
battalionTypes = battalionTypes
makeGameState(
gameId = 0x123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
provinces = provinces,
factions = factions
)
).results
ex.getMessage shouldBe "requirement failed: all armies must be attacking in the battle request phase"
@@ -330,15 +368,14 @@ class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
)
val results = RequestBattlesAction(
gameId = 0x123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
battalionTypes = battalionTypes
makeGameState(
gameId = 0x123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
provinces = provinces,
factions = factions
)
).results
inside(results.head.changedProvinces.head) {
@@ -402,15 +439,14 @@ class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
)
val results = RequestBattlesAction(
gameId = 0xabc123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0xa5,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
battalionTypes = battalionTypes
makeGameState(
gameId = 0xabc123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0xa5,
provinces = provinces,
factions = factions
)
).results
results should have size 1
@@ -473,15 +509,14 @@ class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
)
val action = RequestBattlesAction(
gameId = 0x123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
battalionTypes = battalionTypes
makeGameState(
gameId = 0x123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
provinces = provinces,
factions = factions
)
)
val results = action.results
@@ -1,26 +1,30 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.library.actions.applier.ActionResultApplierImpl
import net.eagle0.eagle.model.action_result.changed_province.concrete.{ChangedProvinceC, HostileArmyStatusChange}
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.{EndTruceTurnBackPhaseResultType, WithdrawalForTruceResultType}
import net.eagle0.eagle.model.state.{Army, HostileArmyGroup, HostileArmyGroupStatus, MovingArmy, Supplies}
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.faction.FactionRelationship
import net.eagle0.eagle.model.state.faction.FactionRelationship.RelationshipLevel
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.state.run_status.RunStatus
import net.eagle0.eagle.model.state.RoundPhase
import net.eagle0.common.ProtoMatchers.equalProto
import net.eagle0.eagle.common.action_result_type.ActionResultType.{END_TRUCE_TURN_BACK_PHASE, WITHDRAWAL_FOR_TRUCE}
import net.eagle0.eagle.common.round_phase.{NewRoundPhase, RoundPhase}
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.army.*
import net.eagle0.eagle.internal.changed_province.ChangedProvince
import net.eagle0.eagle.internal.changed_province.ChangedProvince.HostileArmyGroupStatusChange
import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship.RelationshipLevel.TRUCE
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplierImpl
import net.eagle0.eagle.library.util.validations.TestingNoopValidator
import net.eagle0.eagle.library.util.IDable.{mapifyFactions, mapifyProvinces}
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.FactionId
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class TruceTurnBackPhaseActionTest extends AnyFlatSpec with Matchers {
private val actionResultApplier = ActionResultApplierImpl(None)
private val actionResultProtoApplier = new ActionResultProtoApplierImpl(
validator = TestingNoopValidator
)
private val attackingFid: FactionId = 9
private val defendingFid: FactionId = 23
@@ -28,118 +32,103 @@ class TruceTurnBackPhaseActionTest extends AnyFlatSpec with Matchers {
private val destinationPid = 17
private val fleePid = 98
private val fleeProvince = ProvinceC(
id = fleePid,
rulingFactionId = Some(attackingFid)
)
private val fleeProvince =
Province(id = fleePid, rulingFactionId = Some(attackingFid))
private val destinationProvince = ProvinceC(
id = destinationPid,
rulingFactionId = Some(defendingFid),
hostileArmies = Vector(
HostileArmyGroup(
status = HostileArmyGroupStatus.Attacking,
factionId = attackingFid,
armies = Vector(
MovingArmy(
army = Army(
factionId = attackingFid,
units = Vector(),
fleeProvinceId = Some(fleePid)
),
arrivalRound = 155,
destinationProvinceId = destinationPid,
originProvinceId = 3,
supplies = Supplies(gold = 0, food = 0),
suppliesLoss = 0,
id = 991,
startingPositionIndex = Some(5)
private val destinationProvince =
Province(
id = destinationPid,
rulingFactionId = Some(defendingFid),
hostileArmies = Vector(
HostileArmyGroup(
status = Attacking(),
factionId = attackingFid,
armies = Vector(
MovingArmy(
army = Some(
Army(
factionId = attackingFid,
units = Vector(),
fleeProvinceId = Some(fleePid)
)
),
arrivalRound = 155,
destinationProvince = destinationPid,
originProvince = 3,
supplies = None,
suppliesLoss = 0,
id = 991,
startingPositionIndex = Some(5)
)
)
)
)
)
private val gameState = GameState(
currentDate = Some(net.eagle0.eagle.common.date.Date(year = 400, month = 3)),
currentRoundId = 155,
currentPhase = RoundPhase.TRUCE_TURN_BACK,
provinces = mapifyProvinces(destinationProvince, fleeProvince),
factions = mapifyFactions(
Faction(
id = attackingFid,
factionRelationships = Vector(
FactionRelationship(
targetFactionId = defendingFid,
relationshipLevel = TRUCE
)
)
),
Faction(
id = defendingFid,
factionRelationships = Vector(
FactionRelationship(
targetFactionId = attackingFid,
relationshipLevel = TRUCE
)
)
)
)
)
private val attackingFaction = FactionC(
id = attackingFid,
factionHeadId = 1,
name = "Attacking Faction",
leaderIds = Vector(),
factionRelationships = Vector(
FactionRelationship(
targetFactionId = defendingFid,
relationshipLevel = RelationshipLevel.Truce
)
"execute" should "prove to the next phase" in {
val results = TruceTurnBackPhaseAction(GameStateConverter.fromProto(gameState)).execute(
actionResultProtoApplier
)
)
private val defendingFaction = FactionC(
id = defendingFid,
factionHeadId = 2,
name = "Defending Faction",
leaderIds = Vector(),
factionRelationships = Vector(
FactionRelationship(
targetFactionId = attackingFid,
relationshipLevel = RelationshipLevel.Truce
exactly(1, results.map(_.actionResult)).should(
equalProto(
ActionResult(
`type` = END_TRUCE_TURN_BACK_PHASE,
newRoundPhase = Some(NewRoundPhase(RoundPhase.BATTLE_REQUEST))
)
)
)
)
private val gameState = GameState(
gameId = 1,
currentDate = Some(Date(year = 400, month = Date.Month.March)),
currentRoundId = 155,
currentPhase = RoundPhase.TruceTurnBack,
actionResultCount = 0,
provinces = Map(
destinationPid -> destinationProvince,
fleePid -> fleeProvince
),
heroes = Map.empty,
battalions = Map.empty,
destroyedBattalions = Map.empty,
factions = Map(
attackingFid -> attackingFaction,
defendingFid -> defendingFaction
),
factionCommandCounts = Map.empty,
killedHeroes = Map.empty,
destroyedFactions = Map.empty,
outstandingBattles = Vector.empty,
battleCounter = 0,
deferredNotifications = Vector.empty,
runStatus = RunStatus.Running,
victor = None,
battalionTypes = Vector.empty,
randomSeed = 1L,
chronicleEntries = Vector.empty
)
"results" should "advance to the next phase" in {
val results = TruceTurnBackPhaseAction(gameState, actionResultApplier)
.results(SeededRandom(gameState.randomSeed))
results.last shouldBe ActionResultC(
actionResultType = EndTruceTurnBackPhaseResultType,
newRoundPhase = Some(RoundPhase.BattleRequest)
)
}
it should "turn back an army with a truce" in {
val results = TruceTurnBackPhaseAction(gameState, actionResultApplier)
.results(SeededRandom(gameState.randomSeed))
val results = TruceTurnBackPhaseAction(GameStateConverter.fromProto(gameState)).execute(
actionResultProtoApplier
)
results.head shouldBe ActionResultC(
actionResultType = WithdrawalForTruceResultType,
actingFactionId = Some(attackingFid),
provinceId = Some(destinationPid),
changedProvinces = Vector(
ChangedProvinceC(
provinceId = destinationPid,
clearDefendingArmy = true,
hostileArmyStatusChanges = Vector(
HostileArmyStatusChange(attackingFid, HostileArmyGroupStatus.Withdrawing)
results.head.actionResult.should(
equalProto(
ActionResult(
`type` = WITHDRAWAL_FOR_TRUCE,
player = Some(attackingFid),
province = Some(destinationPid),
changedProvinces = Vector(
ChangedProvince(
id = destinationPid,
clearDefendingArmy = true,
hostileArmyStatusChanges = Vector(
HostileArmyGroupStatusChange(
factionId = attackingFid,
newStatus = Withdrawing()
)
)
)
)
)
)
@@ -147,23 +136,23 @@ class TruceTurnBackPhaseActionTest extends AnyFlatSpec with Matchers {
}
it should "just end the phase if there's no truce" in {
val attackingFactionWithoutTruce = attackingFaction.copy(factionRelationships = Vector())
val defendingFactionWithoutTruce = defendingFaction.copy(factionRelationships = Vector())
val gsWithoutTruce = gameState.copy(
factions = Map(
attackingFid -> attackingFactionWithoutTruce,
defendingFid -> defendingFactionWithoutTruce
)
val gsWithoutTruce = gameState.update(
_.factions(attackingFid).factionRelationships := Vector(),
_.factions(defendingFid).factionRelationships := Vector()
)
val results = TruceTurnBackPhaseAction(gsWithoutTruce, actionResultApplier)
.results(SeededRandom(gsWithoutTruce.randomSeed))
val results =
TruceTurnBackPhaseAction(GameStateConverter.fromProto(gsWithoutTruce)).execute(
actionResultProtoApplier
)
results.size shouldBe 1
results.head shouldBe ActionResultC(
actionResultType = EndTruceTurnBackPhaseResultType,
newRoundPhase = Some(RoundPhase.BattleRequest)
exactly(1, results.map(_.actionResult)).should(
equalProto(
ActionResult(
`type` = END_TRUCE_TURN_BACK_PHASE,
newRoundPhase = Some(NewRoundPhase(RoundPhase.BATTLE_REQUEST))
)
)
)
}
}
@@ -13,13 +13,11 @@ import net.eagle0.eagle.library.settings.{
TurnsToResolveConquest
}
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.hero.Profession.NoProfession
import net.eagle0.eagle.model.state.province.{BlizzardEvent, ImminentRiotEvent}
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.state.BattalionTypeId.{
HeavyCavalry,
@@ -255,33 +253,4 @@ class ProvinceUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
HeavyCavalry
)
}
"hasImminentRiot" should "return true when province has imminent riot event" in {
val province = ProvinceC(
id = 17,
activeEvents = Vector(ImminentRiotEvent())
)
ProvinceUtils.hasImminentRiot(province) shouldBe true
}
it should "return false when province has no imminent riot event" in {
val province = ProvinceC(id = 17, activeEvents = Vector.empty)
ProvinceUtils.hasImminentRiot(province) shouldBe false
}
it should "return false when province has other events but no imminent riot" in {
val province = ProvinceC(
id = 17,
activeEvents = Vector(
BlizzardEvent(
startDate = Date(400, Date.Month.January),
endDate = Date(400, Date.Month.March)
)
)
)
ProvinceUtils.hasImminentRiot(province) shouldBe false
}
}
@@ -215,8 +215,7 @@ class HumanPlayerClientConnectionStateTest
availableCommands = Some(availableCommandsAfter),
filteredResults = Vector(),
unfilteredCountAfter = 4,
knownShardokResults = Vector(),
serverGameStatus = None
knownShardokResults = Vector()
)
mockObserver.onNext
@@ -37,11 +37,6 @@ class GameControllerTest extends AnyFlatSpec with MockFactory with Matchers with
)
.anyNumberOfTimes()
(() => mockClientTextStore.incompleteTexts)
.expects()
.returning(Map.empty)
.anyNumberOfTimes()
"gameId" should "return the gameId of the engine" in {
(() => mockEngine.gameId)
.expects()