mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 02:15:43 +00:00
Compare commits
103
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a56567449a | ||
|
|
e105461692 | ||
|
|
2e03352dea | ||
|
|
8c19a93f3c | ||
|
|
9b2bce6537 | ||
|
|
ee4914dcc8 | ||
|
|
1e019c533a | ||
|
|
3d092e580f | ||
|
|
85e530c5a4 | ||
|
|
486960a6aa | ||
|
|
1cb2dd7b6a | ||
|
|
5483c732cc | ||
|
|
6402a8c283 | ||
|
|
eb762e1bae | ||
|
|
be31464e99 | ||
|
|
e6f9d4e4ac | ||
|
|
4d3b2ddb36 | ||
|
|
6edb4de0dc | ||
|
|
72a0f84105 | ||
|
|
f73798ae6e | ||
|
|
19691682e0 | ||
|
|
0e51bece68 | ||
|
|
a89f740b3b | ||
|
|
7959da0a5a | ||
|
|
bc119b2aab | ||
|
|
25c7788254 | ||
|
|
1246f8bcf6 | ||
|
|
f3e44fb9cf | ||
|
|
50aa61b77c | ||
|
|
facfcf9ac9 | ||
|
|
4c21368f96 | ||
|
|
7eccd69a01 | ||
|
|
8e2575be50 | ||
|
|
913d927902 | ||
|
|
066381e24e | ||
|
|
ced52b0195 | ||
|
|
262ba36436 | ||
|
|
d16aa63c00 | ||
|
|
636bf8f9f3 | ||
|
|
b84df05953 | ||
|
|
dcf0261ac3 | ||
|
|
7afe4e788a | ||
|
|
2e4fc0d230 | ||
|
|
a5b608d18a | ||
|
|
e6519fef20 | ||
|
|
94d49e61d7 | ||
|
|
f3e2873f34 | ||
|
|
c74ddb8983 | ||
|
|
c05e5f7f37 | ||
|
|
9d3967c58d | ||
|
|
07f27ea0ff | ||
|
|
b73d834fab | ||
|
|
deecd5a9ca | ||
|
|
314ff83d24 | ||
|
|
9adfd84498 | ||
|
|
63901e24e5 | ||
|
|
a4a128fe34 | ||
|
|
9cee497886 | ||
|
|
42294da2f6 | ||
|
|
75a129fb4c | ||
|
|
71a1858168 | ||
|
|
52f0cbe180 | ||
|
|
31dc53cc9e | ||
|
|
2a0654f884 | ||
|
|
1dd6eabc15 | ||
|
|
fcdc7d80b8 | ||
|
|
54db688c4e | ||
|
|
792c4f2b53 | ||
|
|
5aad32f5d9 | ||
|
|
78a833c086 | ||
|
|
f7c382446e | ||
|
|
a380eca47e | ||
|
|
90f239c696 | ||
|
|
be393a4cdd | ||
|
|
9e97f71bb9 | ||
|
|
113d54b936 | ||
|
|
5d6c2fef90 | ||
|
|
5e2e7a454c | ||
|
|
914141aed1 | ||
|
|
2ae235e933 | ||
|
|
14d83def79 | ||
|
|
170e998324 | ||
|
|
0dcdac1719 | ||
|
|
164933dbdd | ||
|
|
bf4db493ab | ||
|
|
89cabe9d17 | ||
|
|
dde7a58b44 | ||
|
|
486e99a02d | ||
|
|
2982927200 | ||
|
|
0551453536 | ||
|
|
ce357c612e | ||
|
|
f9e69b6f75 | ||
|
|
f43e914720 | ||
|
|
6c50c0da24 | ||
|
|
d265b76607 | ||
|
|
09a51e4280 | ||
|
|
5593effe69 | ||
|
|
44c268de93 | ||
|
|
0a40acb84d | ||
|
|
9603b497d2 | ||
|
|
0551dd0f13 | ||
|
|
45c4cf783d | ||
|
|
72c52e0b0d |
@@ -29,6 +29,9 @@ 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
|
||||
|
||||
@@ -216,6 +216,31 @@ 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:
|
||||
@@ -254,6 +279,32 @@ done
|
||||
- **Always test performance changes** - what seems like an optimization may sometimes have unexpected overhead or
|
||||
behavior changes.
|
||||
|
||||
## Troubleshooting Scala Build Errors
|
||||
|
||||
### MissingType Errors
|
||||
|
||||
When you see errors like:
|
||||
```
|
||||
dotty.tools.dotc.core.MissingType: Cannot resolve reference to type net.eagle0.eagle.internal.game_state.type.GameState
|
||||
```
|
||||
|
||||
**This is NOT a Scala compiler crash.** This is a missing dependency in BUILD.bazel.
|
||||
|
||||
**How to fix:**
|
||||
1. Identify the missing type from the error message (e.g., `game_state.GameState`)
|
||||
2. Find the Bazel target that provides this type (e.g., `//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto`)
|
||||
3. Add it to the `deps` of the failing target
|
||||
4. If the type appears in a public method signature, also add it to `exports` so downstream targets can see it
|
||||
|
||||
**Common pattern:** When adding a method to a class that takes or returns a proto type, the proto dependency often needs to be added to both `deps` AND `exports`.
|
||||
|
||||
### Bazel Clean
|
||||
|
||||
**NEVER run `bazel clean` without asking first.** It rarely fixes actual issues and wastes significant rebuild time. The issues that seem like they need `bazel clean` are usually:
|
||||
- Missing imports in Scala code
|
||||
- Missing dependencies in BUILD.bazel
|
||||
- Missing exports for types used in public signatures
|
||||
|
||||
## Game Content
|
||||
|
||||
**Maps:** `.e0mj` files in `/src/main/resources/net/eagle0/shardok/maps/`
|
||||
|
||||
@@ -161,6 +161,10 @@ maven.install(
|
||||
# Other
|
||||
"org.reactivestreams:reactive-streams:1.0.4",
|
||||
"javax.xml.bind:jaxb-api:2.3.1",
|
||||
|
||||
# OkHttp (for SSE with read timeout support)
|
||||
"com.squareup.okhttp3:okhttp:4.12.0",
|
||||
"com.squareup.okhttp3:okhttp-sse:4.12.0",
|
||||
],
|
||||
duplicate_version_warning = "error",
|
||||
fail_if_repin_required = True,
|
||||
|
||||
+224
-639
@@ -9,9 +9,9 @@
|
||||
│ GRPC BOUNDARY │
|
||||
│ EagleServiceImpl.scala ←→ Proto Messages ←→ Unity Client │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
GameStateConverter
|
||||
↓
|
||||
↓
|
||||
GameStateConverter
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ SCALA ENGINE │
|
||||
│ │
|
||||
@@ -22,9 +22,9 @@
|
||||
│ │
|
||||
│ HeroC, FactionC, ProvinceC, BattalionC, ArmyC, etc. │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
GameStateConverter
|
||||
↓
|
||||
↓
|
||||
GameStateConverter
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ PERSISTENCE BOUNDARY │
|
||||
│ GameHistory.scala ←→ Proto Messages ←→ File/Database │
|
||||
@@ -35,561 +35,269 @@
|
||||
|
||||
## Current State
|
||||
|
||||
### What's Done ✅
|
||||
### Completed Phases
|
||||
|
||||
| Component | Status | Details |
|
||||
|-----------|--------|---------|
|
||||
| Commands | **100% Complete** | All 41 commands use Scala models only |
|
||||
| Scala Models | **43 models** | GameState, Hero, Faction, Province, Battalion, Army, Supplies, Date, RoundPhase, etc. |
|
||||
| Converters | **43 converters** | Bidirectional toProto/fromProto for all models including GameState |
|
||||
| Protoless Base Classes | **Available** | `ProtolessSimpleAction`, `ProtolessSequentialResultsAction`, `ProtolessRandomSimpleAction` |
|
||||
| **GameState** | **Complete** | `GameState.scala` with all 22 fields + `GameStateConverter` with full round-trip support |
|
||||
| **EngineImpl** | **Complete** | Holds Scala `GameState` internally (PR #4563) |
|
||||
| **GameHistory** | **Complete** | `stateAfter` returns Scala GameState, `sinceDate` uses Scala Date (PR #4576) |
|
||||
| Phase | Status | Summary |
|
||||
|-------|--------|---------|
|
||||
| Phase 1: GameStateC | **Complete** | Scala `GameState` model with 22 fields |
|
||||
| Phase 2: EngineImpl | **Complete** | Holds Scala `GameState` internally |
|
||||
| Phase 3: GameHistory | **Complete** | `stateAfter` returns Scala GameState |
|
||||
| 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 |
|
||||
|
||||
### What's Partially Done ⚠️
|
||||
### Phase 5c/5d Progress (Complete)
|
||||
|
||||
| Component | Status | Details |
|
||||
|-----------|--------|---------|
|
||||
| Actions | **~59/59 (100%)** | All actions return `ActionResultT`. DeterministicSingleResultAction conversions complete. |
|
||||
| ActionResultT | **Trait complete** | Full concrete implementation with converters |
|
||||
`RoundPhaseAdvancer.checkForPhaseAdvancement` now accepts Scala `GameState` and `ActionResultApplier` directly (PR #4677).
|
||||
|
||||
### What's Outstanding ❌
|
||||
| Action | PR | Status |
|
||||
|--------|-----|--------|
|
||||
| `PrisonerExchangeAction` | #4670 | ✅ Merged |
|
||||
| `PerformForcedTurnBackAction` | #4671 | ✅ Merged |
|
||||
| `PerformHeroDeparturesAction` | #4672 | ✅ Merged |
|
||||
| `RequestFreeForAllBattlesAction` | #4673 | ✅ Merged |
|
||||
| `EndPlayerCommandsPhaseAction` | #4674 | ✅ Merged |
|
||||
| `EndDiplomacyResolutionPhaseAction` | #4675 | ✅ Merged |
|
||||
| `RoundPhaseAdvancer` itself | #4677 | ✅ Merged |
|
||||
|
||||
| Component | Priority | Details |
|
||||
|-----------|----------|---------|
|
||||
| ~~DeterministicSingleResultAction~~ | ~~High~~ | ✅ All converted to ProtolessSimpleAction |
|
||||
| ~~RandomSequentialResultsAction~~ | ~~Medium~~ | ✅ All converted to use Scala GameState (PR #4624) |
|
||||
| Legacy Utilities | **Low** | Direct proto imports in utility classes |
|
||||
### EngineImpl Progress
|
||||
|
||||
| Change | PR | Status |
|
||||
|--------|-----|--------|
|
||||
| `recursiveTransform` deleted | #4677 | ✅ Merged |
|
||||
| `recursiveTransformT` uses `RandomStateTSequencer` | #4677 | ✅ Merged |
|
||||
|
||||
### Current Architecture
|
||||
|
||||
**ActionResultT Production (100% Complete):**
|
||||
- All actions produce `ActionResultT`
|
||||
- Conversion to `ActionResultProto` happens via `ActionResultProtoConverter.toProto()`
|
||||
- No direct `ActionResultProto` construction outside the converter
|
||||
|
||||
**ActionResultProto Consumption (Next Target):**
|
||||
- `ActionResultProtoApplierImpl` - applies proto results to proto GameState
|
||||
- `RoundPhaseAdvancer` - calls converter, passes protos to applier
|
||||
- `InMemoryHistory` / `PersistedHistory` - stores proto results
|
||||
- Service layer (`GameController`, `GamesManager`, etc.) - uses proto for client communication
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: ~~Create GameStateC~~ ✅ COMPLETE
|
||||
|
||||
**Already done!** The Scala `GameState` model and converter exist:
|
||||
|
||||
- `src/main/scala/net/eagle0/eagle/model/state/game_state/GameState.scala` - Case class with 22 fields
|
||||
- `src/main/scala/net/eagle0/eagle/model/proto_converters/game_state/GameStateConverter.scala` - Full bidirectional conversion
|
||||
|
||||
```scala
|
||||
// GameState.scala - already exists with all fields:
|
||||
case class GameState(
|
||||
gameId: GameId,
|
||||
currentRoundId: RoundId,
|
||||
currentPhase: RoundPhase,
|
||||
currentDate: Option[Date],
|
||||
actionResultCount: Int,
|
||||
provinces: Map[ProvinceId, ProvinceT],
|
||||
heroes: Map[HeroId, HeroT],
|
||||
battalions: Map[BattalionId, BattalionT],
|
||||
destroyedBattalions: Map[BattalionId, BattalionT],
|
||||
factions: Map[FactionId, FactionT],
|
||||
factionCommandCounts: Map[FactionId, Int],
|
||||
killedHeroes: Map[HeroId, HeroT],
|
||||
destroyedFactions: Map[FactionId, FactionT],
|
||||
outstandingBattles: Vector[ShardokBattle],
|
||||
battleCounter: Int,
|
||||
deferredNotifications: Vector[NotificationT],
|
||||
runStatus: RunStatus,
|
||||
victor: Option[FactionId],
|
||||
battalionTypes: Vector[BattalionType],
|
||||
randomSeed: Long,
|
||||
chronicleEntries: Vector[ChronicleEntry]
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: ~~Update EngineImpl~~ ✅ COMPLETE (PR #4563)
|
||||
## Phase 6: Migrate to ActionResultT Consumers
|
||||
|
||||
### Objective
|
||||
Change EngineImpl to hold `GameState` (Scala model) internally instead of `GameState` (proto).
|
||||
|
||||
### Completed Changes
|
||||
Eliminate internal consumption of `ActionResultProto`. Everything inside the engine should work with `ActionResultT`.
|
||||
|
||||
- EngineImpl now uses `net.eagle0.eagle.model.state.game_state.GameState` (Scala)
|
||||
- Engine trait updated to return Scala `GameState`
|
||||
- GameController, GamesManager, and AIClient updated to use Scala models
|
||||
- Converters called only at persistence/GRPC boundaries
|
||||
|
||||
### Validation
|
||||
- [x] EngineImpl compiles with new types
|
||||
- [x] Engine operations work correctly
|
||||
- [x] All tests pass
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: ~~Update GameHistory~~ ✅ COMPLETE (PR #4576)
|
||||
|
||||
### Objective
|
||||
GameHistory should work with Scala models internally, converting to/from proto only for persistence.
|
||||
|
||||
### Completed Changes
|
||||
|
||||
- `GameHistory.stateAfter(count: Int)` now returns Scala `GameState`
|
||||
- `GameHistory.sinceDate(date: Date)` now accepts Scala `Date`
|
||||
- `InMemoryHistory` and `PersistedHistory` updated to use converters at boundaries
|
||||
- Callers updated:
|
||||
- `EngineImpl` - wraps with `GameStateConverter.toProto()` where proto needed
|
||||
- `UnrequestedTextHandler` - same
|
||||
- `HumanPlayerClientConnectionState` - imports converter
|
||||
- `NewRoundAction` / `ChronicleEventGenerator` - uses Scala Date
|
||||
|
||||
### Validation
|
||||
- [x] GameHistory compiles
|
||||
- [x] Game state persistence/retrieval works
|
||||
- [x] All 7 service tests pass
|
||||
- [x] NewRoundActionTest passes
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: ~~Complete ActionResultT Implementation~~ ✅ ESSENTIALLY COMPLETE
|
||||
|
||||
### Assessment Results (2024-11)
|
||||
|
||||
The ActionResultT infrastructure is **already 86% complete**:
|
||||
|
||||
| Component | Status | Details |
|
||||
|-----------|--------|---------|
|
||||
| **ActionResultT trait** | ✅ Complete | Full interface with 27 fields |
|
||||
| **ActionResultC case class** | ✅ Complete | Concrete implementation |
|
||||
| **ActionResultTApplier** | ✅ Exists | Wraps proto applier for gradual migration |
|
||||
| **ActionResultProtoConverter** | ✅ Complete | Bidirectional toProto/fromProto |
|
||||
| **Actions using ActionResultT** | ✅ 51/59 (86%) | Only ~10 actions still use proto |
|
||||
|
||||
### Remaining Proto Actions (4 DeterministicSingleResultAction)
|
||||
|
||||
**DeterministicSingleResultAction subclasses (0 remaining):**
|
||||
- ~~EndBattleRequestPhaseAction~~ ✅ PR #4581
|
||||
- ~~EndBattleResolutionPhaseAction~~ ✅ PR #4581
|
||||
- ~~EndFreeForAllBattleResolutionPhaseAction~~ ✅ PR #4585
|
||||
- ~~EndFreeForAllBattleRequestPhaseAction~~ ✅ PR #4585
|
||||
- ~~EndPleaseRecruitMePhaseAction~~ ✅ PR #4586
|
||||
- ~~EndDefenseDecisionPhaseAction~~ ✅ PR #4586
|
||||
- ~~PerformFoodConsumptionPhaseAction~~ ✅
|
||||
- ~~PerformHostileArmySetupAction~~ ✅
|
||||
- ~~NewYearAction~~ ✅
|
||||
|
||||
**RandomSequentialResultsAction subclasses (all converted to use Scala GameState - PR #4624):**
|
||||
- PerformProvinceMoveResolutionAction ✅, TruceTurnBackPhaseAction ✅
|
||||
- PerformUnaffiliatedHeroesAction ✅ (internal deproto complete PR #4606 - uses Scala GameState, inlined move action)
|
||||
- PerformVassalCommandsPhaseAction ✅
|
||||
- PerformVassalDefenseDecisionsAction ✅, PerformReconResolutionAction ✅
|
||||
- EndVassalCommandsPhaseAction ✅, EndHandleRiotsPhaseAction ✅
|
||||
- PerformProvinceEventsAction ✅
|
||||
- NewRoundAction ✅
|
||||
|
||||
**DeterministicSequentialResultsAction subclasses (already return ActionResultT via results()):**
|
||||
- PrisonerExchangeAction, PerformForcedTurnBackAction, PerformHeroDeparturesAction
|
||||
|
||||
### Decision
|
||||
|
||||
These remaining actions will be converted as part of **Phase 5** alongside the GameState parameter migration. No separate Phase 4 work needed.
|
||||
|
||||
### Validation
|
||||
- [x] All action result types have Scala models
|
||||
- [x] Converter handles all cases
|
||||
- [x] ActionResultTApplier exists for gradual migration
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Convert Actions & RoundPhaseAdvancer
|
||||
|
||||
### Objective
|
||||
Convert actions to accept Scala `GameState`, then convert `RoundPhaseAdvancer` to orchestrate with Scala models throughout.
|
||||
|
||||
### Strategic Insight
|
||||
|
||||
`RoundPhaseAdvancer` is the central orchestrator that calls all phase-handling actions. Currently:
|
||||
- It receives proto `GameState` (converted from Scala in EngineImpl)
|
||||
- ~13 actions take proto `GameState` directly
|
||||
- ~6 actions already take individual Scala model parameters (protoless)
|
||||
- Inline conversions exist to bridge proto→Scala for protoless actions
|
||||
|
||||
**Key win**: Converting actions called by RoundPhaseAdvancer enables converting RoundPhaseAdvancer itself, eliminating all inline conversions.
|
||||
|
||||
### Recommended Tiers
|
||||
|
||||
**Tier 1 - RoundPhaseAdvancer Actions (HIGH PRIORITY):**
|
||||
|
||||
These 13 actions are called directly from RoundPhaseAdvancer with proto `GameState`:
|
||||
### Current Flow (Proto-Heavy)
|
||||
```
|
||||
NewRoundAction
|
||||
PrisonerExchangeAction
|
||||
PerformProvinceEventsAction
|
||||
PerformForcedTurnBackAction
|
||||
PerformProvinceMoveResolutionAction
|
||||
EndHandleRiotsPhaseAction
|
||||
PerformHeroDeparturesAction
|
||||
PerformUnaffiliatedHeroesAction
|
||||
EndPleaseRecruitMePhaseAction
|
||||
PerformVassalCommandsPhaseAction / EndVassalCommandsPhaseAction
|
||||
PerformFoodConsumptionPhaseAction
|
||||
EndDefenseDecisionPhaseAction
|
||||
TruceTurnBackPhaseAction
|
||||
PerformReconResolutionAction
|
||||
EndBattleResolutionPhaseAction / EndFreeForAllBattleResolutionPhaseAction
|
||||
RequestFreeForAllBattlesAction / EndFreeForAllBattleRequestPhaseAction
|
||||
PerformHostileArmySetupAction
|
||||
PerformVassalDefenseDecisionsAction
|
||||
Action.execute()
|
||||
→ ActionResultT
|
||||
→ ActionResultProtoConverter.toProto()
|
||||
→ ActionResultProto
|
||||
→ ActionResultProtoApplierImpl.applyActionResults()
|
||||
→ GameStateProto
|
||||
→ GameStateConverter.fromProto()
|
||||
→ GameStateC
|
||||
```
|
||||
|
||||
Target: Convert each to accept Scala `GameState` parameter.
|
||||
### Target Flow (T-Types Throughout)
|
||||
```
|
||||
Action.execute()
|
||||
→ ActionResultT
|
||||
→ ActionResultApplier.applyActionResults()
|
||||
→ GameStateC
|
||||
|
||||
**Tier 2 - RoundPhaseAdvancer Itself:**
|
||||
(Proto conversion only at boundaries)
|
||||
```
|
||||
|
||||
Once Tier 1 is complete:
|
||||
### Key Files to Convert
|
||||
|
||||
**Tier 1 - Core Applier:** ✅ **Complete**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/actions/applier/ActionResultApplierImpl.scala
|
||||
```
|
||||
`ActionResultApplier` applies `ActionResultT` directly to Scala `GameState`. The legacy `ActionResultTApplierImpl` wraps it and converts to/from proto for callers that still need proto types.
|
||||
|
||||
**Tier 2 - RoundPhaseAdvancer:** ✅ **Complete**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/RoundPhaseAdvancer.scala
|
||||
```
|
||||
Now accepts Scala `GameState` and `ActionResultApplier`. Only converts to proto lazily for `AvailableCommandsFactory` calls.
|
||||
|
||||
Changes:
|
||||
1. Accept Scala `GameState` instead of proto
|
||||
2. Pattern match on Scala `RoundPhase` sealed trait (cleaner than proto enum)
|
||||
3. Remove all inline converter calls (no more `ProvinceConverter.fromProto`, etc.)
|
||||
4. Convert to proto only when calling `ActionResultProtoApplier.applyActionResults`
|
||||
|
||||
**Tier 3 - Remaining Actions:**
|
||||
|
||||
Other actions not called from RoundPhaseAdvancer:
|
||||
- Command actions, battle resolution, etc.
|
||||
- Can be converted incrementally after Tier 2
|
||||
|
||||
### Pattern for Conversion
|
||||
|
||||
**Before (proto-dependent):**
|
||||
```scala
|
||||
class SomeAction(gameState: GameState) extends ProtolessSimpleAction {
|
||||
def apply(): ActionResultT = {
|
||||
val faction = gameState.factions(factionId) // proto access
|
||||
// ...
|
||||
}
|
||||
}
|
||||
**Tier 3 - Sequencers:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateTSequencer.scala
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateProtoSequencer.scala
|
||||
```
|
||||
Modify `RandomStateTSequencer` to thread Scala `GameState` throughout (currently converts to proto internally). Then evaluate whether `RandomStateProtoSequencer` is still needed at all.
|
||||
|
||||
**After (Scala GameState):**
|
||||
```scala
|
||||
class SomeAction(gameState: GameState) extends ProtolessSimpleAction {
|
||||
// Now using: import net.eagle0.eagle.model.state.game_state.GameState
|
||||
def apply(): ActionResultT = {
|
||||
val faction = gameState.factions(factionId) // Scala model access
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
**Current State**: `RandomStateTSequencer` accepts Scala `GameState` via its `apply()` method but internally converts to proto. All callback methods (`withRandomActionResult`, `withActionResults`, etc.) pass `GameStateProto` to callers, forcing actions that use the sequencer to work with proto types internally.
|
||||
|
||||
### Files to Modify
|
||||
**Target State**: Create a fully protoless sequencer where:
|
||||
1. `lastState` returns Scala `GameState` (not `lastStateProto`)
|
||||
2. All callback methods pass Scala `GameState` to callers
|
||||
3. Actions using the sequencer can be fully protoless
|
||||
|
||||
**Migration Path**:
|
||||
1. Add `lastState: GameState` method alongside `lastStateProto` (non-breaking)
|
||||
2. Add parallel callback methods that pass Scala GameState (e.g., `withScalaActionResult`)
|
||||
3. Migrate actions one by one to use the new Scala-based callbacks
|
||||
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/library/RoundPhaseAdvancer.scala
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/action/*.scala
|
||||
src/main/scala/net/eagle0/eagle/service/InMemoryHistory.scala
|
||||
src/main/scala/net/eagle0/eagle/service/PersistedHistory.scala
|
||||
```
|
||||
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions. `PersistedHistory` converts to proto internally for disk persistence; `InMemoryHistory` doesn't need proto at all.
|
||||
|
||||
### ActionResultProto Consumer Inventory
|
||||
|
||||
| File | Usage | Status |
|
||||
|------|-------|--------|
|
||||
| `ActionResultApplierImpl.scala` | Applies ActionResultT to Scala GameState | ✅ **Complete** |
|
||||
| `ActionResultTApplierImpl.scala` | Legacy wrapper - converts to/from proto | Keep until all callers migrated |
|
||||
| `RoundPhaseAdvancer.scala` | Uses Scala GameState | ✅ **Complete** |
|
||||
| `RandomStateSequencer.scala` | Threads Scala GameState | ✅ **Complete** |
|
||||
| `VigorXPApplier.scala` | Has both proto and Scala methods | Scala method exists, delete proto method when unused |
|
||||
| `PerformForcedTurnBackAction.scala` | Fully protoless | ✅ **Complete** |
|
||||
| `ResolveBattleAction.scala` | Heavy proto usage | Blocked by proto dependencies |
|
||||
| `InMemoryHistory.scala` | Stores proto results | Pending - vend Scala types |
|
||||
| `PersistedHistory.scala` | Stores proto results | Pending - vend Scala types, convert for disk |
|
||||
| `GameController.scala` | Uses proto for client communication | Keep proto (gRPC boundary) |
|
||||
|
||||
### Remaining Proto Usage in Actions
|
||||
|
||||
The following actions still have proto usage, blocked by utility dependencies:
|
||||
|
||||
| Action | Proto Usage | Blocker |
|
||||
|--------|-------------|---------|
|
||||
| `EndBattleAftermathPhaseAction` | **Unblocked** | `ProvinceViewFilter` Scala overload added (PR #4752) |
|
||||
| `NewRoundAction` | 1 `fromProto` call | `ChronicleEventGenerator` returns proto |
|
||||
| `EndHandleRiotsPhaseAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `PerformVassalCommandsPhaseAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `PerformVassalDefenseDecisionsAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `EndVassalCommandsPhaseAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `PerformReconResolutionAction` | **Unblocked** | `ProvinceViewFilter` Scala overload added (PR #4752) |
|
||||
| `ResolveBattleAction` | Heavy proto usage | Large refactor needed |
|
||||
|
||||
### Estimated Effort (Remaining)
|
||||
|
||||
| Component | Lines | Complexity |
|
||||
|-----------|-------|------------|
|
||||
| `ProvinceViewFilter` faction-filtered views | ~100 | Medium (PR #4752 completed server-side) |
|
||||
| `CommandChoiceHelpers` to Scala | ~2000 | High |
|
||||
| `ChronicleEventGenerator` to Scala | ~400 | Medium |
|
||||
| History API updates | ~100 | Low |
|
||||
| **Total Remaining** | **~2600** | |
|
||||
|
||||
### Validation
|
||||
- [x] Each converted action compiles
|
||||
- [x] Each converted action passes its tests
|
||||
- [ ] RoundPhaseAdvancer works with Scala GameState
|
||||
- [ ] No inline converter calls in RoundPhaseAdvancer
|
||||
- [ ] EngineImpl no longer converts to proto before calling RoundPhaseAdvancer
|
||||
|
||||
---
|
||||
|
||||
## Phase 5b: Delete RandomSequentialResultsAction Base Class
|
||||
|
||||
### Objective
|
||||
Migrate `RandomSequentialResultsAction` subclasses to a new `TRandomSequentialResultsAction` base class, enabling deletion of the proto-based base class.
|
||||
|
||||
### Progress (IN PROGRESS)
|
||||
|
||||
**Completed:**
|
||||
- [x] Created `TRandomSequentialResultsAction` base class
|
||||
- Takes Scala `GameState` as constructor parameter
|
||||
- Extends `Action` trait (provides `execute()`)
|
||||
- Uses `ActionResultTApplier` for applying results
|
||||
- Uses `RandomStateTSequencer` for sequencing operations
|
||||
- [x] Converted `EndVassalCommandsPhaseAction`
|
||||
- [x] Converted `TruceTurnBackPhaseAction`
|
||||
- [x] Converted `PerformUnaffiliatedHeroesAction` (was already mostly T-based internally)
|
||||
- [x] Converted `PerformProvinceMoveResolutionAction` (uses T-based sub-actions)
|
||||
- [x] Converted `PerformReconResolutionAction` (uses T-based types: `ChangedProvinceC`, `ChangedFactionC`, `ClientTextVisibilityExtensionC`)
|
||||
- [x] Converted `NewRoundAction` (uses T-based types: `ChangedProvinceC`, `ChangedHeroC`, `ChangedFactionC`, `LlmRequestT.ChronicleUpdateMessage`, `ChronicleEntry`)
|
||||
|
||||
**Remaining - Blocked (4 actions):**
|
||||
|
||||
These actions cannot be easily converted because they use `CommandFactory.makeCommand()` which returns proto `Action`:
|
||||
|
||||
| Action | Blocker |
|
||||
|--------|---------|
|
||||
| `EndHandleRiotsPhaseAction` | Uses `CommandFactory.makeCommand()` |
|
||||
| `PerformProvinceEventsAction` | Heavily proto-based throughout (builds `ChangedProvince`, `ChangedFaction`, etc.) |
|
||||
| `PerformVassalCommandsPhaseAction` | Uses `CommandFactory.makeCommand()` |
|
||||
| `PerformVassalDefenseDecisionsAction` | Uses `CommandFactory.makeCommand()` |
|
||||
|
||||
**Future work needed:**
|
||||
- Create T-based `CommandFactory` that returns protoless actions (or add `withRandomAction` to T-sequencer)
|
||||
- Convert the 4 remaining actions once infrastructure is in place
|
||||
- Note: `ChangedProvinceC`, `ChangedFactionC` now have full field support (used by `PerformReconResolutionAction`)
|
||||
|
||||
### New Architecture
|
||||
|
||||
**TRandomSequentialResultsAction** (new base class):
|
||||
- Location: `src/main/scala/net/eagle0/eagle/library/actions/impl/common/TRandomSequentialResultsAction.scala`
|
||||
- Takes `GameState` (Scala) as constructor parameter
|
||||
- Extends `Action` trait
|
||||
- `randomResults(FunctionalRandom, ActionResultTApplier)` returns `RandomState[Vector[ActionResultT]]`
|
||||
- `execute(ActionResultProtoApplier)` converts results to proto and applies them
|
||||
|
||||
**ProtolessRandomSequentialResultsAction** (kept unchanged):
|
||||
- Used for sub-actions that don't need `execute()` (e.g., `CheckForFactionChangesAction`)
|
||||
- Simpler interface: `randomResults(FunctionalRandom)` only
|
||||
|
||||
### Pattern for Conversion
|
||||
|
||||
**Before (proto ActionResult):**
|
||||
```scala
|
||||
case class SomeAction(gameState: GameState)
|
||||
extends RandomSequentialResultsAction(GameStateConverter.toProto(gameState)) {
|
||||
|
||||
override def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultProtoApplier: ActionResultProtoApplier
|
||||
): RandomState[Vector[ActionResult]] = {
|
||||
RandomStateProtoSequencer(...)
|
||||
.withActionResult(_ => ActionResult(`type` = SOME_TYPE, ...))
|
||||
.actionResults
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After (ActionResultT):**
|
||||
```scala
|
||||
case class SomeAction(gameState: GameState)
|
||||
extends TRandomSequentialResultsAction(gameState) {
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultTApplier: ActionResultTApplier
|
||||
): RandomState[Vector[ActionResultT]] = {
|
||||
RandomStateTSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultTApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.withActionResultT(_ => ActionResultC(SomeResultType, ...))
|
||||
.actionResults
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Changes in Conversion
|
||||
|
||||
1. **Base class**: `RandomSequentialResultsAction` → `TRandomSequentialResultsAction`
|
||||
2. **Sequencer**: `RandomStateProtoSequencer` → `RandomStateTSequencer`
|
||||
3. **Return type**: `RandomState[Vector[ActionResult]]` → `RandomState[Vector[ActionResultT]]`
|
||||
4. **Applier**: `ActionResultProtoApplier` → `ActionResultTApplier`
|
||||
5. **Result construction**: `ActionResult(...)` → `ActionResultC(...)`
|
||||
6. **Field names**: Proto field names → Scala model field names (e.g., `player` → `actingFactionId`)
|
||||
|
||||
### Estimated Effort
|
||||
- 8 remaining actions × ~50 lines each = ~400 lines to change
|
||||
- Complexity: Medium-High (requires careful mapping of proto → Scala field names)
|
||||
- Infrastructure complete: `TRandomSequentialResultsAction` base class created
|
||||
|
||||
### Validation
|
||||
- [x] `TRandomSequentialResultsAction` base class created
|
||||
- [x] 6/10 actions converted (`EndVassalCommandsPhaseAction`, `TruceTurnBackPhaseAction`, `PerformUnaffiliatedHeroesAction`, `PerformProvinceMoveResolutionAction`, `PerformReconResolutionAction`, `NewRoundAction`)
|
||||
- [x] Converted action tests pass
|
||||
- [ ] All 10 actions converted to `TRandomSequentialResultsAction` (4 blocked - see table above)
|
||||
- [ ] All action tests pass
|
||||
- [ ] `RandomSequentialResultsAction` base class deleted
|
||||
- [ ] No proto `ActionResult` construction in converted actions
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Clean Up Legacy Utilities
|
||||
|
||||
### Objective
|
||||
Remove direct proto imports from utility classes.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
#### 6.1 LegacyFactionUtils.scala
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/LegacyFactionUtils.scala
|
||||
```
|
||||
- Replace `import net.eagle0.eagle.internal.faction.Faction` with `FactionT`
|
||||
- Update method signatures
|
||||
|
||||
#### 6.2 LegacyUnaffiliatedHeroUtils.scala
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/LegacyUnaffiliatedHeroUtils.scala
|
||||
```
|
||||
- Replace proto imports with Scala model imports
|
||||
- Update method signatures
|
||||
|
||||
#### 6.3 BattalionTypeLoader.scala
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/BattalionTypeLoader.scala
|
||||
```
|
||||
- Keep proto usage for file loading (persistence boundary)
|
||||
- Convert immediately to Scala model after load
|
||||
|
||||
#### 6.4 BeastUtils.scala
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/BeastUtils.scala
|
||||
```
|
||||
- Replace proto BeastInfo with Scala model (if one exists, otherwise create)
|
||||
|
||||
### Validation
|
||||
- [ ] No proto imports in utility files (except for persistence/serialization)
|
||||
- [ ] All utilities work correctly with Scala models
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Verify GRPC Boundary
|
||||
|
||||
### Objective
|
||||
Confirm that protos are used correctly at the GRPC boundary — and ONLY there.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Create Scala-Native Sequencer (Future)
|
||||
|
||||
### Objective
|
||||
Create a `ScalaOnlySequencer` that operates on Scala `GameState` throughout, eliminating proto conversions inside sequenced operations.
|
||||
|
||||
### Background
|
||||
|
||||
Currently `RandomStateTSequencer` internally:
|
||||
1. Takes Scala `GameState` as input
|
||||
2. Converts to proto `GameStateProto` immediately
|
||||
3. Threads proto state through all operations
|
||||
4. Callbacks receive `GameStateProto`, requiring converter calls at every step:
|
||||
```scala
|
||||
.withRandomActionResults { (gs, fr) =>
|
||||
CheckForFactionChangesAction(
|
||||
factions = gs.factions.values.toVector.map(FactionConverter.fromProto), // conversion!
|
||||
provinces = gs.provinces.values.toVector.map(ProvinceConverter.fromProto), // conversion!
|
||||
...
|
||||
).randomResults(fr)
|
||||
}
|
||||
```
|
||||
|
||||
### Proposed Design
|
||||
|
||||
**ScalaOnlySequencer:**
|
||||
- Takes Scala `GameState` as input
|
||||
- Threads Scala `GameState` through all operations (no proto internally)
|
||||
- Callbacks receive Scala `GameState` directly
|
||||
- Uses a new `ActionResultScalaApplier` that applies `ActionResultT` to Scala `GameState`
|
||||
- Converts to proto only at the final output boundary
|
||||
|
||||
**Usage after conversion:**
|
||||
```scala
|
||||
ScalaOnlySequencer(gameState, applier, random)
|
||||
.withRandomActionResults { (gs, fr) =>
|
||||
CheckForFactionChangesAction(
|
||||
factions = gs.factions.values.toVector, // no conversion!
|
||||
provinces = gs.provinces.values.toVector, // no conversion!
|
||||
...
|
||||
).randomResults(fr)
|
||||
}
|
||||
.actionResults
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
- Phase 5 complete (actions use `TRandomSequentialResultsAction`)
|
||||
- Phase 6 complete (utilities work with Scala models)
|
||||
- `ActionResultScalaApplier` created (applies `ActionResultT` directly to Scala `GameState`)
|
||||
|
||||
### Estimated Effort
|
||||
- `ActionResultScalaApplier`: ~800-900 lines (port of `ActionResultProtoApplierImpl`)
|
||||
- `ScalaOnlySequencer`: ~200 lines (similar to `RandomStateTSequencer`)
|
||||
- Action updates: Remove converter calls from callbacks
|
||||
|
||||
### Benefits
|
||||
- Eliminates per-callback proto conversions
|
||||
- Cleaner code at usage sites
|
||||
- Better type safety (no proto field access mistakes)
|
||||
- Prepares for eventual removal of proto `GameState` from internal operations
|
||||
|
||||
### Validation
|
||||
- [ ] `ActionResultScalaApplier` created and tested
|
||||
- [ ] `ScalaOnlySequencer` created
|
||||
- [ ] Actions migrated to use `ScalaOnlySequencer`
|
||||
- [ ] No proto conversions inside sequenced operations
|
||||
- [x] `ActionResultApplier` created and tested
|
||||
- [x] `RandomStateSequencer` threads Scala GameState throughout
|
||||
- [x] `RoundPhaseAdvancer` uses T-types internally
|
||||
- [x] `ProvinceViewFilter` has Scala overload for server-side use (PR #4752)
|
||||
- [ ] `ProvinceViewFilter` faction-filtered views use Scala types
|
||||
- [ ] `CommandChoiceHelpers` uses Scala types
|
||||
- [ ] History APIs vend Scala types
|
||||
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
|
||||
- [ ] All tests pass
|
||||
|
||||
### Files to Review
|
||||
---
|
||||
|
||||
#### 7.1 EagleServiceImpl.scala
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/service/EagleServiceImpl.scala
|
||||
```
|
||||
## Phase 7: Clean Up Legacy Utilities
|
||||
|
||||
**Correct Pattern:**
|
||||
```scala
|
||||
// Receive proto from client
|
||||
def postCommand(request: PostCommandRequest): Future[PostCommandResponse] = {
|
||||
// Convert to Scala model
|
||||
val command = CommandConverter.fromProto(request.command)
|
||||
### Objective
|
||||
Remove remaining direct proto imports from utility classes.
|
||||
|
||||
// Process with Scala models
|
||||
val result = engine.processCommand(command)
|
||||
### Files to Modify
|
||||
|
||||
// Convert back to proto for response
|
||||
PostCommandResponse(ActionResultConverter.toProto(result))
|
||||
}
|
||||
```
|
||||
| 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 |
|
||||
|
||||
**This file SHOULD have proto imports** — it's the boundary.
|
||||
### View Filters (Partially Complete)
|
||||
|
||||
### Validation
|
||||
- [ ] GRPC service converts proto ↔ Scala at the boundary
|
||||
- [ ] No proto types leak into engine internals
|
||||
- [ ] No Scala model types leak into GRPC responses
|
||||
The view filter utilities now have Scala overloads for server-side use:
|
||||
|
||||
| File | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| `ProvinceViewFilter.scala` | **Partial** | `filteredProvinceView(ProvinceT, ScalaGameState)` added (PR #4752) |
|
||||
| `ArmyFilter.scala` | **Partial** | `filterArmy(ScalaArmy, Map[BattalionId, BattalionT], Option[FactionId])` added |
|
||||
| `BattalionViewFilter.scala` | **Complete** | Uses Scala `BattalionT` throughout |
|
||||
| `GameStateViewFilter.scala` | Pending | Uses proto types throughout |
|
||||
| `GameStateViewDiffer.scala` | Pending | Works with view protos |
|
||||
|
||||
**Unblocked Actions** (PR #4752):
|
||||
- `EndBattleAftermathPhaseAction` - can now use `filteredProvinceView(province, scalaGameState)`
|
||||
- `PerformReconResolutionAction` - can now use Scala overload
|
||||
- `GameStateFactionExtensions` - can now use `updatedReconnedProvinces` with Scala types
|
||||
|
||||
**Remaining Work**:
|
||||
- Faction-filtered `filteredProvinceView(Province, GameState, FactionId)` still uses proto types
|
||||
- `withdrawnFromProvinceView` still uses proto types
|
||||
- These are needed for client-facing views with visibility restrictions
|
||||
|
||||
---
|
||||
|
||||
## Rollout Strategy
|
||||
## Phase 8: Verify Boundaries
|
||||
|
||||
### Approach: Incremental, Always-Green
|
||||
### Objective
|
||||
Confirm protos are used correctly at boundaries — and ONLY there.
|
||||
|
||||
Each phase should result in a **buildable, testable** state. Never have a broken build.
|
||||
### Expected Proto Usage (Keep)
|
||||
- `EagleServiceImpl.scala` - gRPC boundary
|
||||
- `InMemoryHistory.scala` / `PersistedHistory.scala` - Persistence boundary
|
||||
- `*Converter.scala` - Explicit conversion utilities
|
||||
- `*Loader.scala` - File loading utilities
|
||||
|
||||
```
|
||||
Phase 1 (GameStateC) ──→ PR ──→ Merge ✅ (already existed)
|
||||
│
|
||||
↓
|
||||
Phase 2 (EngineImpl) ──→ PR #4563 ──→ Merge ✅
|
||||
│
|
||||
↓
|
||||
Phase 3 (GameHistory) ──→ PR #4576 ──→ Merge ✅
|
||||
│
|
||||
↓
|
||||
Phase 4 (ActionResult) ──→ ✅ Already complete (86% of actions use ActionResultT)
|
||||
│
|
||||
↓
|
||||
Phase 5 (Actions - batched) ──→ PRs ──→ Merge ← NEXT
|
||||
│
|
||||
↓
|
||||
Phase 6 (Utilities) ──→ PR ──→ Merge
|
||||
│
|
||||
↓
|
||||
Phase 7 (Verification) ──→ PR ──→ Merge
|
||||
```
|
||||
### Expected No Proto Usage (Verify)
|
||||
- `/library/actions/impl/` - Pure Scala models
|
||||
- `/library/util/` - Pure Scala models (except loaders)
|
||||
- `/model/state/` - Pure Scala models
|
||||
|
||||
### Testing Strategy
|
||||
---
|
||||
|
||||
1. **Unit Tests**: Each converter should have round-trip tests
|
||||
2. **Integration Tests**: Engine operations should produce correct results
|
||||
3. **Regression Tests**: Existing game scenarios should work identically
|
||||
## Open Questions
|
||||
|
||||
### Rollback Plan
|
||||
1. **Persistence Format**: Currently game state is persisted as proto. Should we keep proto for persistence (good for schema evolution) or switch to a different format?
|
||||
|
||||
If issues arise:
|
||||
- Each phase is a separate PR
|
||||
- Can revert individual PRs without affecting others
|
||||
- Converters provide backward compatibility during transition
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -599,132 +307,9 @@ If issues arise:
|
||||
- [ ] Zero proto imports in `/library/actions/` (except boundaries)
|
||||
- [ ] Zero proto imports in `/library/` utilities (except loaders)
|
||||
- [ ] `GameStateT` used throughout engine internals
|
||||
- [ ] Proto usage limited to: `EagleServiceImpl`, loaders, converters
|
||||
|
||||
### Functionality
|
||||
- [ ] All existing tests pass
|
||||
- [ ] Game behavior unchanged
|
||||
- [ ] Performance within acceptable bounds
|
||||
- [ ] Proto usage limited to: `EagleServiceImpl`, loaders, converters, persistence
|
||||
|
||||
### Architecture
|
||||
- [ ] Clear separation: Scala models (internal) vs Proto (boundaries)
|
||||
- [ ] Converters as the only bridge between domains
|
||||
- [ ] No "proto creep" into business logic
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: File Inventory
|
||||
|
||||
### Proto Files (Keep - for serialization)
|
||||
```
|
||||
src/main/protobuf/net/eagle0/eagle/internal/game_state.proto
|
||||
src/main/protobuf/net/eagle0/eagle/internal/action_result.proto
|
||||
src/main/protobuf/net/eagle0/eagle/internal/hero.proto
|
||||
... (all .proto files)
|
||||
```
|
||||
|
||||
### Scala Models (Complete)
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/model/state/
|
||||
├── army/Army.scala ✅
|
||||
├── battalion/BattalionT.scala ✅
|
||||
├── faction/FactionT.scala ✅
|
||||
├── hero/HeroT.scala ✅
|
||||
├── province/ProvinceT.scala ✅
|
||||
├── game_state/GameState.scala ✅
|
||||
└── ... (43 total)
|
||||
```
|
||||
|
||||
### Converters (Complete)
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/model/proto_converters/
|
||||
├── army/ArmyConverter.scala ✅
|
||||
├── battalion/BattalionConverter.scala ✅
|
||||
├── game_state/GameStateConverter.scala ✅
|
||||
└── ... (43 total)
|
||||
```
|
||||
|
||||
### Actions (Convert)
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/actions/
|
||||
├── ProvinceConqueredAction.scala ✅ (converted)
|
||||
├── HeroBackstoryUpdateAction.scala ✅ (converted)
|
||||
├── NewRoundAction.scala ✅ (converted - PR #4624)
|
||||
├── ResolveBattleAction.scala ❌ (needs conversion)
|
||||
└── ... (All RandomSequentialResultsAction subclasses converted)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Estimated Effort
|
||||
|
||||
| Phase | Files | Lines Changed | Complexity | Status |
|
||||
|-------|-------|---------------|------------|--------|
|
||||
| Phase 1 | ✅ | 0 | Complete | **DONE** |
|
||||
| Phase 2 | 10 | ~200 | High | **DONE** (PR #4563) |
|
||||
| Phase 3 | 18 | ~200 | Medium | **DONE** (PR #4576) |
|
||||
| Phase 4 | ✅ | 0 | Already complete | **DONE** (93% of actions use ActionResultT) |
|
||||
| Phase 5 | ~10 | ~500 | Medium | **DONE** (All action base classes converted) |
|
||||
| Phase 6 | 4-6 | ~300 | Low | |
|
||||
| Phase 7 | 1-2 | ~50 | Low | |
|
||||
| **Total** | **~35** | **~1250** | | **~85% complete** |
|
||||
|
||||
### Phase 5 Progress
|
||||
|
||||
**DeterministicSingleResultAction → ProtolessSimpleAction:**
|
||||
- PR #4581: EndBattleRequestPhaseAction, EndBattleResolutionPhaseAction ✅
|
||||
- PR #4585: EndFreeForAllBattleRequestPhaseAction, EndFreeForAllBattleResolutionPhaseAction ✅
|
||||
- PR #4586: EndPleaseRecruitMePhaseAction, EndDefenseDecisionPhaseAction ✅
|
||||
- PR #4606: PerformUnaffiliatedHeroesAction internal deproto (uses Scala GameState, inlined UnaffiliatedHeroMovedAction) ✅
|
||||
- PR #4611: PerformFoodConsumptionPhaseAction, PerformHostileArmySetupAction, NewYearAction ✅
|
||||
|
||||
**All DeterministicSingleResultAction conversions complete!** The `DeterministicSingleResultAction` base class has been deleted.
|
||||
|
||||
**RandomSequentialResultsAction → Scala GameState:**
|
||||
- PR #4624: EndVassalCommandsPhaseAction, EndHandleRiotsPhaseAction, PerformProvinceMoveResolutionAction, PerformProvinceEventsAction, TruceTurnBackPhaseAction, PerformVassalCommandsPhaseAction, PerformVassalDefenseDecisionsAction, PerformReconResolutionAction, NewRoundAction ✅
|
||||
|
||||
**All RandomSequentialResultsAction conversions complete!** All now accept Scala GameState.
|
||||
|
||||
Next: Phase 6 (Legacy Utilities) or Phase 7 (GRPC Boundary verification).
|
||||
|
||||
---
|
||||
|
||||
## Appendix C: Open Questions
|
||||
|
||||
1. ~~**GameState size**: Already resolved - using single case class with 22 fields.~~
|
||||
|
||||
2. **Mutable vs Immutable**: Currently using immutable case classes with `copy`. Is this performant enough for game state updates?
|
||||
|
||||
3. **Shardok Integration**: `ResolveBattleAction` communicates with Shardok. Should the Shardok interface use protos (external service) or Scala models?
|
||||
|
||||
4. **View Generation**: `GameStateViewDiffer` works with view protos for client updates. Should views also have Scala models, or is proto acceptable for client-facing projections?
|
||||
|
||||
5. **Persistence Format**: Currently game state is persisted as proto. Should we keep proto for persistence (good for schema evolution) or switch to a different format?
|
||||
|
||||
---
|
||||
|
||||
## Appendix D: Lessons Learned (from Phases 2-3)
|
||||
|
||||
### What Worked Well
|
||||
|
||||
1. **Incremental approach** - Each phase produces a working build. Tests catch issues immediately.
|
||||
|
||||
2. **Converter pattern** - Having `GameStateConverter.toProto()`/`fromProto()` allows gradual migration. Callers can convert at their boundaries without changing everything at once.
|
||||
|
||||
3. **Type aliases help readability** - Using `import ... as DateProto` and `import ... as ScalaDate` makes the code clearer when both types coexist during migration.
|
||||
|
||||
### Challenges Encountered
|
||||
|
||||
1. **Test mock expectations need updating** - When a method's return type changes (e.g., `stateAfter` returning Scala GameState instead of proto), all mock expectations need to match the new type. This can cascade through many test files.
|
||||
|
||||
2. **ActionWithResultingState still uses proto internally** - The `ActionWithResultingState` case class contains proto `GameState` and `ActionResult`. This means callers sometimes need to convert even when we'd prefer not to. Consider converting this class in a future phase.
|
||||
|
||||
3. **Build file dependencies** - Each source file change may require BUILD.bazel updates for new imports. Running `bazel run gazelle` helps but manual review is sometimes needed.
|
||||
|
||||
### Recommendations for Future Phases
|
||||
|
||||
1. **Run tests early and often** - Build failures reveal type mismatches quickly. Fix one file at a time rather than trying to change everything before testing.
|
||||
|
||||
2. **Update tests alongside source** - When changing a trait's return type, update the tests for that trait's implementations in the same PR.
|
||||
|
||||
3. **Consider deferring ActionWithResultingState conversion** - This is used in the event-sourcing history and may be better left as proto until we're ready to migrate persistence format.
|
||||
|
||||
Binary file not shown.
+102
-12
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
|
||||
"__INPUT_ARTIFACTS_HASH": 571423113,
|
||||
"__RESOLVED_ARTIFACTS_HASH": 438039003,
|
||||
"__INPUT_ARTIFACTS_HASH": 289080209,
|
||||
"__RESOLVED_ARTIFACTS_HASH": -131178107,
|
||||
"conflict_resolution": {
|
||||
"com.google.guava:failureaccess:1.0.1": "com.google.guava:failureaccess:1.0.2",
|
||||
"com.squareup.okio:okio:2.10.0": "com.squareup.okio:okio:3.6.0",
|
||||
"io.netty:netty-buffer:4.1.110.Final": "io.netty:netty-buffer:4.1.112.Final",
|
||||
"io.netty:netty-codec-http2:4.1.110.Final": "io.netty:netty-codec-http2:4.1.112.Final",
|
||||
"io.netty:netty-codec-http:4.1.110.Final": "io.netty:netty-codec-http:4.1.112.Final",
|
||||
@@ -155,6 +156,18 @@
|
||||
},
|
||||
"version": "1.4.2"
|
||||
},
|
||||
"com.squareup.okhttp3:okhttp": {
|
||||
"shasums": {
|
||||
"jar": "b1050081b14bb7a3a7e55a4d3ef01b5dcfabc453b4573a4fc019767191d5f4e0"
|
||||
},
|
||||
"version": "4.12.0"
|
||||
},
|
||||
"com.squareup.okhttp3:okhttp-sse": {
|
||||
"shasums": {
|
||||
"jar": "bff4fbcaef7aac2d910d4ff46dafaa4e6d15da127df6bac97216da46943a7d4c"
|
||||
},
|
||||
"version": "4.12.0"
|
||||
},
|
||||
"com.squareup.okhttp:okhttp": {
|
||||
"shasums": {
|
||||
"jar": "88ac9fd1bb51f82bcc664cc1eb9c225c90dc4389d660231b4cc737bebfe7d0aa"
|
||||
@@ -163,9 +176,15 @@
|
||||
},
|
||||
"com.squareup.okio:okio": {
|
||||
"shasums": {
|
||||
"jar": "a27f091d34aa452e37227e2cfa85809f29012a8ef2501a9b5a125a978e4fcbc1"
|
||||
"jar": "8e63292e5c53bb93c4a6b0c213e79f15990fed250c1340f1c343880e1c9c39b5"
|
||||
},
|
||||
"version": "2.10.0"
|
||||
"version": "3.6.0"
|
||||
},
|
||||
"com.squareup.okio:okio-jvm": {
|
||||
"shasums": {
|
||||
"jar": "67543f0736fc422ae927ed0e504b98bc5e269fda0d3500579337cb713da28412"
|
||||
},
|
||||
"version": "3.6.0"
|
||||
},
|
||||
"com.thesamet.scalapb:compilerplugin_3": {
|
||||
"shasums": {
|
||||
@@ -444,15 +463,27 @@
|
||||
},
|
||||
"org.jetbrains.kotlin:kotlin-stdlib": {
|
||||
"shasums": {
|
||||
"jar": "b8ab1da5cdc89cb084d41e1f28f20a42bd431538642a5741c52bbfae3fa3e656"
|
||||
"jar": "55e989c512b80907799f854309f3bc7782c5b3d13932442d0379d5c472711504"
|
||||
},
|
||||
"version": "1.4.20"
|
||||
"version": "1.9.10"
|
||||
},
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-common": {
|
||||
"shasums": {
|
||||
"jar": "a7112c9b3cefee418286c9c9372f7af992bd1e6e030691d52f60cb36dbec8320"
|
||||
"jar": "cde3341ba18a2ba262b0b7cf6c55b20c90e8d434e42c9a13e6a3f770db965a88"
|
||||
},
|
||||
"version": "1.4.20"
|
||||
"version": "1.9.10"
|
||||
},
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk7": {
|
||||
"shasums": {
|
||||
"jar": "ac6361bf9ad1ed382c2103d9712c47cdec166232b4903ed596e8876b0681c9b7"
|
||||
},
|
||||
"version": "1.9.10"
|
||||
},
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk8": {
|
||||
"shasums": {
|
||||
"jar": "a4c74d94d64ce1abe53760fe0389dd941f6fc558d0dab35e47c085a11ec80f28"
|
||||
},
|
||||
"version": "1.9.10"
|
||||
},
|
||||
"org.jetbrains:annotations": {
|
||||
"shasums": {
|
||||
@@ -779,12 +810,23 @@
|
||||
"org.checkerframework:checker-qual",
|
||||
"org.ow2.asm:asm"
|
||||
],
|
||||
"com.squareup.okhttp3:okhttp": [
|
||||
"com.squareup.okio:okio",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
],
|
||||
"com.squareup.okhttp3:okhttp-sse": [
|
||||
"com.squareup.okhttp3:okhttp",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
],
|
||||
"com.squareup.okhttp:okhttp": [
|
||||
"com.squareup.okio:okio"
|
||||
],
|
||||
"com.squareup.okio:okio": [
|
||||
"org.jetbrains.kotlin:kotlin-stdlib",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-common"
|
||||
"com.squareup.okio:okio-jvm"
|
||||
],
|
||||
"com.squareup.okio:okio-jvm": [
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-common",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
],
|
||||
"com.thesamet.scalapb:compilerplugin_3": [
|
||||
"com.google.protobuf:protobuf-java",
|
||||
@@ -992,6 +1034,13 @@
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-common",
|
||||
"org.jetbrains:annotations"
|
||||
],
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk7": [
|
||||
"org.jetbrains.kotlin:kotlin-stdlib"
|
||||
],
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk8": [
|
||||
"org.jetbrains.kotlin:kotlin-stdlib",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk7"
|
||||
],
|
||||
"org.json4s:json4s-ast_3": [
|
||||
"org.scala-lang:scala3-library_3"
|
||||
],
|
||||
@@ -1451,6 +1500,29 @@
|
||||
"com.google.truth:truth": [
|
||||
"com.google.common.truth"
|
||||
],
|
||||
"com.squareup.okhttp3:okhttp": [
|
||||
"okhttp3",
|
||||
"okhttp3.internal",
|
||||
"okhttp3.internal.authenticator",
|
||||
"okhttp3.internal.cache",
|
||||
"okhttp3.internal.cache2",
|
||||
"okhttp3.internal.concurrent",
|
||||
"okhttp3.internal.connection",
|
||||
"okhttp3.internal.http",
|
||||
"okhttp3.internal.http1",
|
||||
"okhttp3.internal.http2",
|
||||
"okhttp3.internal.io",
|
||||
"okhttp3.internal.platform",
|
||||
"okhttp3.internal.platform.android",
|
||||
"okhttp3.internal.proxy",
|
||||
"okhttp3.internal.publicsuffix",
|
||||
"okhttp3.internal.tls",
|
||||
"okhttp3.internal.ws"
|
||||
],
|
||||
"com.squareup.okhttp3:okhttp-sse": [
|
||||
"okhttp3.internal.sse",
|
||||
"okhttp3.sse"
|
||||
],
|
||||
"com.squareup.okhttp:okhttp": [
|
||||
"com.squareup.okhttp",
|
||||
"com.squareup.okhttp.internal",
|
||||
@@ -1459,7 +1531,7 @@
|
||||
"com.squareup.okhttp.internal.io",
|
||||
"com.squareup.okhttp.internal.tls"
|
||||
],
|
||||
"com.squareup.okio:okio": [
|
||||
"com.squareup.okio:okio-jvm": [
|
||||
"okio",
|
||||
"okio.internal"
|
||||
],
|
||||
@@ -1814,6 +1886,7 @@
|
||||
"kotlin.annotation",
|
||||
"kotlin.collections",
|
||||
"kotlin.collections.builders",
|
||||
"kotlin.collections.jdk8",
|
||||
"kotlin.collections.unsigned",
|
||||
"kotlin.comparisons",
|
||||
"kotlin.concurrent",
|
||||
@@ -1822,24 +1895,36 @@
|
||||
"kotlin.coroutines.cancellation",
|
||||
"kotlin.coroutines.intrinsics",
|
||||
"kotlin.coroutines.jvm.internal",
|
||||
"kotlin.enums",
|
||||
"kotlin.experimental",
|
||||
"kotlin.internal",
|
||||
"kotlin.internal.jdk7",
|
||||
"kotlin.internal.jdk8",
|
||||
"kotlin.io",
|
||||
"kotlin.io.encoding",
|
||||
"kotlin.io.path",
|
||||
"kotlin.jdk7",
|
||||
"kotlin.js",
|
||||
"kotlin.jvm",
|
||||
"kotlin.jvm.functions",
|
||||
"kotlin.jvm.internal",
|
||||
"kotlin.jvm.internal.markers",
|
||||
"kotlin.jvm.internal.unsafe",
|
||||
"kotlin.jvm.jdk8",
|
||||
"kotlin.jvm.optionals",
|
||||
"kotlin.math",
|
||||
"kotlin.properties",
|
||||
"kotlin.random",
|
||||
"kotlin.random.jdk8",
|
||||
"kotlin.ranges",
|
||||
"kotlin.reflect",
|
||||
"kotlin.sequences",
|
||||
"kotlin.streams.jdk8",
|
||||
"kotlin.system",
|
||||
"kotlin.text",
|
||||
"kotlin.time"
|
||||
"kotlin.text.jdk8",
|
||||
"kotlin.time",
|
||||
"kotlin.time.jdk8"
|
||||
],
|
||||
"org.jetbrains:annotations": [
|
||||
"org.intellij.lang.annotations",
|
||||
@@ -2270,8 +2355,11 @@
|
||||
"com.google.protobuf:protobuf-java",
|
||||
"com.google.re2j:re2j",
|
||||
"com.google.truth:truth",
|
||||
"com.squareup.okhttp3:okhttp",
|
||||
"com.squareup.okhttp3:okhttp-sse",
|
||||
"com.squareup.okhttp:okhttp",
|
||||
"com.squareup.okio:okio",
|
||||
"com.squareup.okio:okio-jvm",
|
||||
"com.thesamet.scalapb:compilerplugin_3",
|
||||
"com.thesamet.scalapb:lenses_3",
|
||||
"com.thesamet.scalapb:protoc-bridge_2.13",
|
||||
@@ -2324,6 +2412,8 @@
|
||||
"org.hamcrest:hamcrest-core",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-common",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk7",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk8",
|
||||
"org.jetbrains:annotations",
|
||||
"org.json4s:json4s-ast_3",
|
||||
"org.json4s:json4s-core_3",
|
||||
|
||||
@@ -65,11 +65,6 @@ private:
|
||||
const GameStateW& guessedState,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
[[nodiscard]] auto ChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const net::eagle0::shardok::api::GameStateView& gsv,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
public:
|
||||
explicit ShardokAIClient(
|
||||
PlayerId playerId,
|
||||
@@ -86,6 +81,12 @@ public:
|
||||
[[nodiscard]] auto ChooseCommandIndex(const ShardokEngine& engine) const
|
||||
-> CommandChoiceResults;
|
||||
|
||||
// Overload that works on copies of state - allows caller to release lock during AI thinking
|
||||
[[nodiscard]] auto ChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const net::eagle0::shardok::api::GameStateView& gsv,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
// MCTS configuration methods (only relevant when using MCTS algorithm)
|
||||
[[nodiscard]] auto GetMCTSConfig() const -> const mcts::MCTSConfig& { return mctsConfig; }
|
||||
void SetMCTSConfig(const mcts::MCTSConfig& config) { mctsConfig = config; }
|
||||
|
||||
@@ -24,9 +24,6 @@ using std::scoped_lock;
|
||||
using std::string;
|
||||
using std::unique_lock;
|
||||
using std::weak_ptr;
|
||||
using std::chrono::duration;
|
||||
|
||||
static constexpr duration kWaitForUpdatesDuration = std::chrono::milliseconds(5000);
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
|
||||
@@ -81,7 +78,7 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
|
||||
pi.is_defender(),
|
||||
e->GetCurrentGameState()->hex_map(),
|
||||
e->GetGameSettings()->GetGetter(),
|
||||
AIAlgorithmType::MCTS,
|
||||
AIAlgorithmType::ITERATIVE_DEEPENING,
|
||||
ScoringCalculatorType::MCTS_OPTIMIZED,
|
||||
mctsConfig);
|
||||
|
||||
@@ -111,17 +108,67 @@ void ShardokGameController::DoAIThread() {
|
||||
if (aiClients.empty()) { printf("No AI players, exiting AI thread.\n"); }
|
||||
|
||||
while (aiThreadKeepGoing) {
|
||||
while (incomingRegistrations > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
// Phase 1: Gather data for AI decision (brief lock)
|
||||
shared_ptr<ShardokAIClient> aiClient;
|
||||
PlayerId playerId;
|
||||
GameSettingsSPtr settings;
|
||||
net::eagle0::shardok::api::GameStateView gsv;
|
||||
CommandListSPtr availableCommands;
|
||||
size_t expectedHistoryCount;
|
||||
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
|
||||
if (engine->GameIsOver()) {
|
||||
aiThreadKeepGoing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
playerId = engine->GetCurrentPlayerId();
|
||||
aiClient = LockedAIClientForPid(playerId);
|
||||
|
||||
if (!aiClient) {
|
||||
// Not an AI player's turn - wait for signal
|
||||
aiCondition.wait(lk);
|
||||
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get copies of everything the AI needs
|
||||
settings = engine->GetGameSettings();
|
||||
gsv = engine->GetGameStateView(playerId);
|
||||
availableCommands = engine->GetAvailableCommandsForAIPlayer(playerId);
|
||||
expectedHistoryCount = engine->GetUnfilteredHistoryCount();
|
||||
}
|
||||
// Lock released - polls can now get through
|
||||
|
||||
if (availableCommands->empty()) {
|
||||
printf("no commands for player %d\n", playerId);
|
||||
continue;
|
||||
}
|
||||
|
||||
unique_lock lk(masterLock);
|
||||
if (LockedCheckOneAICommand()) {
|
||||
// Phase 2: AI thinks (NO LOCK - this is the slow part)
|
||||
const auto results = aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
|
||||
// Phase 3: Post the command (brief lock)
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
|
||||
// Verify state hasn't changed while we were thinking
|
||||
if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
|
||||
// State changed (e.g., human posted command) - re-evaluate
|
||||
printf("AI: State changed while thinking, re-evaluating\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (engine->GameIsOver()) {
|
||||
aiThreadKeepGoing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
engine->PostCommand(playerId, results.chosenIndex);
|
||||
LockedNotifyClients();
|
||||
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
|
||||
} else {
|
||||
aiCondition.wait(lk);
|
||||
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
|
||||
aiThreadKeepGoing = !engine->GameIsOver();
|
||||
}
|
||||
}
|
||||
printf("Exiting AI thread.\n");
|
||||
@@ -136,24 +183,6 @@ ShardokGameController::~ShardokGameController() {
|
||||
aiThread.join();
|
||||
}
|
||||
|
||||
auto ShardokGameController::LockedCheckOneAICommand() -> bool {
|
||||
if (engine->GameIsOver()) {
|
||||
printf("Game is over!\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const PlayerId currentPid = engine->GetCurrentPlayerId();
|
||||
if (const shared_ptr<ShardokAIClient> currentPlayerClient = LockedAIClientForPid(currentPid)) {
|
||||
const int index = currentPlayerClient->ChooseCommandIndex(*engine).chosenIndex;
|
||||
|
||||
engine->PostCommand(currentPid, index);
|
||||
LockedNotifyClients();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void CheckFactionId(
|
||||
const unique_ptr<ShardokEngine> &engine,
|
||||
const PlayerId shardokPlayerId,
|
||||
@@ -229,6 +258,7 @@ void ShardokGameController::PostPlacementCommands(
|
||||
}
|
||||
|
||||
auto ShardokGameController::GetCurrentGameStateBytes() -> byte_vector {
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
return engine->GetCurrentGameStateBytes();
|
||||
}
|
||||
|
||||
@@ -245,15 +275,9 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
awrs = engine->GetGameHistory(startingActionId);
|
||||
incomingRegistrations--;
|
||||
|
||||
// If the current player is an AI, wait for some results to post. Otherwise go ahead and
|
||||
// return, we might be telling the caller about available commands.
|
||||
if (awrs.empty() && !engine->GameIsOver() &&
|
||||
engine->GetPlayerInfos()[engine->GetCurrentPlayerId()].is_ai()) {
|
||||
updateCondition.wait_for(guard, kWaitForUpdatesDuration);
|
||||
incomingRegistrations++;
|
||||
awrs = engine->GetGameHistory(startingActionId);
|
||||
incomingRegistrations--;
|
||||
}
|
||||
// Note: Previously this had a 5-second wait for AI players to support long-polling.
|
||||
// With streaming (WaitForUpdatesAndPush), the caller already waits for updates,
|
||||
// so this wait is no longer needed. GetGameStatus is deprecated in favor of streaming.
|
||||
|
||||
updates.mainResults.reserve(awrs.size());
|
||||
std::ranges::transform(
|
||||
@@ -292,6 +316,9 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
-1,
|
||||
engine->FilterNewResults(-1, startingActionId),
|
||||
nullptr);
|
||||
|
||||
auto gameStateBytes = engine->GetCurrentGameStateBytes();
|
||||
updates.currentGameState.swap(gameStateBytes);
|
||||
}
|
||||
|
||||
return updates;
|
||||
@@ -321,4 +348,74 @@ auto ShardokGameController::ResolvedPlayerInfos()
|
||||
return engine->GetPlayerInfos();
|
||||
}
|
||||
|
||||
void ShardokGameController::RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber) {
|
||||
scoped_lock<mutex> guard(subscriberLock);
|
||||
subscribers.push_back(subscriber);
|
||||
}
|
||||
|
||||
void ShardokGameController::UnregisterSubscriber(const StreamSubscriber *subscriber) {
|
||||
scoped_lock<mutex> guard(subscriberLock);
|
||||
subscribers.erase(
|
||||
std::remove_if(
|
||||
subscribers.begin(),
|
||||
subscribers.end(),
|
||||
[subscriber](const std::weak_ptr<StreamSubscriber> &weakSub) {
|
||||
auto sub = weakSub.lock();
|
||||
return !sub || sub.get() == subscriber;
|
||||
}),
|
||||
subscribers.end());
|
||||
}
|
||||
|
||||
auto ShardokGameController::WaitForUpdatesAndPush(
|
||||
std::shared_ptr<StreamSubscriber> subscriber,
|
||||
int64_t startingActionId) -> bool {
|
||||
int64_t lastPushedActionId = startingActionId;
|
||||
|
||||
while (subscriber->IsActive()) {
|
||||
bool gameOver = false;
|
||||
GameOverInfo gameOverInfo{};
|
||||
|
||||
{
|
||||
unique_lock<mutex> guard(masterLock);
|
||||
|
||||
// Wait for updates or game over
|
||||
updateCondition.wait(guard, [this, lastPushedActionId] {
|
||||
return engine->GetUnfilteredHistoryCount() >
|
||||
static_cast<size_t>(lastPushedActionId) ||
|
||||
engine->GameIsOver();
|
||||
});
|
||||
|
||||
if (!subscriber->IsActive()) { return false; }
|
||||
|
||||
gameOver = engine->GameIsOver();
|
||||
|
||||
if (gameOver) {
|
||||
gameOverInfo.gameStatus = fb::ToProto(engine->GetGameStatus());
|
||||
gameOverInfo.playerInfos = engine->GetPlayerInfos();
|
||||
gameOverInfo.endGameUnits = engine->EndGameUnits();
|
||||
}
|
||||
}
|
||||
// Lock released - GetUpdates will acquire its own lock
|
||||
|
||||
if (gameOver) {
|
||||
subscriber->OnGameOver(gameOverInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get updates outside the lock (GetUpdates acquires masterLock internally)
|
||||
AllUpdates updates = GetUpdates(lastPushedActionId);
|
||||
lastPushedActionId = updates.newUnfilteredCount;
|
||||
|
||||
if (!updates.mainResults.empty()) {
|
||||
subscriber->OnUpdate(
|
||||
updates.mainResults,
|
||||
updates.filteredResults,
|
||||
updates.newUnfilteredCount,
|
||||
updates.currentGameState);
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Subscriber disconnected
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#define ShardokGameController_hpp
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -29,6 +30,50 @@ using std::shared_ptr;
|
||||
using std::unique_ptr;
|
||||
using std::weak_ptr;
|
||||
|
||||
// Forward declaration
|
||||
class ShardokGameController;
|
||||
|
||||
/// Info about a game that has ended, for notifying subscribers
|
||||
struct GameOverInfo {
|
||||
vector<net::eagle0::shardok::common::PlayerInfo> playerInfos;
|
||||
vector<net::eagle0::shardok::storage::ResolvedUnit> endGameUnits;
|
||||
net::eagle0::shardok::common::GameStatus gameStatus;
|
||||
};
|
||||
|
||||
/// Updates for a single player (includes faction ID for client routing)
|
||||
struct OnePlayerUpdates {
|
||||
int32_t eagleFactionId;
|
||||
vector<ActionResultView> resultViews;
|
||||
shared_ptr<AvailableCommands> availableCommands;
|
||||
|
||||
OnePlayerUpdates(
|
||||
const int32_t fid,
|
||||
const vector<ActionResultView>& arvs,
|
||||
const shared_ptr<AvailableCommands>& acs)
|
||||
: eagleFactionId(fid),
|
||||
resultViews(arvs),
|
||||
availableCommands(acs) {}
|
||||
};
|
||||
|
||||
/// Interface for subscribers that receive streaming updates from a game
|
||||
class StreamSubscriber {
|
||||
public:
|
||||
virtual ~StreamSubscriber() = default;
|
||||
|
||||
/// Called when new game updates are available
|
||||
virtual void OnUpdate(
|
||||
const vector<ActionResult>& mainResults,
|
||||
const vector<OnePlayerUpdates>& filteredResults,
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector& currentGameState) = 0;
|
||||
|
||||
/// Called when the game ends
|
||||
virtual void OnGameOver(const GameOverInfo& info) = 0;
|
||||
|
||||
/// Returns true if this subscriber is still active and should receive updates
|
||||
[[nodiscard]] virtual auto IsActive() const -> bool = 0;
|
||||
};
|
||||
|
||||
class ShardokGameController {
|
||||
private:
|
||||
// This lock should be held any time we call into engine or modify clients.
|
||||
@@ -39,10 +84,17 @@ private:
|
||||
// Fires whenever there is a new game state update.
|
||||
mutable std::condition_variable updateCondition{};
|
||||
|
||||
// Stream subscribers - protected by separate lock to avoid deadlock with masterLock
|
||||
mutable std::mutex subscriberLock{};
|
||||
std::vector<std::weak_ptr<StreamSubscriber>> subscribers{};
|
||||
|
||||
string serializedRequest;
|
||||
|
||||
unique_ptr<ShardokEngine> engine;
|
||||
|
||||
// Cached immutable data - safe to access without lock since it never changes after construction
|
||||
const GameId cachedGameId;
|
||||
|
||||
std::atomic_int incomingRegistrations = 0;
|
||||
|
||||
const string mapName;
|
||||
@@ -60,11 +112,9 @@ private:
|
||||
|
||||
void LockedNotifyClients() const;
|
||||
|
||||
auto LockedCheckOneAICommand() -> bool;
|
||||
|
||||
auto LockedAIClientForPid(PlayerId pid) const -> shared_ptr<ShardokAIClient>;
|
||||
|
||||
static vector<shared_ptr<ShardokAIClient>> MakeAIClients(const unique_ptr<ShardokEngine> &e);
|
||||
static vector<shared_ptr<ShardokAIClient>> MakeAIClients(const unique_ptr<ShardokEngine>& e);
|
||||
|
||||
void DoAIThread();
|
||||
|
||||
@@ -75,6 +125,7 @@ public:
|
||||
string serializedRequest = "")
|
||||
: serializedRequest(std::move(serializedRequest)),
|
||||
engine(std::move(e)),
|
||||
cachedGameId(engine->GetGameId()),
|
||||
mapName(std::move(mapName)),
|
||||
logFilePath(MakeLogFilePath()),
|
||||
aiClients(MakeAIClients(engine)),
|
||||
@@ -98,26 +149,13 @@ public:
|
||||
PlayerId shardokPlayerId,
|
||||
int eagleFactionId,
|
||||
int64_t token,
|
||||
const vector<UnitPlacementInfo> &infos);
|
||||
|
||||
struct OnePlayerUpdates {
|
||||
int32_t eagleFactionId;
|
||||
vector<ActionResultView> resultViews;
|
||||
shared_ptr<AvailableCommands> availableCommands;
|
||||
|
||||
OnePlayerUpdates(
|
||||
const int32_t fid,
|
||||
const vector<ActionResultView> &arvs,
|
||||
const shared_ptr<AvailableCommands> &acs)
|
||||
: eagleFactionId(fid),
|
||||
resultViews(arvs),
|
||||
availableCommands(acs) {}
|
||||
};
|
||||
const vector<UnitPlacementInfo>& infos);
|
||||
|
||||
struct AllUpdates {
|
||||
vector<ActionResult> mainResults;
|
||||
vector<OnePlayerUpdates> filteredResults;
|
||||
int32_t newUnfilteredCount;
|
||||
byte_vector currentGameState;
|
||||
};
|
||||
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
|
||||
auto GetCurrentGameStateBytes() -> byte_vector;
|
||||
@@ -127,13 +165,23 @@ public:
|
||||
auto ResolvedPlayerInfos() -> vector<net::eagle0::shardok::common::PlayerInfo>;
|
||||
auto EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit>;
|
||||
|
||||
[[nodiscard]] auto GetGameId() const -> GameId { return engine->GetGameId(); }
|
||||
|
||||
[[nodiscard]] auto GetHexMap() const -> const HexMap * {
|
||||
return engine->GetCurrentGameState()->hex_map();
|
||||
}
|
||||
[[nodiscard]] auto GetGameId() const -> GameId { return cachedGameId; }
|
||||
|
||||
[[nodiscard]] auto GetLogFilePath() const -> string { return logFilePath; }
|
||||
|
||||
/// Register a subscriber to receive streaming updates for this game.
|
||||
/// The subscriber will receive updates until it becomes inactive or is unregistered.
|
||||
void RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber);
|
||||
|
||||
/// Unregister a subscriber. Safe to call even if the subscriber was never registered.
|
||||
void UnregisterSubscriber(const StreamSubscriber* subscriber);
|
||||
|
||||
/// Wait for game updates, pushing them to the given subscriber.
|
||||
/// Blocks until the game ends or the subscriber becomes inactive.
|
||||
/// Returns true if the game ended normally, false if subscriber disconnected.
|
||||
auto WaitForUpdatesAndPush(
|
||||
std::shared_ptr<StreamSubscriber> subscriber,
|
||||
int64_t startingActionId) -> bool;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -529,6 +529,125 @@ auto FromInternalStatus(
|
||||
throw ShardokInternalErrorException("Bad unit status on resolved unit");
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::SubscribeToGame(
|
||||
ServerContext *context,
|
||||
const GameSubscriptionRequest *request,
|
||||
grpc::ServerWriter<GameStatusResponse> *writer) -> Status {
|
||||
shared_ptr<ShardokGameController> controller;
|
||||
try {
|
||||
controller = ControllerForGame(request->game_id(), request->game_setup_info());
|
||||
} catch (NewGameException &e) { return e.GetStatus(); }
|
||||
|
||||
if (!controller) { return Status(StatusCode::NOT_FOUND, "Game not found"); }
|
||||
|
||||
// Send initial state
|
||||
GameStatusResponse initialResponse;
|
||||
PopulateGameStatusResponse(
|
||||
controller,
|
||||
request->game_setup_info().known_result_count(),
|
||||
&initialResponse);
|
||||
if (!writer->Write(initialResponse)) {
|
||||
return Status::OK; // Client disconnected
|
||||
}
|
||||
|
||||
// If game was already over, we're done
|
||||
if (initialResponse.has_game_over_response()) { return Status::OK; }
|
||||
|
||||
// Create a subscriber that writes to the gRPC stream
|
||||
class GrpcStreamSubscriber : public StreamSubscriber {
|
||||
private:
|
||||
grpc::ServerWriter<GameStatusResponse> *writer_;
|
||||
ServerContext *context_;
|
||||
std::atomic<bool> active_{true};
|
||||
std::string gameId_;
|
||||
|
||||
public:
|
||||
GrpcStreamSubscriber(
|
||||
grpc::ServerWriter<GameStatusResponse> *w,
|
||||
ServerContext *ctx,
|
||||
std::string gameId)
|
||||
: writer_(w),
|
||||
context_(ctx),
|
||||
gameId_(std::move(gameId)) {}
|
||||
|
||||
void OnUpdate(
|
||||
const vector<ActionResult> &mainResults,
|
||||
const vector<OnePlayerUpdates> &filteredResults,
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector ¤tGameState) override {
|
||||
if (!active_) return;
|
||||
|
||||
GameStatusResponse response;
|
||||
response.set_game_id(gameId_);
|
||||
|
||||
response.mutable_game_update_response()->mutable_update_responses()->Add(
|
||||
begin(mainResults),
|
||||
end(mainResults));
|
||||
|
||||
// Add filtered results for each player with their faction IDs
|
||||
for (const auto &playerUpdate : filteredResults) {
|
||||
auto *filtered =
|
||||
response.mutable_game_update_response()->add_filtered_update_responses();
|
||||
filtered->set_eagle_faction_id(playerUpdate.eagleFactionId);
|
||||
filtered->mutable_action_result_views()->Add(
|
||||
begin(playerUpdate.resultViews),
|
||||
end(playerUpdate.resultViews));
|
||||
if (playerUpdate.availableCommands) {
|
||||
*filtered->mutable_available_commands() = *playerUpdate.availableCommands;
|
||||
}
|
||||
}
|
||||
|
||||
response.mutable_game_update_response()->set_total_action_result_count(
|
||||
newUnfilteredCount);
|
||||
*response.mutable_game_update_response()->mutable_current_game_state() =
|
||||
std::string(currentGameState.begin(), currentGameState.end());
|
||||
|
||||
if (!writer_->Write(response)) { active_ = false; }
|
||||
}
|
||||
|
||||
void OnGameOver(const GameOverInfo &info) override {
|
||||
if (!active_) return;
|
||||
|
||||
GameStatusResponse response;
|
||||
response.set_game_id(gameId_);
|
||||
|
||||
PopulateGameOverResponse(
|
||||
gameId_,
|
||||
info.gameStatus,
|
||||
info.playerInfos,
|
||||
info.endGameUnits,
|
||||
response.mutable_game_over_response());
|
||||
|
||||
writer_->Write(response);
|
||||
active_ = false;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto IsActive() const -> bool override {
|
||||
return active_ && !context_->IsCancelled();
|
||||
}
|
||||
};
|
||||
|
||||
auto subscriber =
|
||||
std::make_shared<GrpcStreamSubscriber>(writer, context, controller->GetGameId());
|
||||
|
||||
controller->RegisterSubscriber(subscriber);
|
||||
|
||||
// Wait for updates and push them until game ends or subscriber disconnects
|
||||
bool gameEnded = controller->WaitForUpdatesAndPush(
|
||||
subscriber,
|
||||
request->game_setup_info().known_result_count());
|
||||
|
||||
controller->UnregisterSubscriber(subscriber.get());
|
||||
|
||||
if (gameEnded) {
|
||||
printf("SubscribeToGame: Game ended normally\n");
|
||||
} else {
|
||||
printf("SubscribeToGame: Subscriber disconnected\n");
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#ifndef NDEBUG
|
||||
|
||||
@@ -33,6 +33,7 @@ using grpc::Status;
|
||||
using net::eagle0::common::GameSetupInfo;
|
||||
using net::eagle0::common::GameStatusRequest;
|
||||
using net::eagle0::common::GameStatusResponse;
|
||||
using net::eagle0::common::GameSubscriptionRequest;
|
||||
using net::eagle0::common::HexMapNamesRequest;
|
||||
using net::eagle0::common::HexMapNamesResponse;
|
||||
using net::eagle0::common::HexMapRequest;
|
||||
@@ -78,6 +79,11 @@ public:
|
||||
ServerContext* context,
|
||||
const HexMapNamesRequest* request,
|
||||
HexMapNamesResponse* response) -> Status override;
|
||||
|
||||
auto SubscribeToGame(
|
||||
ServerContext* context,
|
||||
const GameSubscriptionRequest* request,
|
||||
grpc::ServerWriter<GameStatusResponse>* writer) -> Status override;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceRejectedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/RiotSuppressedNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManagerEditor.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/AutoScrollingText.cs" />
|
||||
<Compile Include="Assets/Bluetooth/NativeDiceInterfaceImports.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ProvinceConqueredNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/ExtraTroopsRowController.cs" />
|
||||
|
||||
+3
@@ -449,4 +449,7 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
} };
|
||||
|
||||
public List<IClientConnectionSubscriber.StreamingTextStatus> StreamingTextStatuses => new();
|
||||
|
||||
// CustomBattleHandler only handles Shardok updates, not Eagle, so no count to update
|
||||
public void UpdateResultCounts(GameUpdate update) {}
|
||||
}
|
||||
|
||||
+6
-2
@@ -24,7 +24,7 @@ namespace eagle {
|
||||
|
||||
public void OnTextUpdate(string text, bool completed) { SetUp(); }
|
||||
|
||||
public string TextId() { return CurrentEntry.GeneratedTextId; }
|
||||
public string TextId() { return _entries.Count > 0 ? CurrentEntry.GeneratedTextId : null; }
|
||||
|
||||
private void OnEnable() {
|
||||
ClientTextProvider.Provider.AddListener(this);
|
||||
@@ -41,11 +41,13 @@ 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;
|
||||
|
||||
if (!gameObject.activeSelf) {
|
||||
// Jump to the last entry when first populating, or if not currently viewing
|
||||
if (wasEmpty || !gameObject.activeSelf) {
|
||||
_currentIndex = _entries.Count - 1;
|
||||
|
||||
ScrollToTop();
|
||||
@@ -60,6 +62,8 @@ 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;
|
||||
|
||||
|
||||
+70
-16
@@ -19,35 +19,86 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe provider for streaming LLM text content.
|
||||
///
|
||||
/// HandleNewStreamingText can be called from any thread (e.g., gRPC thread).
|
||||
/// ProcessPendingUpdates must be called from the main thread (once per frame)
|
||||
/// to notify listeners of changes.
|
||||
/// </summary>
|
||||
public class ClientTextProvider {
|
||||
public static readonly ClientTextProvider Provider = new();
|
||||
|
||||
// Lock for thread-safe access to text dictionary
|
||||
// Using a lock instead of ConcurrentDictionary because HandleNewStreamingText
|
||||
// does a read-modify-write that must be atomic
|
||||
private readonly object _lock = new();
|
||||
private readonly Dictionary<String, TextEntry> _streamingTexts = new();
|
||||
|
||||
// Track which text IDs have pending updates
|
||||
private readonly HashSet<String> _pendingUpdates = new();
|
||||
|
||||
// Listeners are only added/removed from main thread
|
||||
private readonly HashSet<IClientTextListener> _listeners = new();
|
||||
|
||||
public void Clear() { _streamingTexts.Clear(); }
|
||||
|
||||
public Dictionary<String, TextEntry> All() {
|
||||
return new Dictionary<string, TextEntry>(_streamingTexts);
|
||||
public void Clear() {
|
||||
lock (_lock) {
|
||||
_streamingTexts.Clear();
|
||||
_pendingUpdates.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<String, TextEntry> All() {
|
||||
lock (_lock) { return new Dictionary<string, TextEntry>(_streamingTexts); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the text dictionary. Thread-safe - can be called from any thread.
|
||||
/// Listeners are NOT notified here; call ProcessPendingUpdates from main thread.
|
||||
/// </summary>
|
||||
public String
|
||||
HandleNewStreamingText(String llmId, String newText, Int32 knownByteCount, bool completed) {
|
||||
var currentText = "";
|
||||
if (_streamingTexts.TryGetValue(llmId, out var entry)) { currentText = entry.Text; }
|
||||
lock (_lock) {
|
||||
var currentText = "";
|
||||
if (_streamingTexts.TryGetValue(llmId, out var entry)) { currentText = entry.Text; }
|
||||
|
||||
var currentTextBytes = Encoding.UTF8.GetBytes(currentText);
|
||||
var truncatedBytes = currentTextBytes.Take(knownByteCount).ToArray();
|
||||
var currentTextBytes = Encoding.UTF8.GetBytes(currentText);
|
||||
var truncatedBytes = currentTextBytes.Take(knownByteCount).ToArray();
|
||||
|
||||
var updatedText = Encoding.UTF8.GetString(truncatedBytes) + newText;
|
||||
_streamingTexts[llmId] = new TextEntry(updatedText, completed);
|
||||
var updatedText = Encoding.UTF8.GetString(truncatedBytes) + newText;
|
||||
_streamingTexts[llmId] = new TextEntry(updatedText, completed);
|
||||
|
||||
_listeners.Where(x => x.TextId() == llmId)
|
||||
.ToList()
|
||||
.ForEach(x => x.OnTextUpdate(updatedText, completed));
|
||||
// Mark this text ID as having pending updates
|
||||
_pendingUpdates.Add(llmId);
|
||||
|
||||
return updatedText;
|
||||
return updatedText;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process pending updates and notify listeners. Must be called from main thread.
|
||||
/// This batches multiple updates to the same text ID into a single notification per frame.
|
||||
/// </summary>
|
||||
public void ProcessPendingUpdates() {
|
||||
List<(String llmId, TextEntry entry)> updates;
|
||||
|
||||
lock (_lock) {
|
||||
if (_pendingUpdates.Count == 0) return;
|
||||
|
||||
// Collect pending updates and their current values
|
||||
updates = _pendingUpdates.Where(id => _streamingTexts.ContainsKey(id))
|
||||
.Select(id => (id, _streamingTexts[id]))
|
||||
.ToList();
|
||||
|
||||
_pendingUpdates.Clear();
|
||||
}
|
||||
|
||||
// Notify listeners outside the lock to avoid potential deadlocks
|
||||
foreach (var (llmId, textEntry) in updates) {
|
||||
foreach (var listener in _listeners.Where(x => x.TextId() == llmId)) {
|
||||
listener.OnTextUpdate(textEntry.Text, textEntry.Completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TextEntry GetTextEntry(string streamId) {
|
||||
@@ -57,7 +108,10 @@ namespace eagle {
|
||||
return new TextEntry(text, true);
|
||||
}
|
||||
|
||||
return _streamingTexts.GetValueOrDefault(streamId, null);
|
||||
lock (_lock) {
|
||||
_streamingTexts.TryGetValue(streamId, out var entry);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddListener(IClientTextListener listener) {
|
||||
@@ -70,4 +124,4 @@ namespace eagle {
|
||||
|
||||
public void RemoveListener(IClientTextListener listener) { _listeners.Remove(listener); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-37
@@ -224,13 +224,14 @@ namespace eagle {
|
||||
tp => tp.TypeId == battalionTypeId && tp.MeetsRequirements);
|
||||
}
|
||||
|
||||
private void MaybeActivateRow(EventBasedTable table, BattalionTypeId battalionTypeId) {
|
||||
private void MaybeActivateRow(
|
||||
EventBasedTable table,
|
||||
BattalionTypeId battalionTypeId,
|
||||
int[] extraTroopsByType) {
|
||||
var parent = table.gameObject.transform.parent;
|
||||
|
||||
var allowed = TypeIsAllowed(battalionTypeId) ||
|
||||
extraTroops.Where(tfb => tfb.type == battalionTypeId)
|
||||
.Select(tfb => tfb.count)
|
||||
.Sum() > 0;
|
||||
var allowed =
|
||||
TypeIsAllowed(battalionTypeId) || extraTroopsByType[(int)battalionTypeId] > 0;
|
||||
|
||||
table.gameObject.GetComponent<OrganizeExtrasTable>().Set(
|
||||
allowed,
|
||||
@@ -251,12 +252,12 @@ namespace eagle {
|
||||
parent.GetComponentInChildren<RawImage>().color = color;
|
||||
}
|
||||
|
||||
private void MaybeActivateRows() {
|
||||
MaybeActivateRow(lightInfantryTable, BattalionTypeId.LightInfantry);
|
||||
MaybeActivateRow(heavyInfantryTable, BattalionTypeId.HeavyInfantry);
|
||||
MaybeActivateRow(longbowmenTable, BattalionTypeId.Longbowmen);
|
||||
MaybeActivateRow(dragoonsTable, BattalionTypeId.LightCavalry);
|
||||
MaybeActivateRow(knightsTable, BattalionTypeId.HeavyCavalry);
|
||||
private void MaybeActivateRows(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);
|
||||
}
|
||||
|
||||
protected override void SetUpUI() {
|
||||
@@ -330,7 +331,8 @@ namespace eagle {
|
||||
} else
|
||||
return false;
|
||||
|
||||
eb.Update(existingBattalions);
|
||||
// Note: We don't call Update() here - the caller (UpdateTable or MaxClickedImpl)
|
||||
// will call it when needed. This avoids redundant recalculations.
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -367,7 +369,6 @@ 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,7 +465,8 @@ namespace eagle {
|
||||
} else
|
||||
return false;
|
||||
|
||||
newB.Update(existingBattalions);
|
||||
// Note: We don't call Update() here - the caller (UpdateTable or MaxClickedImpl)
|
||||
// will call it when needed. This avoids redundant recalculations.
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -639,8 +641,6 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public void UpdateTable() {
|
||||
battalionsTable.RowCount = 0;
|
||||
|
||||
maxAllButton.gameObject.SetActive(false);
|
||||
mergeButton.gameObject.SetActive(false);
|
||||
|
||||
@@ -653,23 +653,30 @@ namespace eagle {
|
||||
{ BattalionTypeId.Longbowmen, 0 }
|
||||
};
|
||||
|
||||
maxAllButton.gameObject.SetActive(false);
|
||||
foreach (var eb in existingBattalions) {
|
||||
if (eb.dismissed) continue;
|
||||
// Cache extra troop counts by type to avoid repeated LINQ queries
|
||||
// Use array indexed by enum value for O(1) access without hashing
|
||||
var battalionTypeCount = Enum.GetValues(typeof(BattalionTypeId)).Length;
|
||||
var extraTroopsByType = new int[battalionTypeCount];
|
||||
foreach (var et in extraTroops) { extraTroopsByType[(int)et.type] += et.count; }
|
||||
|
||||
var newRow = battalionsTable.AddRowWithComponent<OrganizeTroopsTableRow>();
|
||||
// Count total rows needed and set RowCount to reuse existing rows
|
||||
var activeExisting = existingBattalions.Where(eb => !eb.dismissed).ToList();
|
||||
var totalRows = activeExisting.Count + newBattalions.Count;
|
||||
battalionsTable.RowCount = totalRows;
|
||||
|
||||
int rowIndex = 0;
|
||||
foreach (var eb in activeExisting) {
|
||||
var row = battalionsTable.ComponentAt<OrganizeTroopsTableRow>(rowIndex);
|
||||
eb.Update(existingBattalions);
|
||||
newRow.BattalionInfo = eb;
|
||||
row.BattalionInfo = eb;
|
||||
|
||||
newRow.PlusButtonClickedCallback = () => PlusClicked(eb);
|
||||
newRow.MinusButtonClickedCallback = () => MinusClicked(eb);
|
||||
newRow.MaxButtonClickedCallback = () => MaxClicked(eb);
|
||||
newRow.DismissButtonClickedCallback = () => DismissClicked(eb);
|
||||
row.PlusButtonClickedCallback = () => PlusClicked(eb);
|
||||
row.MinusButtonClickedCallback = () => MinusClicked(eb);
|
||||
row.MaxButtonClickedCallback = () => MaxClicked(eb);
|
||||
row.DismissButtonClickedCallback = () => DismissClicked(eb);
|
||||
|
||||
bool canAugment =
|
||||
TypeIsAllowed(eb.TypeId) ||
|
||||
extraTroops.Where(tfb => tfb.type == eb.TypeId).Sum(tfb => tfb.count) > 0;
|
||||
newRow.CanAugment = canAugment;
|
||||
bool canAugment = TypeIsAllowed(eb.TypeId) || extraTroopsByType[(int)eb.TypeId] > 0;
|
||||
row.CanAugment = canAugment;
|
||||
|
||||
if (eb.Count < eb.Capacity) {
|
||||
// Enable MaxAll button if we could add new troops to this battalion type
|
||||
@@ -681,18 +688,19 @@ namespace eagle {
|
||||
mergeButton.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
foreach (var newB in newBattalions) {
|
||||
var newRow = battalionsTable.AddRowWithComponent<OrganizeTroopsTableRow>();
|
||||
var row = battalionsTable.ComponentAt<OrganizeTroopsTableRow>(rowIndex);
|
||||
|
||||
newB.Update(existingBattalions);
|
||||
newRow.BattalionInfo = newB;
|
||||
row.BattalionInfo = newB;
|
||||
|
||||
newRow.PlusButtonClickedCallback = () => PlusClicked(newB);
|
||||
newRow.MinusButtonClickedCallback = () => MinusClicked(newB);
|
||||
newRow.MaxButtonClickedCallback = () => MaxClicked(newB);
|
||||
newRow.DismissButtonClickedCallback = () => DismissClicked(newB);
|
||||
row.PlusButtonClickedCallback = () => PlusClicked(newB);
|
||||
row.MinusButtonClickedCallback = () => MinusClicked(newB);
|
||||
row.MaxButtonClickedCallback = () => MaxClicked(newB);
|
||||
row.DismissButtonClickedCallback = () => DismissClicked(newB);
|
||||
|
||||
if (newB.Count < newB.Capacity) {
|
||||
maxAllButton.gameObject.SetActive(true);
|
||||
@@ -701,6 +709,7 @@ namespace eagle {
|
||||
mergeButton.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
lightInfantryTable.RowCount = 0;
|
||||
@@ -739,15 +748,18 @@ 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.Update(existingBattalions).Size > 0) ||
|
||||
(newBattalions.Exists(
|
||||
b => b.newBattalion.NewTroops > 0 ||
|
||||
b.newBattalion.TroopsFromOtherBattalion.Count > 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();
|
||||
MaybeActivateRows(extraTroopsByType);
|
||||
}
|
||||
|
||||
public override AvailableCommand.SealedValueOneofCase CommandType =>
|
||||
|
||||
+14
-1
@@ -120,6 +120,19 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force the circuit breaker to allow an immediate reconnect attempt.
|
||||
/// Resets the state to HalfOpen so the next connection will be a test.
|
||||
/// </summary>
|
||||
public void ForceReconnect() {
|
||||
lock (this) {
|
||||
if (_state == State.Open) {
|
||||
_state = State.HalfOpen;
|
||||
_logger.LogLine("[CIRCUIT] OPEN → HALF_OPEN (forced by user)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get human-readable status for UI display.
|
||||
/// </summary>
|
||||
@@ -138,7 +151,7 @@ namespace eagle {
|
||||
var timeUntilTest = OpenTimeoutSeconds -
|
||||
(DateTime.UtcNow - _openedAt.Value).TotalSeconds;
|
||||
if (timeUntilTest > 0) {
|
||||
return $"Server unavailable. Retrying in {(int)timeUntilTest}s";
|
||||
return $"Server unavailable. Retrying in {Math.Min(10, (int)timeUntilTest)}s";
|
||||
}
|
||||
}
|
||||
return "Server unavailable. Testing...";
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
using System;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Provides game state information for the connection status UI.
|
||||
/// Implement this interface to show game-specific status when connected.
|
||||
/// </summary>
|
||||
public interface IGameStateProvider {
|
||||
/// <summary>Server-reported game status. Null if no status received yet.</summary>
|
||||
ServerGameStatus ServerStatus { get; }
|
||||
|
||||
/// <summary>True if a command was submitted and we're awaiting response (for >
|
||||
/// 500ms).</summary>
|
||||
bool IsProcessingCommand { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple UI component to display connection status and reconnection countdown.
|
||||
/// Attach to a TextMeshProUGUI component to display status.
|
||||
@@ -10,6 +25,10 @@ namespace eagle {
|
||||
public class ConnectionStatusUI : MonoBehaviour {
|
||||
private TextMeshProUGUI _textComponent;
|
||||
private PersistentClientConnection _connection;
|
||||
private IGameStateProvider _gameStateProvider;
|
||||
|
||||
[Tooltip("Optional button to force immediate reconnection when server is down")]
|
||||
public Button retryButton;
|
||||
|
||||
// Update interval in seconds
|
||||
private const float UpdateInterval = 0.5f;
|
||||
@@ -24,9 +43,10 @@ namespace eagle {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the PersistentClientConnection - this assumes it's accessible
|
||||
// In production, this would need proper dependency injection
|
||||
// For now, the connection will be set externally or found via another method
|
||||
if (retryButton != null) {
|
||||
retryButton.onClick.AddListener(OnRetryClicked);
|
||||
retryButton.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -36,6 +56,14 @@ namespace eagle {
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the game state provider for showing game-specific status.
|
||||
/// Call this when entering a game, clear it when leaving.
|
||||
/// </summary>
|
||||
public void SetGameStateProvider(IGameStateProvider provider) {
|
||||
_gameStateProvider = provider;
|
||||
}
|
||||
|
||||
void Update() {
|
||||
if (_connection == null || _textComponent == null) { return; }
|
||||
|
||||
@@ -50,11 +78,12 @@ namespace eagle {
|
||||
// Check circuit breaker state first - it takes precedence
|
||||
var circuitState = _connection.CircuitBreaker.CurrentState;
|
||||
if (circuitState == ConnectionCircuitBreaker.State.Open) {
|
||||
SetRetryButtonVisible(true);
|
||||
var nextTest = _connection.CircuitBreaker.NextTestAttempt;
|
||||
if (nextTest.HasValue) {
|
||||
var timeUntilTest = nextTest.Value - DateTime.UtcNow;
|
||||
if (timeUntilTest.TotalSeconds > 0) {
|
||||
int seconds = (int)Math.Ceiling(timeUntilTest.TotalSeconds);
|
||||
int seconds = Math.Min(10, (int)Math.Ceiling(timeUntilTest.TotalSeconds));
|
||||
_textComponent.text =
|
||||
$"<color=red>●</color> Server down. Retry in {seconds}s";
|
||||
return;
|
||||
@@ -63,6 +92,7 @@ namespace eagle {
|
||||
_textComponent.text = "<color=red>●</color> Server unavailable";
|
||||
return;
|
||||
} else if (circuitState == ConnectionCircuitBreaker.State.HalfOpen) {
|
||||
SetRetryButtonVisible(false);
|
||||
_textComponent.text = "<color=yellow>●</color> Testing connection...";
|
||||
return;
|
||||
}
|
||||
@@ -71,8 +101,13 @@ namespace eagle {
|
||||
var state = _connection.CurrentState;
|
||||
var nextAttempt = _connection.NextReconnectAttempt;
|
||||
|
||||
// Show retry button if we're counting down to a reconnect attempt
|
||||
bool isCountingDown = state == ConnectionState.Reconnecting && nextAttempt.HasValue &&
|
||||
(nextAttempt.Value - DateTime.UtcNow).TotalSeconds > 0;
|
||||
SetRetryButtonVisible(isCountingDown);
|
||||
|
||||
string statusText = state switch {
|
||||
ConnectionState.Connected => "<color=green>●</color> Connected",
|
||||
ConnectionState.Connected => GetConnectedStatusText(),
|
||||
ConnectionState.Connecting => "<color=yellow>●</color> Connecting...",
|
||||
ConnectionState.Disconnected => "<color=red>●</color> Disconnected",
|
||||
ConnectionState.Reconnecting => GetReconnectingText(nextAttempt),
|
||||
@@ -83,6 +118,34 @@ namespace eagle {
|
||||
_textComponent.text = statusText;
|
||||
}
|
||||
|
||||
private string GetConnectedStatusText() {
|
||||
// If no game state provider, just show "Connected"
|
||||
if (_gameStateProvider == null) { return "<color=green>●</color> Connected"; }
|
||||
|
||||
// Processing takes priority (client knows it submitted a command)
|
||||
if (_gameStateProvider.IsProcessingCommand) {
|
||||
return "<color=green>●</color> Processing...";
|
||||
}
|
||||
|
||||
// Use server-reported status
|
||||
var serverStatus = _gameStateProvider.ServerStatus;
|
||||
if (serverStatus == null) {
|
||||
// No server status yet - waiting for first response
|
||||
return "<color=green>●</color> Connected";
|
||||
}
|
||||
|
||||
return serverStatus.Status switch {
|
||||
ServerGameStatus.Types.Status.YourTurn => "<color=green>●</color> Your turn",
|
||||
ServerGameStatus.Types.Status.WaitingForPlayers =>
|
||||
"<color=green>●</color> Waiting for other players",
|
||||
ServerGameStatus.Types.Status.GeneratingText =>
|
||||
"<color=green>●</color> Generating...",
|
||||
ServerGameStatus.Types.Status.ProcessingAction =>
|
||||
"<color=green>●</color> Processing...",
|
||||
_ => "<color=green>●</color> Connected"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetReconnectingText(DateTime? nextAttempt) {
|
||||
if (!nextAttempt.HasValue) { return "<color=yellow>●</color> Reconnecting..."; }
|
||||
|
||||
@@ -91,8 +154,16 @@ namespace eagle {
|
||||
return "<color=yellow>●</color> Reconnecting...";
|
||||
}
|
||||
|
||||
int secondsRemaining = (int)Math.Ceiling(timeUntilRetry.TotalSeconds);
|
||||
int secondsRemaining = Math.Min(10, (int)Math.Ceiling(timeUntilRetry.TotalSeconds));
|
||||
return $"<color=orange>●</color> Retry in {secondsRemaining}s";
|
||||
}
|
||||
|
||||
private void SetRetryButtonVisible(bool visible) {
|
||||
if (retryButton != null) { retryButton.gameObject.SetActive(visible); }
|
||||
}
|
||||
|
||||
private void OnRetryClicked() {
|
||||
if (_connection != null) { _connection.ForceReconnect(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-6
@@ -165,6 +165,10 @@ namespace eagle {
|
||||
Model = null;
|
||||
chronicleCanvasController.Entries = new List<ChronicleEntry>();
|
||||
SetMusic();
|
||||
|
||||
// Clear game state provider when leaving game
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) { statusUI.SetGameStateProvider(null); }
|
||||
}
|
||||
|
||||
void MapControllerChangedTarget(List<ProvinceId> newTarget) {
|
||||
@@ -184,6 +188,9 @@ namespace eagle {
|
||||
}
|
||||
|
||||
void Update() {
|
||||
// Process batched streaming text updates (thread-safe, once per frame)
|
||||
ClientTextProvider.Provider.ProcessPendingUpdates();
|
||||
|
||||
ArrangeLayout();
|
||||
if (_newModel != null) { SwapModel(); }
|
||||
|
||||
@@ -222,12 +229,11 @@ namespace eagle {
|
||||
|
||||
void OnApplicationPause(bool pause) {
|
||||
if (ModelUpdater == null) { return; }
|
||||
if (pause) {
|
||||
ModelUpdater.StopListeningForUpdates();
|
||||
} else {
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
_ = ModelUpdater.StartListeningForUpdates();
|
||||
}
|
||||
// Don't unsubscribe on pause - this caused a race condition where the subscriber
|
||||
// could be lost if pause happened before the async StartListeningForUpdates completed.
|
||||
// With MainQueue rate-limiting, keeping the subscription during pause is safe.
|
||||
// Reconnects will continue to work, and updates will queue up and be processed on
|
||||
// resume.
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -263,6 +269,10 @@ namespace eagle {
|
||||
|
||||
ModelUpdater.ErrorHandler = errorHandler;
|
||||
|
||||
// Set up game state provider for connection status UI
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) { statusUI.SetGameStateProvider(ModelUpdater); }
|
||||
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
MainQueue.Q.EnqueueForNextUpdate(
|
||||
() => { _ = ModelUpdater.StartListeningForUpdates(); });
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace eagle {
|
||||
TokenId? CommandToken { get; }
|
||||
TokenId LastPostedToken { get; }
|
||||
|
||||
Dictionary<String, ShardokGameModel> ShardokGameModels { get; }
|
||||
ConcurrentDictionary<String, ShardokGameModel> ShardokGameModels { get; }
|
||||
|
||||
private bool ShardokGameModelIsRunning(ShardokGameModel sgm) =>
|
||||
sgm.GameStatus.State == GameStatus.Types.State.GameRunning
|
||||
@@ -62,7 +62,7 @@ namespace eagle {
|
||||
RollFetcher RollFetcher { get; }
|
||||
}
|
||||
|
||||
public class GameModelUpdater : IClientConnectionSubscriber {
|
||||
public class GameModelUpdater : IClientConnectionSubscriber, IGameStateProvider {
|
||||
private FactionId? PlayerId => _currentModel.PlayerId;
|
||||
public Int64? CurrentEagleToken => _currentModel.CommandToken;
|
||||
public Int64? CurrentShardokToken(string shardokGameId) {
|
||||
@@ -95,15 +95,22 @@ namespace eagle {
|
||||
|
||||
public long GameId { get; }
|
||||
|
||||
public int LastUnfilteredResultCount => _lastUnfilteredResultCount;
|
||||
public int LastUnfilteredResultCount {
|
||||
get {
|
||||
lock (_resultCountLock) { return _lastUnfilteredResultCount; }
|
||||
}
|
||||
}
|
||||
|
||||
public List<IClientConnectionSubscriber.ShardokViewStatus> ShardokViewStatuses =>
|
||||
_currentModel.ShardokGameModels
|
||||
.Select(sgm => {
|
||||
var needsResync = _shardokNeedsResync.GetValueOrDefault(sgm.Key, false);
|
||||
// Use thread-safe count from gRPC thread updates. Fall back to 0 if
|
||||
// not yet tracked (avoids accessing non-thread-safe History.Count).
|
||||
var count = _shardokResultCounts.GetValueOrDefault(sgm.Key, 0);
|
||||
return new IClientConnectionSubscriber.ShardokViewStatus {
|
||||
shardokGameId = sgm.Key,
|
||||
filteredResultCount = needsResync ? 0 : sgm.Value.History.Count(),
|
||||
filteredResultCount = needsResync ? 0 : count,
|
||||
requestFullResync = needsResync
|
||||
};
|
||||
})
|
||||
@@ -122,15 +129,27 @@ namespace eagle {
|
||||
|
||||
public ErrorHandler ErrorHandler;
|
||||
|
||||
// Thread-safe: updated from gRPC thread via UpdateResultCounts, read from main thread
|
||||
private int _lastUnfilteredResultCount = 0;
|
||||
private readonly object _resultCountLock = new();
|
||||
|
||||
private readonly Logger _connectionLogger = Logger.GetLogger("ConnectionLogger");
|
||||
|
||||
// Track when a command was submitted for "Processing..." display
|
||||
// Only show "Processing..." if command has been pending for > 500ms
|
||||
private DateTime? _commandSubmittedTime;
|
||||
private const double ProcessingDisplayDelayMs = 500.0;
|
||||
|
||||
// Track which Shardok games need full state resync after connection drop
|
||||
// Thread-safe: accessed from both connection thread and Unity main thread
|
||||
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
|
||||
@@ -179,7 +198,8 @@ namespace eagle {
|
||||
return new List<AvailableCommand>();
|
||||
}
|
||||
|
||||
public Dictionary<String, ShardokGameModel> ShardokGameModels { get; set; }
|
||||
// Thread-safe: accessed from heartbeat timer thread via ShardokViewStatuses
|
||||
public ConcurrentDictionary<String, ShardokGameModel> ShardokGameModels { get; set; }
|
||||
|
||||
public FactionView MaybeDestroyedFaction(FactionId factionId) {
|
||||
if (ActiveFactions.TryGetValue(factionId, out var factionView)) {
|
||||
@@ -221,7 +241,8 @@ namespace eagle {
|
||||
new Dictionary<ProvinceId, OneProvinceAvailableCommands>();
|
||||
|
||||
_currentModel.GsView = new GameStateView();
|
||||
_currentModel.ShardokGameModels = new Dictionary<ShardokGameId, ShardokGameModel>();
|
||||
_currentModel.ShardokGameModels =
|
||||
new ConcurrentDictionary<ShardokGameId, ShardokGameModel>();
|
||||
|
||||
_currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();
|
||||
|
||||
@@ -234,9 +255,9 @@ namespace eagle {
|
||||
|
||||
if (battleView == null) {
|
||||
// Battle was removed (e.g., it ended) before we could create the model.
|
||||
// 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)");
|
||||
// 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.
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -284,8 +305,18 @@ namespace eagle {
|
||||
|
||||
switch (updateItem.GameUpdateDetailsCase) {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
_lastUnfilteredResultCount =
|
||||
updateItem.ActionResultResponse.UnfilteredResultCountAfter;
|
||||
// Clear processing state - we received a response from the server
|
||||
_commandSubmittedTime = null;
|
||||
|
||||
// Store server-reported game status for UI
|
||||
if (updateItem.ActionResultResponse.ServerGameStatus != null) {
|
||||
ServerStatus = updateItem.ActionResultResponse.ServerGameStatus;
|
||||
}
|
||||
|
||||
// Note: _lastUnfilteredResultCount is updated on the gRPC thread in
|
||||
// UpdateResultCounts() before enqueueing. We don't update it here to avoid
|
||||
// race conditions where a backlogged MainQueue update overwrites a newer count.
|
||||
|
||||
if (updateItem.ActionResultResponse.ActionResultViews.Any() ||
|
||||
updateItem.ActionResultResponse.AvailableCommands == null ||
|
||||
updateItem.ActionResultResponse.AvailableCommands.Token !=
|
||||
@@ -311,7 +342,10 @@ namespace eagle {
|
||||
// Battle may have ended before we could create the model - remove
|
||||
// any stale reference and skip this update
|
||||
if (shardokGameModel == null) {
|
||||
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
|
||||
_currentModel.ShardokGameModels.TryRemove(
|
||||
oneResponse.ShardokGameId,
|
||||
out _);
|
||||
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -339,7 +373,10 @@ namespace eagle {
|
||||
shardokGameModel;
|
||||
} else {
|
||||
// Game ended - remove from active models so UI knows battle is over
|
||||
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
|
||||
_currentModel.ShardokGameModels.TryRemove(
|
||||
oneResponse.ShardokGameId,
|
||||
out _);
|
||||
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
|
||||
}
|
||||
}
|
||||
if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
|
||||
@@ -360,6 +397,29 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update result counts immediately when an update is received from the server.
|
||||
/// Called from the gRPC thread BEFORE enqueueing to MainQueue, to ensure
|
||||
/// reconnects use accurate counts even when MainQueue is blocked (e.g., backgrounded).
|
||||
/// </summary>
|
||||
public void UpdateResultCounts(GameUpdate update) {
|
||||
switch (update.GameUpdateDetailsCase) {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
lock (_resultCountLock) {
|
||||
_lastUnfilteredResultCount =
|
||||
update.ActionResultResponse.UnfilteredResultCountAfter;
|
||||
}
|
||||
break;
|
||||
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
|
||||
foreach (var response in update.ShardokActionResultResponse
|
||||
.ShardokGameResponses) {
|
||||
_shardokResultCounts[response.ShardokGameId] = response.NewResultViewCount;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to game updates. Returns true if subscription was acknowledged by server.
|
||||
/// </summary>
|
||||
@@ -405,6 +465,7 @@ namespace eagle {
|
||||
_currentModel.AvailableCommandsByProvince.Clear();
|
||||
_currentModel.CommandToken = null;
|
||||
_currentModel.LastPostedToken = token;
|
||||
_commandSubmittedTime = DateTime.UtcNow;
|
||||
return PersistentConnection.PostEagleCommand(
|
||||
gameId: GameId,
|
||||
token: token,
|
||||
@@ -424,6 +485,31 @@ namespace eagle {
|
||||
_currentModel.BattalionTypes =
|
||||
startingState.BattalionTypes.ToDictionary(bt => bt.TypeId, bt => bt);
|
||||
_currentModel.ChronicleEntries = startingState.ChronicleEntries.ToList();
|
||||
|
||||
// For any outstanding battles not in ShardokGameModels, create models and mark for
|
||||
// resync. This ensures fresh clients get Shardok state for ongoing battles.
|
||||
bool needsResubscribe = false;
|
||||
foreach (var battle in _currentModel.ShardokBattles) {
|
||||
if (!_currentModel.ShardokGameModels.ContainsKey(battle.ShardokGameId)) {
|
||||
var model = MakeGameModel(battle.ShardokGameId);
|
||||
if (model != null) {
|
||||
_currentModel.ShardokGameModels[battle.ShardokGameId] = model;
|
||||
MarkShardokForResync(battle.ShardokGameId);
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_RESYNC] Created ShardokGameModel for outstanding battle {battle.ShardokGameId}");
|
||||
needsResubscribe = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we created new ShardokGameModels, re-subscribe to request their full state.
|
||||
// The original subscribe didn't include these battles since we didn't know about them
|
||||
// yet.
|
||||
if (needsResubscribe) {
|
||||
_connectionLogger.LogLine(
|
||||
"[STATE_RESYNC] Re-subscribing to request full Shardok state for new battles");
|
||||
_ = StartListeningForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleUpdates(List<ActionResultView> results) {
|
||||
@@ -721,6 +807,15 @@ 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.TryRemove(rb, out _);
|
||||
}
|
||||
_shardokResultCounts.TryRemove(rb, out _);
|
||||
|
||||
for (int i = 0; i < _currentModel.ShardokBattles.Count; i++) {
|
||||
if (_currentModel.ShardokBattles[i].ShardokGameId == rb) {
|
||||
_currentModel.ShardokBattles.RemoveAt(i);
|
||||
@@ -766,5 +861,21 @@ namespace eagle {
|
||||
Notify(result);
|
||||
}
|
||||
}
|
||||
|
||||
#region IGameStateProvider implementation
|
||||
|
||||
/// <summary>Server-reported game status from the last ActionResultResponse.</summary>
|
||||
public ServerGameStatus ServerStatus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if a command was submitted and we're awaiting response.
|
||||
/// Only returns true if processing for > 500ms to avoid flashing.
|
||||
/// </summary>
|
||||
public bool IsProcessingCommand =>
|
||||
_commandSubmittedTime.HasValue &&
|
||||
(DateTime.UtcNow - _commandSubmittedTime.Value).TotalMilliseconds >
|
||||
ProcessingDisplayDelayMs;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -18,10 +18,11 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +60,14 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void OnTextUpdate(TextEntry entry) {
|
||||
if (entry != null) OnTextUpdate(entry.Text, entry.Completed);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnTextUpdate(string text, bool completed) {
|
||||
|
||||
+7
@@ -11,6 +11,13 @@ namespace eagle {
|
||||
|
||||
public void ReceiveGameUpdate(GameUpdate update);
|
||||
|
||||
/// <summary>
|
||||
/// Update the known result counts immediately when an update is received.
|
||||
/// Called from the gRPC thread BEFORE enqueueing to MainQueue, to ensure
|
||||
/// reconnects don't request stale data while MainQueue is blocked.
|
||||
/// </summary>
|
||||
public void UpdateResultCounts(GameUpdate update);
|
||||
|
||||
// Used for registering for stream updates
|
||||
struct ShardokViewStatus {
|
||||
public string shardokGameId;
|
||||
|
||||
+3
@@ -132,7 +132,10 @@ namespace eagle {
|
||||
|
||||
public void ProvinceHovered(ProvinceId? pid) {
|
||||
// Highlight moving armies table
|
||||
// Check row count to avoid index out of range if data changed after table was built
|
||||
var rowCount = movingArmiesTable.RowCount;
|
||||
MovingArmies.Each((army, i) => {
|
||||
if (i >= rowCount) return;
|
||||
var row = movingArmiesTable.ComponentAt<MovingArmyTableRow>(i);
|
||||
if (army.DestinationProvinceId == pid || army.OriginProvinceId == pid) {
|
||||
row.ShadeOn();
|
||||
|
||||
@@ -17,6 +17,10 @@ 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,
|
||||
@@ -54,7 +58,13 @@ 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));
|
||||
@@ -79,6 +89,8 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public void DismissAllClicked() {
|
||||
// Increment generation immediately so pending AddNote calls will skip
|
||||
_dismissGeneration++;
|
||||
_notes.Clear();
|
||||
SetPopupInfos();
|
||||
}
|
||||
|
||||
+35
-4
@@ -1,8 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
|
||||
namespace eagle.Notifications.ARNNotifications {
|
||||
using ProvinceId = System.Int32;
|
||||
using HeroId = System.Int32;
|
||||
|
||||
public static class ProfessionGainedDetailsNotificationGenerator {
|
||||
public static readonly ARNNotificationGenerator Generator = GenerateNotifications;
|
||||
|
||||
@@ -44,19 +48,46 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
: "Profession Gained";
|
||||
}
|
||||
|
||||
private static ProvinceId? FindProvinceForHero(HeroId heroId, IGameModel model) {
|
||||
foreach (var province in model.Provinces.Values) {
|
||||
if (province.FullInfo?.RulingFactionHeroIds.Contains(heroId) == true) {
|
||||
return province.Id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<Notification> GenerateNotifications(
|
||||
Net.Eagle0.Eagle.Common.Notification notification,
|
||||
IGameModel currentModel) {
|
||||
var details = notification.Details.ProfessionGainedDetails;
|
||||
var hero = currentModel.Heroes[details.HeroId];
|
||||
var factionName = currentModel.FactionName(details.FactionId);
|
||||
var professionName = GetProfessionName(details.NewProfession);
|
||||
var article = GetArticle(professionName);
|
||||
|
||||
var affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
string textTemplate;
|
||||
List<ProvinceId> affectedProvinces;
|
||||
|
||||
string textTemplate =
|
||||
$"{{Hero}} of {factionName} became {article} {professionName}.\n\n";
|
||||
if (details.FactionId == currentModel.PlayerId) {
|
||||
// Player's own hero - vary text based on faction leader status
|
||||
string heroDescription;
|
||||
if (hero.IsFactionLeader) {
|
||||
heroDescription =
|
||||
$"Your sworn {DisplayNames.SiblingDescription(hero.PronounGender)}";
|
||||
} else {
|
||||
heroDescription = "Your vassal";
|
||||
}
|
||||
textTemplate = $"{heroDescription} {{Hero}} became {article} {professionName}.\n\n";
|
||||
var heroProvince = FindProvinceForHero(details.HeroId, currentModel);
|
||||
affectedProvinces = heroProvince.HasValue
|
||||
? new List<ProvinceId> { heroProvince.Value }
|
||||
: currentModel.ProvincesForFaction(details.FactionId);
|
||||
} else {
|
||||
// Another faction's hero
|
||||
var factionName = currentModel.FactionName(details.FactionId);
|
||||
textTemplate = $"{{Hero}} of {factionName} became {article} {professionName}.\n\n";
|
||||
affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
}
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "Hero", (hero.NameTextId, "A hero") }
|
||||
|
||||
+7
-1
@@ -31,7 +31,13 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
};
|
||||
|
||||
if (playerId.HasValue && playerId.Value == paidToFactionId) {
|
||||
// no notification
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Accepted",
|
||||
textTemplate: $"We have accepted the ransom from {currentModel.FactionName(paidByFactionId)} for {{RansomedHero}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId>(),
|
||||
displayedHeroes: new List<HeroView> { ransomedHero, offeringFactionHead });
|
||||
} else if (playerId.HasValue && playerId.Value == paidByFactionId) {
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Accepted",
|
||||
|
||||
+13
@@ -9,6 +9,7 @@ namespace eagle.Notifications {
|
||||
private List<GeneratedTextListener> textListeners = new();
|
||||
private string textTemplate;
|
||||
private Dictionary<string, string> placeholderValues = new();
|
||||
private Dictionary<string, string> fallbackValues = new();
|
||||
|
||||
public DynamicTextNotification(
|
||||
string title,
|
||||
@@ -72,6 +73,9 @@ namespace eagle.Notifications {
|
||||
string nameTextId = kvp.Value.nameTextId;
|
||||
string fallback = kvp.Value.fallback;
|
||||
|
||||
// Store fallback so UpdateText can use it for missing placeholders
|
||||
fallbackValues[placeholder] = fallback;
|
||||
|
||||
if (!string.IsNullOrEmpty(nameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
nameTextId,
|
||||
@@ -95,6 +99,15 @@ namespace eagle.Notifications {
|
||||
|
||||
private void UpdateText() {
|
||||
string result = textTemplate;
|
||||
|
||||
// First apply fallbacks for any placeholder without a loaded value
|
||||
foreach (var kvp in fallbackValues) {
|
||||
if (!placeholderValues.ContainsKey(kvp.Key)) {
|
||||
result = result.Replace($"{{{kvp.Key}}}", kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Then apply actual loaded values
|
||||
foreach (var kvp in placeholderValues) {
|
||||
result = result.Replace($"{{{kvp.Key}}}", kvp.Value);
|
||||
}
|
||||
|
||||
+5
@@ -102,6 +102,11 @@ namespace eagle.Notifications {
|
||||
}
|
||||
|
||||
public void Append(string text, List<ProvinceId> provinceIds) {
|
||||
// Skip if this exact text is already in the notification (prevents duplicates
|
||||
// when the same update is processed multiple times, e.g., after resuming from
|
||||
// background)
|
||||
if (Text.Contains(text)) { return; }
|
||||
|
||||
if (ShouldAppend) {
|
||||
Text += "\n" + text;
|
||||
ProvinceIds.AddRange(provinceIds);
|
||||
|
||||
+165
-8
@@ -46,6 +46,8 @@ namespace eagle {
|
||||
private readonly Logger _timingsLogger = Logger.GetLogger("ConnectionLogger");
|
||||
private volatile bool _isConnecting = false;
|
||||
private Timer _idleCheckTimer = null;
|
||||
private Timer _heartbeatTimer = null;
|
||||
private const double HeartbeatIntervalSeconds = 10.0;
|
||||
|
||||
// Connection metrics for diagnostics
|
||||
private DateTime? _lastConnectAttempt = null;
|
||||
@@ -67,6 +69,17 @@ namespace eagle {
|
||||
private ConnectionCircuitBreaker _circuitBreaker = new ConnectionCircuitBreaker();
|
||||
public ConnectionCircuitBreaker CircuitBreaker => _circuitBreaker;
|
||||
|
||||
/// <summary>
|
||||
/// Force an immediate reconnection attempt, bypassing the circuit breaker timeout.
|
||||
/// </summary>
|
||||
public void ForceReconnect() {
|
||||
_circuitBreaker.ForceReconnect();
|
||||
_retryTimer?.Dispose();
|
||||
NextReconnectAttempt = null;
|
||||
LogConnectionEvent("force_reconnect", "User requested immediate reconnect");
|
||||
Task.Run(() => Connect());
|
||||
}
|
||||
|
||||
private DateTime? GetDeadlineFromNow() {
|
||||
return DateTime.UtcNow.AddSeconds(TimeoutSeconds);
|
||||
}
|
||||
@@ -224,12 +237,10 @@ namespace eagle {
|
||||
$"[SUBSCRIBE] Subscription confirmed for game {gameId}, " +
|
||||
$"confirmedResultCount={ack.ConfirmedResultCount}");
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
|
||||
return true;
|
||||
} else {
|
||||
@@ -363,6 +374,9 @@ namespace eagle {
|
||||
// Start monitoring for idle timeout (relies on HTTP/2 keepalive)
|
||||
StartIdleCheckTimer();
|
||||
|
||||
// Start sending heartbeats with sync status
|
||||
StartHeartbeatTimer();
|
||||
|
||||
await TryPendingCommands();
|
||||
} catch (Exception e) {
|
||||
_circuitBreaker.RecordFailure();
|
||||
@@ -482,6 +496,7 @@ namespace eagle {
|
||||
_lastDisconnect = DateTime.UtcNow;
|
||||
LogConnectionEvent("disconnect_explicit");
|
||||
StopIdleCheckTimer();
|
||||
StopHeartbeatTimer();
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
}
|
||||
@@ -491,6 +506,7 @@ namespace eagle {
|
||||
lock (this) {
|
||||
// Dispose timers first to stop any pending callbacks
|
||||
StopIdleCheckTimer();
|
||||
StopHeartbeatTimer();
|
||||
|
||||
if (_retryTimer != null) {
|
||||
_retryTimer.Enabled = false;
|
||||
@@ -696,9 +712,32 @@ namespace eagle {
|
||||
});
|
||||
break;
|
||||
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.StreamingTextResponse:
|
||||
// Handle streaming text directly on gRPC thread - dictionary is thread-safe.
|
||||
// Listeners are notified later via ProcessPendingUpdates() on main thread.
|
||||
var str = gameUpdate.StreamingTextResponse;
|
||||
if (str != null) {
|
||||
ClientTextProvider.Provider.HandleNewStreamingText(
|
||||
str.LlmIdentifier,
|
||||
str.NewText,
|
||||
str.StartingByteCount,
|
||||
str.Completed);
|
||||
}
|
||||
// No MainQueue enqueue needed - ProcessPendingUpdates handles listener
|
||||
// notification
|
||||
break;
|
||||
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.StreamingTextResponse:
|
||||
// Update result counts IMMEDIATELY on the gRPC thread, before enqueueing.
|
||||
// This ensures reconnects use accurate counts even when MainQueue is blocked
|
||||
// (e.g., when Unity is backgrounded).
|
||||
lock (this) {
|
||||
if (_subscribers.TryGetValue(gameUpdate.GameId, out var sub)) {
|
||||
sub.UpdateResultCounts(gameUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
MainQueue.Q.Enqueue(async () => {
|
||||
IClientConnectionSubscriber subscriber;
|
||||
lock (this) {
|
||||
@@ -743,7 +782,7 @@ namespace eagle {
|
||||
|
||||
switch (current.ResponseDetailsCase) {
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.HeartbeatResponse:
|
||||
_remoteEagleClientLogger.LogLine("Got a heartbeat response!");
|
||||
HandleHeartbeatResponse(current.HeartbeatResponse);
|
||||
break;
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.GameUpdate:
|
||||
@@ -978,5 +1017,123 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartHeartbeatTimer() {
|
||||
StopHeartbeatTimer();
|
||||
_heartbeatTimer =
|
||||
new Timer { AutoReset = true, Interval = HeartbeatIntervalSeconds * 1000 };
|
||||
_heartbeatTimer.Elapsed += (sender, args) => Task.Run(() => SendHeartbeat());
|
||||
_heartbeatTimer.Enabled = true;
|
||||
}
|
||||
|
||||
private void StopHeartbeatTimer() {
|
||||
if (_heartbeatTimer != null) {
|
||||
_heartbeatTimer.Enabled = false;
|
||||
_heartbeatTimer.Dispose();
|
||||
_heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendHeartbeat() {
|
||||
if (_cancellationToken.IsCancellationRequested) { return; }
|
||||
if (_currentState != ConnectionState.Connected) { return; }
|
||||
|
||||
// Build sync status for all subscribed games
|
||||
var heartbeatRequest = new HeartbeatRequest {
|
||||
ClientTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
|
||||
List<IClientConnectionSubscriber> subscribers;
|
||||
lock (this) { subscribers = _subscribers.Values.ToList(); }
|
||||
|
||||
foreach (var subscriber in subscribers) {
|
||||
var gameSyncStatus = new GameSyncStatus {
|
||||
GameId = subscriber.GameId,
|
||||
UnfilteredResultCount = subscriber.LastUnfilteredResultCount
|
||||
};
|
||||
|
||||
foreach (var shardokStatus in subscriber.ShardokViewStatuses) {
|
||||
gameSyncStatus.ShardokSyncStatuses.Add(new ShardokSyncStatus {
|
||||
ShardokGameId = shardokStatus.shardokGameId,
|
||||
FilteredResultCount = shardokStatus.filteredResultCount
|
||||
});
|
||||
}
|
||||
|
||||
heartbeatRequest.GameSyncStatuses.Add(gameSyncStatus);
|
||||
}
|
||||
|
||||
var request = new UpdateStreamRequest { HeartbeatRequest = heartbeatRequest };
|
||||
|
||||
var sent = await SendUpdateStreamRequestAsync(request);
|
||||
if (sent) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[HEARTBEAT] Sent heartbeat with {heartbeatRequest.GameSyncStatuses.Count} games");
|
||||
}
|
||||
}
|
||||
|
||||
// Grace period after connect before sync mismatch triggers reconnect.
|
||||
// Allows time to receive and process historical results after fresh subscribe.
|
||||
private const double SyncMismatchGracePeriodSeconds = 60.0;
|
||||
|
||||
private void HandleHeartbeatResponse(HeartbeatResponse response) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[HEARTBEAT] Got response, server_timestamp={response.ServerTimestamp}");
|
||||
|
||||
// Check for sync mismatches reported by server
|
||||
bool hasMismatch = false;
|
||||
foreach (var syncResult in response.GameSyncResults) {
|
||||
if (!syncResult.EagleInSync) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}: Eagle out of sync, " +
|
||||
$"server has {syncResult.ServerUnfilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_eagle",
|
||||
$"game={syncResult.GameId}, server_count={syncResult.ServerUnfilteredResultCount}");
|
||||
hasMismatch = true;
|
||||
}
|
||||
|
||||
foreach (var shardokResult in syncResult.ShardokSyncResults) {
|
||||
if (!shardokResult.InSync) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}, Shardok {shardokResult.ShardokGameId}: " +
|
||||
$"out of sync, server has {shardokResult.ServerFilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_shardok",
|
||||
$"game={syncResult.GameId}, shardok={shardokResult.ShardokGameId}, " +
|
||||
$"server_count={shardokResult.ServerFilteredResultCount}");
|
||||
hasMismatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMismatch) return;
|
||||
|
||||
// Grace period after connect - allow time to receive historical results
|
||||
if (_lastSuccessfulConnect.HasValue) {
|
||||
var timeSinceConnect =
|
||||
(DateTime.UtcNow - _lastSuccessfulConnect.Value).TotalSeconds;
|
||||
if (timeSinceConnect < SyncMismatchGracePeriodSeconds) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Ignoring mismatch during grace period ({timeSinceConnect:F1}s < {SyncMismatchGracePeriodSeconds}s)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger resync by reconnecting - this will re-subscribe with current counts
|
||||
// and the server will send missing updates
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"[SYNC_MISMATCH] Detected sync mismatch, triggering reconnect to resync");
|
||||
LogConnectionEvent("sync_mismatch_reconnect", "Triggering reconnect to resync");
|
||||
|
||||
// Schedule reconnect to resync
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
}
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
ScheduleReconnect("SyncMismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,21 @@
|
||||
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;
|
||||
|
||||
// Track queue depth for logging
|
||||
private int _lastLoggedQueueDepth = 0;
|
||||
|
||||
private MainQueue() {}
|
||||
|
||||
void Awake() {
|
||||
@@ -15,6 +24,29 @@ public class MainQueue : MonoBehaviour {
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {
|
||||
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) {
|
||||
Debug.Log($"[MainQueue] Processing backlog: {queueDepthBefore} actions queued");
|
||||
_lastLoggedQueueDepth = queueDepthBefore;
|
||||
} else if (queueDepthBefore <= 100) {
|
||||
_lastLoggedQueueDepth = 0;
|
||||
}
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
Action possibleAction;
|
||||
do {
|
||||
possibleAction = null;
|
||||
@@ -23,7 +55,7 @@ public class MainQueue : MonoBehaviour {
|
||||
}
|
||||
|
||||
if (possibleAction != null) { possibleAction.Invoke(); }
|
||||
} while (possibleAction != null);
|
||||
} while (possibleAction != null && stopwatch.ElapsedMilliseconds < MaxMillisecondsPerFrame);
|
||||
|
||||
lock (_nextUpdateQueue) {
|
||||
foreach (Action action in _nextUpdateQueue) { Enqueue(action); }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using common;
|
||||
using eagle;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using TMPro;
|
||||
@@ -27,6 +28,9 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
public Toggle tooltipHugsCursorToggle;
|
||||
public HoveringTooltip hoveringTooltip;
|
||||
|
||||
public Slider autoScrollSpeedSlider;
|
||||
public TMP_Text autoScrollSpeedLabel;
|
||||
|
||||
public HexGrid hexGrid;
|
||||
|
||||
private bool _active = false;
|
||||
@@ -54,6 +58,11 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
|
||||
tooltipHugsCursorToggle.isOn = PlayerPrefs.GetInt(TooltipHugsCursorKey, 0) == 1;
|
||||
hoveringTooltip.HugsCursor = tooltipHugsCursorToggle.isOn;
|
||||
|
||||
if (autoScrollSpeedSlider != null) {
|
||||
autoScrollSpeedSlider.value = AutoScrollingText.GlobalScrollSpeedMultiplier;
|
||||
UpdateAutoScrollSpeedLabel(autoScrollSpeedSlider.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
@@ -107,6 +116,21 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
hoveringTooltip.HugsCursor = val;
|
||||
}
|
||||
|
||||
public void OnAutoScrollSpeedSliderChange(float val) {
|
||||
AutoScrollingText.GlobalScrollSpeedMultiplier = val;
|
||||
UpdateAutoScrollSpeedLabel(val);
|
||||
}
|
||||
|
||||
private void UpdateAutoScrollSpeedLabel(float val) {
|
||||
if (autoScrollSpeedLabel != null) {
|
||||
if (val < 0.01f) {
|
||||
autoScrollSpeedLabel.text = "Paused";
|
||||
} else {
|
||||
autoScrollSpeedLabel.text = $"{val:F2}x";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnResourcesFolderClick() {
|
||||
Process.Start(Path.Combine(Application.persistentDataPath, "eagle0", "Resources"));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ 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();
|
||||
|
||||
+95
-95
@@ -385,98 +385,99 @@ namespace Shardok {
|
||||
Model.MyUncommittedUnits.Where(uv => uv.Location.Row == -1).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the ShardokGameModel is updated. This is invoked from UpdateAction,
|
||||
/// which is called from ShardokGameModel.HandleUpdates, which runs on MainQueue.
|
||||
/// No need to re-enqueue - we're already on the main thread.
|
||||
/// </summary>
|
||||
void ModelUpdated() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (Model == null) { return; }
|
||||
SetHeroLabels();
|
||||
SetModifiers();
|
||||
UpdateReserves();
|
||||
if (Model == null) { return; }
|
||||
SetHeroLabels();
|
||||
SetModifiers();
|
||||
UpdateReserves();
|
||||
|
||||
HandleEnemyStartingPositionOverlays();
|
||||
HandleEnemyStartingPositionOverlays();
|
||||
|
||||
endTurnButton.interactable = false;
|
||||
endTurnButton.interactable = false;
|
||||
|
||||
if (Model.GameStatus != null &&
|
||||
(Model.GameStatus.State == GameStatus.Types.State.Victory)) {
|
||||
turnStatusLabel.text = "Game Over!";
|
||||
if (Model.GameStatus != null &&
|
||||
(Model.GameStatus.State == GameStatus.Types.State.Victory)) {
|
||||
turnStatusLabel.text = "Game Over!";
|
||||
|
||||
gameOverText.text = Model.GameStatus.Description;
|
||||
gameOverCanvas.gameObject.SetActive(true);
|
||||
gameOverText.text = Model.GameStatus.Description;
|
||||
gameOverCanvas.gameObject.SetActive(true);
|
||||
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Back to Eagle";
|
||||
endTurnButton.interactable = true;
|
||||
} else if (Model.GameStatus != null && Model.MyTurn) {
|
||||
gameOverCanvas.gameObject.SetActive(false);
|
||||
turnStatusLabel.text = "Your Turn";
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Back to Eagle";
|
||||
endTurnButton.interactable = true;
|
||||
} else if (Model.GameStatus != null && Model.MyTurn) {
|
||||
gameOverCanvas.gameObject.SetActive(false);
|
||||
turnStatusLabel.text = "Your Turn";
|
||||
|
||||
if (Model.InSetUp) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Commit";
|
||||
if (Model.InSetUp) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Commit";
|
||||
|
||||
var unplacedUnitsWithLocations =
|
||||
Model.MyUncommittedUnits
|
||||
.Where(u => u.Location.Row >= 0 && u.Location.Column >= 0)
|
||||
.ToList();
|
||||
var unplacedUnitsWithLocations =
|
||||
Model.MyUncommittedUnits
|
||||
.Where(u => u.Location.Row >= 0 && u.Location.Column >= 0)
|
||||
.ToList();
|
||||
|
||||
if (!Model.MyTurn) {
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() < 1) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text =
|
||||
"No units placed";
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() > 10) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text =
|
||||
"Too many units";
|
||||
endTurnButton.interactable = false;
|
||||
} else {
|
||||
endTurnButton.interactable = true;
|
||||
}
|
||||
|
||||
SetDisplayedCommandGroup(0);
|
||||
if (!Model.MyTurn) {
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() < 1) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "No units placed";
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() > 10) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Too many units";
|
||||
endTurnButton.interactable = false;
|
||||
} else {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
|
||||
}
|
||||
|
||||
if (Model.HasAvailableCommandWhere(
|
||||
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
|
||||
CommandTypeUIManager.EndTurnCommandGroup)) {
|
||||
endTurnButton.interactable = true;
|
||||
}
|
||||
|
||||
SelectAppropriateDefaultCommand();
|
||||
SetDisplayedCommandGroup(0);
|
||||
} else {
|
||||
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
|
||||
}
|
||||
|
||||
locationNameText.text = Model.LocationName;
|
||||
|
||||
if (Model.History.Count > 0) {
|
||||
string monthString = new DateTime(777, Model.Month, 1)
|
||||
.ToString("MMMM", CultureInfo.InvariantCulture);
|
||||
roundInfoText.text = $"{monthString} {Model.CurrentRound}";
|
||||
Weather weather = Model.Weather;
|
||||
if (weather != null) {
|
||||
roundInfoText.text += ", " + ProtoExtensions.WeatherToString(weather);
|
||||
}
|
||||
if (Model.HasAvailableCommandWhere(
|
||||
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
|
||||
CommandTypeUIManager.EndTurnCommandGroup)) {
|
||||
endTurnButton.interactable = true;
|
||||
}
|
||||
|
||||
if (Model.History.Count == 0) {
|
||||
turnHistoryButtonText.text = NoHistoryText;
|
||||
} else if (Model.History.Count > _lastRetrievedHistoryCount) {
|
||||
for (int i = _lastRetrievedHistoryCount; i < Model.History.Count; i++) {
|
||||
var historyEntry = Model.History[i];
|
||||
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
|
||||
SelectAppropriateDefaultCommand();
|
||||
} else {
|
||||
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
|
||||
}
|
||||
|
||||
ActionType type = historyEntry.Type;
|
||||
var thisSound = soundManager.SoundForType(type);
|
||||
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
|
||||
}
|
||||
_lastRetrievedHistoryCount = Model.History.Count;
|
||||
turnHistoryButtonText.text = GetActionResultDescription(Model.History.Last());
|
||||
SetModifiers();
|
||||
locationNameText.text = Model.LocationName;
|
||||
|
||||
if (Model.History.Count > 0) {
|
||||
string monthString = new DateTime(777, Model.Month, 1)
|
||||
.ToString("MMMM", CultureInfo.InvariantCulture);
|
||||
roundInfoText.text = $"{monthString} {Model.CurrentRound}";
|
||||
Weather weather = Model.Weather;
|
||||
if (weather != null) {
|
||||
roundInfoText.text += ", " + ProtoExtensions.WeatherToString(weather);
|
||||
}
|
||||
}
|
||||
|
||||
SetupArmiesTable();
|
||||
});
|
||||
if (Model.History.Count == 0) {
|
||||
turnHistoryButtonText.text = NoHistoryText;
|
||||
} else if (Model.History.Count > _lastRetrievedHistoryCount) {
|
||||
for (int i = _lastRetrievedHistoryCount; i < Model.History.Count; i++) {
|
||||
var historyEntry = Model.History[i];
|
||||
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
|
||||
|
||||
ActionType type = historyEntry.Type;
|
||||
var thisSound = soundManager.SoundForType(type);
|
||||
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
|
||||
}
|
||||
_lastRetrievedHistoryCount = Model.History.Count;
|
||||
turnHistoryButtonText.text = GetActionResultDescription(Model.History.Last());
|
||||
SetModifiers();
|
||||
}
|
||||
|
||||
SetupArmiesTable();
|
||||
}
|
||||
|
||||
private void SetupArmiesTable() {
|
||||
@@ -707,34 +708,33 @@ namespace Shardok {
|
||||
}
|
||||
|
||||
void SetModifiers() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
hexGrid.ClearCellModifierImages();
|
||||
// Called from ModelUpdated which runs on MainQueue - no need to re-enqueue
|
||||
hexGrid.ClearCellModifierImages();
|
||||
|
||||
for (byte row = 0; row < Model.Map.RowCount; row++) {
|
||||
for (byte column = 0; column < Model.Map.ColumnCount; column++) {
|
||||
Coords coords = new Coords();
|
||||
coords.Row = row;
|
||||
coords.Column = column;
|
||||
for (byte row = 0; row < Model.Map.RowCount; row++) {
|
||||
for (byte column = 0; column < Model.Map.ColumnCount; column++) {
|
||||
Coords coords = new Coords();
|
||||
coords.Row = row;
|
||||
coords.Column = column;
|
||||
|
||||
var terrain = Model.Map.TerrainAt(coords);
|
||||
int cellIndex = MapCoordsToGridIndex(coords);
|
||||
int numberForCell = _randomNumberForCellIndex[cellIndex];
|
||||
hexGrid.SetCellTerrainImage(
|
||||
cellIndex,
|
||||
_imageForTerrainTracker
|
||||
.GetImageForTerrain(terrain, numberForCell, Model.Month));
|
||||
if (terrain.Modifier?.Fire != null) {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, fireEffectPrefab);
|
||||
} else {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, null);
|
||||
}
|
||||
var terrain = Model.Map.TerrainAt(coords);
|
||||
int cellIndex = MapCoordsToGridIndex(coords);
|
||||
int numberForCell = _randomNumberForCellIndex[cellIndex];
|
||||
hexGrid.SetCellTerrainImage(
|
||||
cellIndex,
|
||||
_imageForTerrainTracker
|
||||
.GetImageForTerrain(terrain, numberForCell, Model.Month));
|
||||
if (terrain.Modifier?.Fire != null) {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, fireEffectPrefab);
|
||||
} else {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, null);
|
||||
}
|
||||
|
||||
if (terrain.Modifier?.Bridge != null) {
|
||||
hexGrid.SetCellModifierImage(cellIndex, bridgeImage);
|
||||
}
|
||||
if (terrain.Modifier?.Bridge != null) {
|
||||
hexGrid.SetCellModifierImage(cellIndex, bridgeImage);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void HandleButton() {
|
||||
@@ -961,7 +961,7 @@ namespace Shardok {
|
||||
commandTypeUIManager.CommandGroupForType(command.Type));
|
||||
}
|
||||
|
||||
if (allCommands.Any()) {
|
||||
if (allCommands.Any() && mapMouseCoords != null) {
|
||||
var meleeCommands = allCommands.Where(
|
||||
command => CommandTypeUIManager.MeleeAttackGroup ==
|
||||
commandTypeUIManager.CommandGroupForType(command.Type));
|
||||
|
||||
@@ -82,7 +82,7 @@ public class ShardokGameModel {
|
||||
public int Month { get; private set; }
|
||||
private List<PlayerTotals> PlayerTotals { get; set; }
|
||||
|
||||
public List<CommandDescriptor> AvailableCommands { get; private set; }
|
||||
public List<CommandDescriptor> AvailableCommands { get; private set; } = new();
|
||||
public Action UpdateAction { get; set; }
|
||||
public List<PlayerWithHostility> players;
|
||||
public String GetPlayerName(PlayerId playerId) {
|
||||
@@ -111,6 +111,17 @@ 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;
|
||||
@@ -206,6 +217,8 @@ public class ShardokGameModel {
|
||||
? newCommands.CurrentCommand.ToList()
|
||||
: newCommands.PreviewCommand.ToList();
|
||||
}
|
||||
// Trigger UI refresh after commands are updated so unit action indicators reflect new state
|
||||
UpdateAction?.Invoke();
|
||||
}
|
||||
|
||||
public bool HasTargetedCommand(Coords start, Coords target, List<CommandType> possibleTypes) {
|
||||
|
||||
+36
-9
File diff suppressed because one or more lines are too long
+278
@@ -0,0 +1,278 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace common {
|
||||
/// <summary>
|
||||
/// Automatically scrolls text content that overflows its container.
|
||||
/// Shows a fade gradient at the bottom when content overflows, then begins
|
||||
/// auto-scrolling after a delay. Useful for tooltips where the user can't
|
||||
/// manually scroll.
|
||||
///
|
||||
/// Also supports dynamic height sizing: the ScrollRect will grow to fit its
|
||||
/// content up to available screen space, only enabling scrolling when needed.
|
||||
/// </summary>
|
||||
public class AutoScrollingText : MonoBehaviour {
|
||||
/// <summary>
|
||||
/// Global scroll speed multiplier controlled by Settings.
|
||||
/// Default is 1.0 (use instance scrollSpeed as-is).
|
||||
/// Range: 0.0 (paused) to 2.0 (double speed).
|
||||
/// </summary>
|
||||
public static float GlobalScrollSpeedMultiplier {
|
||||
get => PlayerPrefs.GetFloat(ScrollSpeedKey, 1.0f);
|
||||
set => PlayerPrefs.SetFloat(ScrollSpeedKey, Mathf.Clamp(value, 0f, 2f));
|
||||
}
|
||||
|
||||
private const string ScrollSpeedKey = "autoScrollSpeedMultiplier";
|
||||
|
||||
[Header("Scroll Rect")]
|
||||
[Tooltip("The ScrollRect containing the text content")]
|
||||
public ScrollRect scrollRect;
|
||||
|
||||
[Tooltip("Optional gradient image to show when content overflows (fades at bottom)")]
|
||||
public GameObject fadeGradient;
|
||||
|
||||
[Header("Auto-Scroll Timing")]
|
||||
[Tooltip("Seconds to wait before starting to scroll")]
|
||||
public float scrollDelaySeconds = 1.5f;
|
||||
|
||||
[Tooltip("Scroll speed in normalized units per second (0-1 range)")]
|
||||
public float scrollSpeed = 0.15f;
|
||||
|
||||
[Tooltip("Seconds to pause at the bottom before resetting")]
|
||||
public float pauseAtBottomSeconds = 1.0f;
|
||||
|
||||
[Header("Dynamic Sizing")]
|
||||
[Tooltip("If true, resize the ScrollRect to fit content up to available space")]
|
||||
public bool dynamicHeight = true;
|
||||
|
||||
[Tooltip("Margin from top of screen in pixels")]
|
||||
public float topMargin = 20f;
|
||||
|
||||
[Tooltip("LayoutElement to adjust for dynamic height (usually on ScrollRect)")]
|
||||
public LayoutElement layoutElement;
|
||||
|
||||
[Tooltip("Other content in the panel (e.g., hero details). Height will be preserved.")]
|
||||
public RectTransform otherContent;
|
||||
|
||||
[Tooltip("Optional: Panel to hide until layout is complete (prevents jumpy resize)")]
|
||||
public CanvasGroup panelCanvasGroup;
|
||||
|
||||
private float _visibleTime = 0f;
|
||||
private bool _isScrolling = false;
|
||||
private bool _isPausedAtBottom = false;
|
||||
private float _pauseTimer = 0f;
|
||||
private RectTransform _scrollRectTransform;
|
||||
private Canvas _rootCanvas;
|
||||
|
||||
private void OnEnable() {
|
||||
// Hide panel until layout is complete to prevent jumpy resize
|
||||
if (panelCanvasGroup != null) { panelCanvasGroup.alpha = 0f; }
|
||||
|
||||
if (scrollRect != null) {
|
||||
_scrollRectTransform = scrollRect.GetComponent<RectTransform>();
|
||||
_rootCanvas = scrollRect.GetComponentInParent<Canvas>()?.rootCanvas;
|
||||
|
||||
// Immediately reset scroll position to prevent visual jump on first frame
|
||||
// Use normalizedPosition to reset both horizontal and vertical
|
||||
scrollRect.normalizedPosition = new Vector2(0f, 1f);
|
||||
|
||||
// Also reset the content's anchored position to prevent any offset
|
||||
if (scrollRect.content != null) {
|
||||
var contentRect = scrollRect.content;
|
||||
contentRect.anchoredPosition = new Vector2(0f, contentRect.anchoredPosition.y);
|
||||
}
|
||||
}
|
||||
|
||||
// Delay layout update to next frame so content has time to rebuild
|
||||
StartCoroutine(DelayedInitialize());
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator DelayedInitialize() {
|
||||
yield return null; // Wait one frame for layout to rebuild
|
||||
yield return null; // Extra frame for ContentSizeFitter
|
||||
|
||||
UpdateDynamicHeight();
|
||||
ResetScroll();
|
||||
|
||||
// Show panel now that layout is complete
|
||||
if (panelCanvasGroup != null) { panelCanvasGroup.alpha = 1f; }
|
||||
}
|
||||
|
||||
private void OnDisable() { ResetScroll(); }
|
||||
|
||||
/// <summary>
|
||||
/// Resets scroll position to top and restarts the delay timer.
|
||||
/// Call this when the text content changes.
|
||||
/// </summary>
|
||||
public void ResetScroll() {
|
||||
_visibleTime = 0f;
|
||||
_isScrolling = false;
|
||||
_isPausedAtBottom = false;
|
||||
_pauseTimer = 0f;
|
||||
|
||||
if (scrollRect != null) {
|
||||
scrollRect.verticalNormalizedPosition = 1f; // Top
|
||||
}
|
||||
|
||||
UpdateFadeGradient();
|
||||
}
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging to console")]
|
||||
public bool debugLogging = false;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the ScrollRect height to fit content, up to available screen space.
|
||||
/// </summary>
|
||||
private void UpdateDynamicHeight() {
|
||||
if (!dynamicHeight || layoutElement == null || scrollRect == null) return;
|
||||
if (_scrollRectTransform == null || _rootCanvas == null) return;
|
||||
|
||||
// Get content height - prefer TMP_Text.preferredHeight over rect.height
|
||||
// because rect.height may not reflect actual text size without ContentSizeFitter
|
||||
float contentHeight = 0f;
|
||||
float rectHeight = scrollRect.content != null ? scrollRect.content.rect.height : 0f;
|
||||
var tmpText = scrollRect.content?.GetComponentInChildren<TMP_Text>();
|
||||
if (tmpText != null) {
|
||||
// Use TMP's calculated preferred height - this is the actual text size
|
||||
contentHeight = tmpText.preferredHeight;
|
||||
|
||||
// Sync Content RectTransform height with actual text size.
|
||||
// Must both grow AND shrink to prevent blank space when scrolling to bottom.
|
||||
if (Mathf.Abs(scrollRect.content.rect.height - contentHeight) > 1f) {
|
||||
scrollRect.content.SetSizeWithCurrentAnchors(
|
||||
RectTransform.Axis.Vertical,
|
||||
contentHeight);
|
||||
}
|
||||
} else {
|
||||
contentHeight = rectHeight;
|
||||
}
|
||||
|
||||
// Get the bottom of the ScrollRect in screen space
|
||||
Vector3[] corners = new Vector3[4];
|
||||
_scrollRectTransform.GetWorldCorners(corners);
|
||||
|
||||
Camera cam = _rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay
|
||||
? null
|
||||
: _rootCanvas.worldCamera;
|
||||
Vector2 scrollRectBottomScreen =
|
||||
RectTransformUtility.WorldToScreenPoint(cam, corners[0]);
|
||||
|
||||
// Calculate how much space we have from ScrollRect bottom to top of screen
|
||||
float spaceToTop = Screen.height - scrollRectBottomScreen.y - topMargin;
|
||||
|
||||
// Account for canvas scaling
|
||||
float scaleFactor = _rootCanvas.scaleFactor;
|
||||
if (scaleFactor > 0) { spaceToTop /= scaleFactor; }
|
||||
|
||||
// Get height of other content (hero details) if specified
|
||||
float otherContentHeight = 0f;
|
||||
if (otherContent != null) { otherContentHeight = otherContent.rect.height; }
|
||||
|
||||
// Max height for ScrollRect = space to top minus other content
|
||||
float maxScrollRectHeight = spaceToTop - otherContentHeight;
|
||||
|
||||
// Clamp to reasonable bounds
|
||||
maxScrollRectHeight = Mathf.Max(maxScrollRectHeight, 50f);
|
||||
|
||||
// Set preferred height to content or max, whichever is smaller
|
||||
float targetHeight = Mathf.Min(contentHeight, maxScrollRectHeight);
|
||||
layoutElement.preferredHeight = targetHeight;
|
||||
|
||||
if (debugLogging) {
|
||||
Debug.Log(
|
||||
$"[AutoScrollingText] contentHeight={contentHeight:F0} (rectHeight={rectHeight:F0}), " +
|
||||
$"spaceToTop={spaceToTop:F0}, otherContent={otherContentHeight:F0}, " +
|
||||
$"maxHeight={maxScrollRectHeight:F0}, targetHeight={targetHeight:F0}");
|
||||
}
|
||||
|
||||
// Force layout rebuild
|
||||
RectTransform parentRect = _scrollRectTransform.parent as RectTransform;
|
||||
if (parentRect != null) { LayoutRebuilder.ForceRebuildLayoutImmediate(parentRect); }
|
||||
}
|
||||
|
||||
private void Update() {
|
||||
if (scrollRect == null) return;
|
||||
|
||||
bool hasOverflow = HasContentOverflow();
|
||||
UpdateFadeGradient();
|
||||
|
||||
if (!hasOverflow) {
|
||||
_isScrolling = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Hold Shift to pause scrolling (lets user read at their own pace)
|
||||
bool isPausedByUser =
|
||||
Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
|
||||
if (isPausedByUser) return;
|
||||
|
||||
// Handle pause at bottom
|
||||
if (_isPausedAtBottom) {
|
||||
_pauseTimer += Time.deltaTime;
|
||||
if (_pauseTimer >= pauseAtBottomSeconds) {
|
||||
// Reset to top and start over
|
||||
scrollRect.verticalNormalizedPosition = 1f;
|
||||
_isPausedAtBottom = false;
|
||||
_pauseTimer = 0f;
|
||||
_visibleTime = 0f;
|
||||
_isScrolling = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Track time visible
|
||||
_visibleTime += Time.deltaTime;
|
||||
|
||||
// Start scrolling after delay
|
||||
if (!_isScrolling && _visibleTime >= scrollDelaySeconds) { _isScrolling = true; }
|
||||
|
||||
// Perform scrolling (apply global speed multiplier from settings)
|
||||
if (_isScrolling) {
|
||||
float effectiveSpeed = scrollSpeed * GlobalScrollSpeedMultiplier;
|
||||
float newPosition =
|
||||
scrollRect.verticalNormalizedPosition - (effectiveSpeed * Time.deltaTime);
|
||||
|
||||
if (newPosition <= 0f) {
|
||||
// Reached bottom
|
||||
scrollRect.verticalNormalizedPosition = 0f;
|
||||
_isScrolling = false;
|
||||
_isPausedAtBottom = true;
|
||||
_pauseTimer = 0f;
|
||||
} else {
|
||||
scrollRect.verticalNormalizedPosition = newPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasContentOverflow() {
|
||||
if (scrollRect == null || scrollRect.content == null) return false;
|
||||
|
||||
// Use TMP_Text.preferredHeight for accurate content measurement
|
||||
float contentHeight;
|
||||
var tmpText = scrollRect.content.GetComponentInChildren<TMP_Text>();
|
||||
if (tmpText != null) {
|
||||
contentHeight = tmpText.preferredHeight;
|
||||
} else {
|
||||
contentHeight = scrollRect.content.rect.height;
|
||||
}
|
||||
|
||||
float viewportHeight = scrollRect.viewport != null
|
||||
? scrollRect.viewport.rect.height
|
||||
: scrollRect.GetComponent<RectTransform>().rect.height;
|
||||
|
||||
return contentHeight > viewportHeight + 1f; // Small buffer for floating point
|
||||
}
|
||||
|
||||
private void UpdateFadeGradient() {
|
||||
if (fadeGradient == null) return;
|
||||
|
||||
bool hasOverflow = HasContentOverflow();
|
||||
bool notAtBottom = scrollRect != null && scrollRect.verticalNormalizedPosition > 0.01f;
|
||||
|
||||
// Show gradient when there's overflow and we're not at the bottom
|
||||
fadeGradient.SetActive(hasOverflow && notAtBottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac5d868c54ceb4893b1c95894cd4ec6f
|
||||
+33
-8
@@ -54,18 +54,43 @@ namespace common.GUIUtils {
|
||||
Type = type,
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
errorTextField.text = AllMessageText;
|
||||
panel.gameObject.SetActive(true);
|
||||
});
|
||||
|
||||
// Try to display immediately if possible, otherwise queue for later
|
||||
try {
|
||||
if (MainQueue.Q != null) {
|
||||
MainQueue.Q.Enqueue(ShowErrorPanel);
|
||||
} else {
|
||||
_pendingShow = true;
|
||||
}
|
||||
} catch {
|
||||
// MainQueue not ready yet - will show in Update
|
||||
_pendingShow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _pendingShow = false;
|
||||
|
||||
private void ShowErrorPanel() {
|
||||
if (errorTextField != null && panel != null) {
|
||||
errorTextField.text = AllMessageText;
|
||||
panel.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Register for log messages as early as possible
|
||||
void Awake() { Application.logMessageReceivedThreaded += HandleLog; }
|
||||
|
||||
void Update() {
|
||||
// Handle errors that occurred before MainQueue was ready
|
||||
if (_pendingShow) {
|
||||
_pendingShow = false;
|
||||
ShowErrorPanel();
|
||||
}
|
||||
}
|
||||
|
||||
// Use this for initialization
|
||||
void Start() {
|
||||
panel.gameObject.SetActive(false);
|
||||
Application.logMessageReceivedThreaded += HandleLog;
|
||||
}
|
||||
void Start() { panel.gameObject.SetActive(false); }
|
||||
|
||||
public void DismissClicked() {
|
||||
lock (_messages) {
|
||||
|
||||
@@ -21,14 +21,24 @@ option java_outer_classname = "EagleInterface";
|
||||
option objc_class_prefix = "E0G";
|
||||
|
||||
service ShardokInternalInterface {
|
||||
// Server-side streaming for game updates - replaces polling via GetGameStatus
|
||||
rpc SubscribeToGame(GameSubscriptionRequest) returns (stream GameStatusResponse) {}
|
||||
|
||||
rpc PostCommand(PostCommandRequest) returns (GameStatusResponse) {}
|
||||
rpc PostPlacementCommands(PlacementCommandsRequest) returns (GameStatusResponse) {}
|
||||
|
||||
// Deprecated: Use SubscribeToGame for streaming updates instead
|
||||
rpc GetGameStatus(GameStatusRequest) returns (GameStatusResponse) {}
|
||||
|
||||
rpc GetHexMap(HexMapRequest) returns (HexMapResponse) {}
|
||||
rpc GetHexMapNames(HexMapNamesRequest) returns (HexMapNamesResponse) {}
|
||||
}
|
||||
|
||||
message GameSubscriptionRequest {
|
||||
string game_id = 1;
|
||||
GameSetupInfo game_setup_info = 2;
|
||||
}
|
||||
|
||||
message PostCommandRequest {
|
||||
string game_id = 1;
|
||||
int32 player_id = 2;
|
||||
|
||||
@@ -171,10 +171,43 @@ message GameUpdate {
|
||||
|
||||
message HeartbeatRequest {
|
||||
int64 client_timestamp = 1;
|
||||
// Sync verification: client reports its known action counts per game
|
||||
repeated GameSyncStatus game_sync_statuses = 2;
|
||||
}
|
||||
|
||||
// Client's known sync state for a single game
|
||||
message GameSyncStatus {
|
||||
int64 game_id = 1;
|
||||
// Eagle action count (matches ActionResultResponse.unfiltered_result_count_after)
|
||||
int32 unfiltered_result_count = 2;
|
||||
// Shardok action counts per tactical battle
|
||||
repeated ShardokSyncStatus shardok_sync_statuses = 3;
|
||||
}
|
||||
|
||||
message ShardokSyncStatus {
|
||||
string shardok_game_id = 1;
|
||||
// Matches ShardokActionResultResponse.filtered_result_count_after
|
||||
int32 filtered_result_count = 2;
|
||||
}
|
||||
|
||||
message HeartbeatResponse {
|
||||
int64 server_timestamp = 1;
|
||||
// Sync verification results - only included if there are mismatches
|
||||
repeated GameSyncResult game_sync_results = 2;
|
||||
}
|
||||
|
||||
// Server's sync verification result for a single game
|
||||
message GameSyncResult {
|
||||
int64 game_id = 1;
|
||||
bool eagle_in_sync = 2;
|
||||
int32 server_unfiltered_result_count = 3; // Server's count for comparison
|
||||
repeated ShardokSyncResult shardok_sync_results = 4;
|
||||
}
|
||||
|
||||
message ShardokSyncResult {
|
||||
string shardok_game_id = 1;
|
||||
bool in_sync = 2;
|
||||
int32 server_filtered_result_count = 3; // Server's count for comparison
|
||||
}
|
||||
|
||||
message NewGameOptions {
|
||||
@@ -256,6 +289,25 @@ message ActionResultResponse {
|
||||
int32 unfiltered_result_count_after = 1;
|
||||
repeated .net.eagle0.eagle.views.ActionResultView action_result_views = 2;
|
||||
AvailableCommands available_commands = 3;
|
||||
// Server-reported game status for connection status UI
|
||||
ServerGameStatus server_game_status = 4;
|
||||
}
|
||||
|
||||
// Server-reported game status for the connection status indicator.
|
||||
// Tells the client what the server is doing/waiting for.
|
||||
message ServerGameStatus {
|
||||
enum Status {
|
||||
UNKNOWN = 0;
|
||||
YOUR_TURN = 1; // Player has commands available
|
||||
WAITING_FOR_PLAYERS = 2; // Waiting for other player(s) to act
|
||||
GENERATING_TEXT = 3; // LLM text generation in progress
|
||||
PROCESSING_ACTION = 4; // Server is processing an action
|
||||
}
|
||||
Status status = 1;
|
||||
// For WAITING_FOR_PLAYERS: which faction(s) we're waiting for
|
||||
repeated int32 waiting_for_faction_ids = 2;
|
||||
// For GENERATING_TEXT: which LLM stream is being generated
|
||||
string generating_llm_id = 3;
|
||||
}
|
||||
|
||||
message ShardokActionResultResponse {
|
||||
|
||||
@@ -23,6 +23,7 @@ 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,6 +21,7 @@ scala_binary(
|
||||
":external_text_generation_caller",
|
||||
":external_text_generation_service_impl",
|
||||
":open_ai_chat_completions_service_impl",
|
||||
":open_ai_responses_service_impl",
|
||||
":streaming_text_results",
|
||||
],
|
||||
)
|
||||
@@ -52,7 +53,10 @@ scala_library(
|
||||
":external_text_generation_service_impl",
|
||||
":rate_limits",
|
||||
":streaming_text_results",
|
||||
"//src/main/scala/net/eagle0/common/sse:sse_subscriber",
|
||||
"//src/main/scala/net/eagle0/common/sse:okhttp_sse_listener",
|
||||
"@maven//:com_squareup_okhttp3_okhttp",
|
||||
"@maven//:com_squareup_okhttp3_okhttp_sse",
|
||||
"@maven//:com_squareup_okio_okio_jvm",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -86,6 +90,24 @@ scala_library(
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "open_ai_responses_service_impl",
|
||||
srcs = ["OpenAIResponsesServiceImpl.scala"],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
deps = [
|
||||
":api_keys",
|
||||
":external_text_generation_service_impl",
|
||||
":open_ai_duration_parser",
|
||||
":rate_limits",
|
||||
":streaming_text_results",
|
||||
"@maven//:org_json4s_json4s_ast_3",
|
||||
"@maven//:org_json4s_json4s_core_3",
|
||||
"@maven//:org_json4s_json4s_native_3",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "open_ai_duration_parser",
|
||||
srcs = ["OpenAiDurationParser.scala"],
|
||||
|
||||
+115
-109
@@ -1,21 +1,23 @@
|
||||
package net.eagle0.common.llm_integration
|
||||
|
||||
import java.net.http.{HttpClient, HttpRequest, HttpResponse, HttpTimeoutException}
|
||||
import java.net.http.HttpClient.{Redirect, Version}
|
||||
import java.net.http.HttpResponse.ResponseInfo
|
||||
import java.io.IOException
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.SocketTimeoutException
|
||||
import java.time.{Duration, ZonedDateTime}
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.{Timer, TimerTask}
|
||||
import java.util.concurrent.CompletionException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise}
|
||||
import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala}
|
||||
import scala.jdk.CollectionConverters.{CollectionHasAsScala, IterableHasAsScala}
|
||||
import scala.jdk.FutureConverters.CompletionStageOps
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
import net.eagle0.common.sse.SseSubscriber
|
||||
import net.eagle0.common.sse.OkHttpSseListener
|
||||
import okhttp3.{MediaType, OkHttpClient, Request, RequestBody, Response}
|
||||
import okhttp3.sse.{EventSource, EventSources}
|
||||
|
||||
enum ExternalTextGenerationError extends Error:
|
||||
case RateLimit(code: Int, msg: String)
|
||||
@@ -35,10 +37,51 @@ object ExternalTextGenerationCaller {
|
||||
|
||||
private def nextBackoff(backoffSeconds: Double): Double =
|
||||
Math.min(backoffSeconds * backoffMultiplier, maxBackoffSeconds)
|
||||
|
||||
/** Convert a Java HttpRequest to an OkHttp Request */
|
||||
private def toOkHttpRequest(javaRequest: HttpRequest): Request = {
|
||||
val builder = new Request.Builder()
|
||||
.url(javaRequest.uri().toString)
|
||||
|
||||
// Copy headers
|
||||
javaRequest.headers().map().forEach { (name, values) =>
|
||||
values.forEach(value => builder.addHeader(name, value))
|
||||
}
|
||||
|
||||
// Handle request body
|
||||
javaRequest.method() match {
|
||||
case "GET" => builder.get()
|
||||
case "POST" =>
|
||||
val bodyPublisher = javaRequest.bodyPublisher().orElse(null)
|
||||
if bodyPublisher != null then {
|
||||
// Extract body content - for BodyPublishers.ofString, we can get the content
|
||||
val bodyContent = new StringBuilder()
|
||||
val subscriber = new java.util.concurrent.Flow.Subscriber[java.nio.ByteBuffer] {
|
||||
override def onSubscribe(subscription: java.util.concurrent.Flow.Subscription): Unit =
|
||||
subscription.request(Long.MaxValue)
|
||||
override def onNext(item: java.nio.ByteBuffer): Unit =
|
||||
bodyContent.append(java.nio.charset.StandardCharsets.UTF_8.decode(item).toString)
|
||||
override def onError(throwable: Throwable): Unit = ()
|
||||
override def onComplete(): Unit = ()
|
||||
}
|
||||
bodyPublisher.subscribe(subscriber)
|
||||
val contentType =
|
||||
javaRequest.headers().firstValue("Content-Type").orElse("application/json")
|
||||
val mediaType = MediaType.parse(contentType)
|
||||
builder.post(RequestBody.create(bodyContent.toString, mediaType))
|
||||
} else {
|
||||
builder.post(RequestBody.create("", null))
|
||||
}
|
||||
case other => throw new UnsupportedOperationException(s"HTTP method $other not supported")
|
||||
}
|
||||
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
final class ExternalTextGenerationCaller(
|
||||
val timeoutSeconds: Int = 10,
|
||||
val readTimeoutSeconds: Int = 60,
|
||||
serviceImpl: ExternalTextGenerationServiceImpl
|
||||
) {
|
||||
private var inProgressCount = 0
|
||||
@@ -57,13 +100,17 @@ final class ExternalTextGenerationCaller(
|
||||
|
||||
implicit val ec: ExecutionContext = ExecutionContext.global
|
||||
|
||||
private val httpClient = HttpClient
|
||||
.newBuilder()
|
||||
.version(Version.HTTP_2)
|
||||
.followRedirects(Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(timeoutSeconds))
|
||||
// OkHttp client with read timeout - the key benefit over Java HttpClient
|
||||
// If no data is received for readTimeoutSeconds, the connection will timeout
|
||||
private val okHttpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(timeoutSeconds.toLong, TimeUnit.SECONDS)
|
||||
.readTimeout(readTimeoutSeconds.toLong, TimeUnit.SECONDS)
|
||||
.writeTimeout(timeoutSeconds.toLong, TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(true)
|
||||
.build()
|
||||
|
||||
private val eventSourceFactory = EventSources.createFactory(okHttpClient)
|
||||
|
||||
private var successDurations =
|
||||
new scala.collection.mutable.ArrayBuffer[Long]()
|
||||
|
||||
@@ -72,7 +119,7 @@ final class ExternalTextGenerationCaller(
|
||||
partialCompletion: Option[String],
|
||||
streamingConsumer: Consumer[StreamingTextResults],
|
||||
backoffSeconds: Double = ExternalTextGenerationCaller.initialBackoffSeconds
|
||||
): Future[HttpResponse[Unit]] =
|
||||
): Future[Unit] =
|
||||
streamCompletion(
|
||||
inputText = inputText,
|
||||
request = serviceImpl.makeRequest(
|
||||
@@ -88,13 +135,13 @@ final class ExternalTextGenerationCaller(
|
||||
request: HttpRequest,
|
||||
backoffSeconds: Double,
|
||||
streamingConsumer: Consumer[StreamingTextResults]
|
||||
): Future[HttpResponse[Unit]] = {
|
||||
): Future[Unit] = {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
def tryAgain: () => Future[HttpResponse[Unit]] = () => {
|
||||
def tryAgain: () => Future[Unit] = () => {
|
||||
println(s"Trying again after $backoffSeconds seconds...")
|
||||
|
||||
val promise = Promise[HttpResponse[Unit]]()
|
||||
val promise = Promise[Unit]()
|
||||
val t = new Timer()
|
||||
t.schedule(
|
||||
new TimerTask {
|
||||
@@ -113,104 +160,63 @@ final class ExternalTextGenerationCaller(
|
||||
promise.future
|
||||
}
|
||||
|
||||
val okHttpRequest = ExternalTextGenerationCaller.toOkHttpRequest(request)
|
||||
val sseListener = new OkHttpSseListener(serviceImpl.stringConsumer(streamingConsumer))
|
||||
|
||||
inProgressCount += 1
|
||||
httpClient
|
||||
.sendAsync(
|
||||
request,
|
||||
(respInfo: ResponseInfo) =>
|
||||
if respInfo.statusCode() == HttpURLConnection.HTTP_OK then
|
||||
new SseSubscriber(serviceImpl.stringConsumer(streamingConsumer))
|
||||
else
|
||||
// For error responses, read the body to get error details
|
||||
HttpResponse.BodySubscribers.mapping(
|
||||
HttpResponse.BodySubscribers.ofString(java.nio.charset.StandardCharsets.UTF_8),
|
||||
(body: String) => {
|
||||
val code = respInfo.statusCode()
|
||||
println(s"Error response ($code): $body")
|
||||
// 4xx errors (except 429) are client errors - don't retry
|
||||
if code >= 400 && code < 500 && code != 429 then
|
||||
throw ExternalTextGenerationError.Http(
|
||||
code,
|
||||
s"Client error $code: $body"
|
||||
)
|
||||
else
|
||||
throw new RuntimeException(
|
||||
s"Server error $code: $body"
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
.asScala
|
||||
.andThen {
|
||||
case x =>
|
||||
inProgressCount -= 1
|
||||
x
|
||||
}
|
||||
.transform {
|
||||
case Success(httpResponse) =>
|
||||
val rateLimits = serviceImpl.rateLimitsFrom(
|
||||
headers = httpResponse
|
||||
.headers()
|
||||
.map()
|
||||
.asScala
|
||||
.map { case (k, v) => (k, v.asScala.toVector) }
|
||||
.toMap
|
||||
|
||||
// Start the SSE connection
|
||||
val eventSource = eventSourceFactory.newEventSource(okHttpRequest, sseListener)
|
||||
|
||||
// Convert the CompletableFuture to a Scala Future
|
||||
sseListener.getFuture.asScala.andThen {
|
||||
case _ =>
|
||||
inProgressCount -= 1
|
||||
}.transform {
|
||||
case Success(_) =>
|
||||
val duration = System.currentTimeMillis() - startTime
|
||||
this.synchronized {
|
||||
successDurations.addOne(duration)
|
||||
if successDurations.size % 100 == 0 then {
|
||||
successDurations = successDurations.sorted
|
||||
val p99pos =
|
||||
successDurations.size - successDurations.size / 100
|
||||
println(s"p99 duration: ${successDurations(p99pos)}ms")
|
||||
}
|
||||
}
|
||||
Success(())
|
||||
|
||||
case Failure(e: SocketTimeoutException) =>
|
||||
println(s"Read timeout after ${readTimeoutSeconds}s: ${e.getMessage}")
|
||||
Failure(
|
||||
ExternalTextGenerationError.Timeout(
|
||||
s"Read timeout after ${readTimeoutSeconds}s: ${e.getMessage}"
|
||||
)
|
||||
currentRateLimits = rateLimits
|
||||
currentRateLimitTime = ZonedDateTime.now()
|
||||
)
|
||||
|
||||
val responseCode = httpResponse.statusCode()
|
||||
if responseCode < HttpURLConnection.HTTP_BAD_REQUEST then {
|
||||
val duration = System.currentTimeMillis() - startTime
|
||||
|
||||
this.synchronized {
|
||||
successDurations.addOne(duration)
|
||||
if successDurations.size % 100 == 0 then {
|
||||
successDurations = successDurations.sorted
|
||||
val p99pos =
|
||||
successDurations.size - successDurations.size / 100
|
||||
println(s"p99 duration: ${successDurations(p99pos)}ms")
|
||||
}
|
||||
|
||||
Success(httpResponse)
|
||||
}
|
||||
} else if responseCode == 429 then
|
||||
Failure(
|
||||
ExternalTextGenerationError.RateLimit(
|
||||
responseCode,
|
||||
httpResponse.toString
|
||||
)
|
||||
)
|
||||
else
|
||||
Failure(
|
||||
ExternalTextGenerationError.Http(
|
||||
responseCode,
|
||||
"An error occurred while generating a response.\n" + httpResponse
|
||||
)
|
||||
)
|
||||
|
||||
case Failure(timeoutException: HttpTimeoutException) =>
|
||||
Failure(
|
||||
ExternalTextGenerationError.Timeout(
|
||||
s"Timed out: ${timeoutException.getMessage}"
|
||||
)
|
||||
case Failure(e: IOException) if e.getMessage != null && e.getMessage.contains("timeout") =>
|
||||
println(s"Timeout: ${e.getMessage}")
|
||||
Failure(
|
||||
ExternalTextGenerationError.Timeout(
|
||||
s"Timeout: ${e.getMessage}"
|
||||
)
|
||||
)
|
||||
|
||||
case Failure(exception) => Failure(exception)
|
||||
}
|
||||
.recoverWith {
|
||||
// Don't retry 4xx client errors - they won't succeed
|
||||
case e: CompletionException if e.getCause.isInstanceOf[ExternalTextGenerationError.Http] =>
|
||||
Future.failed(e.getCause)
|
||||
case e: ExternalTextGenerationError.Http =>
|
||||
Future.failed(e)
|
||||
// Retry transient errors (5xx, timeouts, network issues)
|
||||
case e: CompletionException =>
|
||||
println(s"CompletionException error $e - retrying")
|
||||
tryAgain()
|
||||
case e: Throwable =>
|
||||
println(s"error $e - retrying")
|
||||
tryAgain()
|
||||
}
|
||||
case Failure(exception) => Failure(exception)
|
||||
}.recoverWith {
|
||||
// Don't retry 4xx client errors - they won't succeed
|
||||
case e: ExternalTextGenerationError.Http =>
|
||||
Future.failed(e)
|
||||
// Retry transient errors (timeouts, network issues)
|
||||
case e: ExternalTextGenerationError.Timeout =>
|
||||
println(s"Timeout error - retrying: ${e.message}")
|
||||
tryAgain()
|
||||
case e: IOException =>
|
||||
println(s"IOException - retrying: ${e.getMessage}")
|
||||
tryAgain()
|
||||
case e: Throwable =>
|
||||
println(s"error $e - retrying")
|
||||
tryAgain()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -4,6 +4,9 @@ import scala.concurrent.duration.{Duration, SECONDS}
|
||||
import scala.concurrent.Await
|
||||
//import scala.util.Random
|
||||
|
||||
enum LlmProvider:
|
||||
case Claude, OpenAIChatCompletions, OpenAIResponses
|
||||
|
||||
object ExternalTextGenerationCallerApp {
|
||||
def main(args: Array[String]): Unit = {
|
||||
// val personalityWords = Vector(
|
||||
@@ -30,8 +33,16 @@ object ExternalTextGenerationCallerApp {
|
||||
val chatGptImpl: ExternalTextGenerationServiceImpl =
|
||||
new OpenAIChatCompletionsServiceImpl()
|
||||
|
||||
val caller = new ExternalTextGenerationCaller(
|
||||
serviceImpl = if true then claudeImpl else chatGptImpl
|
||||
val openAiResponsesImpl: ExternalTextGenerationServiceImpl =
|
||||
new OpenAIResponsesServiceImpl()
|
||||
|
||||
val selectedProvider = LlmProvider.OpenAIResponses
|
||||
val caller = new ExternalTextGenerationCaller(
|
||||
serviceImpl = selectedProvider match {
|
||||
case LlmProvider.Claude => claudeImpl
|
||||
case LlmProvider.OpenAIChatCompletions => chatGptImpl
|
||||
case LlmProvider.OpenAIResponses => openAiResponsesImpl
|
||||
}
|
||||
)
|
||||
|
||||
(0 to 0).foreach { _ =>
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package net.eagle0.common.llm_integration
|
||||
|
||||
import java.net.{URI, URL}
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpRequest.BodyPublishers
|
||||
import java.time.{Duration, ZonedDateTime}
|
||||
import java.util.function.Consumer
|
||||
|
||||
import org.json4s.{DefaultFormats, JString}
|
||||
import org.json4s.jvalue2extractable
|
||||
import org.json4s.jvalue2monadic
|
||||
import org.json4s.native.{Json, Serialization}
|
||||
|
||||
/**
|
||||
* OpenAI Responses API implementation.
|
||||
*
|
||||
* The Responses API is OpenAI's newer API primitive that offers: - Better performance with reasoning models (3%
|
||||
* improvement in SWE-bench) - Lower costs through improved cache utilization (40-80% improvement) - Semantic streaming
|
||||
* events with clear lifecycle (response.created, response.output_text.delta, response.completed) - Built-in tools (web
|
||||
* search, file search, computer use, code interpreter)
|
||||
*
|
||||
* @see
|
||||
* https://platform.openai.com/docs/api-reference/responses
|
||||
*/
|
||||
object OpenAIResponsesServiceImpl {
|
||||
val gpt5: String = "gpt-5.1"
|
||||
|
||||
private val apiKey = ApiKeys.openAI
|
||||
private val baseURL = new URL("https://api.openai.com/v1/responses")
|
||||
private val temperature: Double = 1.0
|
||||
|
||||
private def baseRequest(timeoutSeconds: Int): HttpRequest.Builder =
|
||||
HttpRequest
|
||||
.newBuilder()
|
||||
.uri(URI.create(baseURL.toString))
|
||||
.timeout(Duration.ofSeconds(timeoutSeconds))
|
||||
.header(
|
||||
"Authorization",
|
||||
s"Bearer ${OpenAIResponsesServiceImpl.apiKey}"
|
||||
)
|
||||
}
|
||||
|
||||
class OpenAIResponsesServiceImpl(
|
||||
timeoutSeconds: Int = 10,
|
||||
val defaultModelName: String = "gpt-5.1"
|
||||
) extends ExternalTextGenerationServiceImpl {
|
||||
implicit val jsonFormats: DefaultFormats.type = DefaultFormats
|
||||
|
||||
private def requestDictionary(
|
||||
inputText: String,
|
||||
partialCompletion: Option[String]
|
||||
): Map[String, Any] = {
|
||||
// For partial completions, we need to format as an array of input items
|
||||
// The Responses API uses "input" instead of "messages"
|
||||
val input = partialCompletion match {
|
||||
case Some(partial) =>
|
||||
// When we have a partial completion, send as array of items
|
||||
Vector(
|
||||
Map("type" -> "message", "role" -> "user", "content" -> inputText),
|
||||
Map("type" -> "message", "role" -> "assistant", "content" -> partial)
|
||||
)
|
||||
case None =>
|
||||
// Simple text input when no partial completion
|
||||
inputText
|
||||
}
|
||||
|
||||
Map(
|
||||
"model" -> defaultModelName,
|
||||
"temperature" -> OpenAIResponsesServiceImpl.temperature,
|
||||
"stream" -> true,
|
||||
"input" -> input
|
||||
)
|
||||
}
|
||||
|
||||
override def makeRequest(
|
||||
inputText: String,
|
||||
partialCompletion: Option[String]
|
||||
): HttpRequest =
|
||||
OpenAIResponsesServiceImpl
|
||||
.baseRequest(timeoutSeconds = timeoutSeconds)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(
|
||||
BodyPublishers.ofString(
|
||||
Serialization.write(
|
||||
requestDictionary(
|
||||
inputText = inputText,
|
||||
partialCompletion = partialCompletion
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.build()
|
||||
|
||||
override def stringConsumer(
|
||||
streamingConsumer: Consumer[StreamingTextResults]
|
||||
): Consumer[String] = (t: String) => {
|
||||
val json = new Json(DefaultFormats)
|
||||
val parsedJson =
|
||||
try
|
||||
json.parse(t)
|
||||
catch {
|
||||
case pe: org.json4s.ParserUtil.ParseException =>
|
||||
println(s"Failed to parse JSON: $t")
|
||||
throw pe
|
||||
}
|
||||
|
||||
val eventType = (parsedJson \ "type").extractOpt[String]
|
||||
|
||||
eventType match {
|
||||
case Some("response.output_text.delta") =>
|
||||
// Text delta event - extract the delta and stream ID
|
||||
val delta = (parsedJson \ "delta").extract[String]
|
||||
val itemId = (parsedJson \ "item_id").extractOpt[String].getOrElse("unknown")
|
||||
val streamId = itemId
|
||||
|
||||
streamingConsumer.accept(
|
||||
StreamingTextResults(
|
||||
streamId = streamId,
|
||||
value = delta,
|
||||
completed = false
|
||||
)
|
||||
)
|
||||
|
||||
case Some("response.output_text.done") =>
|
||||
// Text is complete for this output item
|
||||
val itemId = (parsedJson \ "item_id").extractOpt[String].getOrElse("unknown")
|
||||
val streamId = itemId
|
||||
|
||||
streamingConsumer.accept(
|
||||
StreamingTextResults(
|
||||
streamId = streamId,
|
||||
value = "",
|
||||
completed = true
|
||||
)
|
||||
)
|
||||
|
||||
case Some("response.completed") =>
|
||||
// Response is fully complete - extract the response ID as streamId
|
||||
val responseId = (parsedJson \ "response" \ "id").extractOpt[String].getOrElse("unknown")
|
||||
streamingConsumer.accept(
|
||||
StreamingTextResults(
|
||||
streamId = responseId,
|
||||
value = "",
|
||||
completed = true
|
||||
)
|
||||
)
|
||||
|
||||
case Some("response.created") | Some("response.in_progress") | Some("response.output_item.added") | Some(
|
||||
"response.content_part.added"
|
||||
) =>
|
||||
// Lifecycle events - ignore, no content to stream
|
||||
()
|
||||
|
||||
case Some("error") =>
|
||||
// Error event
|
||||
val errorMsg = (parsedJson \ "error" \ "message").extractOpt[String].getOrElse("Unknown error")
|
||||
throw new RuntimeException(s"OpenAI Responses API error: $errorMsg")
|
||||
|
||||
case Some(other) =>
|
||||
// Unknown event type - log but continue
|
||||
println(s"OpenAI Responses API: ignoring unknown event type: $other")
|
||||
|
||||
case None =>
|
||||
// No event type - might be malformed, log it
|
||||
println(s"OpenAI Responses API: received message without event type: $t")
|
||||
}
|
||||
}
|
||||
|
||||
override def rateLimitsFrom(
|
||||
headers: Map[String, Vector[String]]
|
||||
): Option[RateLimits] =
|
||||
// The Responses API uses the same rate limit headers as Chat Completions
|
||||
for {
|
||||
requestLimit <- headers.get("x-ratelimit-limit-requests")
|
||||
tokenLimit <- headers.get("x-ratelimit-limit-tokens")
|
||||
requestsRemaining <- headers.get("x-ratelimit-remaining-requests")
|
||||
tokensRemaining <- headers.get("x-ratelimit-remaining-tokens")
|
||||
requestResetTime <- headers.get("x-ratelimit-reset-requests")
|
||||
tokenResetTime <- headers.get("x-ratelimit-reset-tokens")
|
||||
} yield RateLimits(
|
||||
requestLimit = requestLimit.head.toInt,
|
||||
tokenLimit = tokenLimit.head.toInt,
|
||||
requestsRemaining = requestsRemaining.head.toInt,
|
||||
tokensRemaining = tokensRemaining.head.toInt,
|
||||
requestResetTime = ZonedDateTime
|
||||
.now()
|
||||
.plus(
|
||||
OpenAiDurationParser
|
||||
.parseDuration(requestResetTime.head)
|
||||
.getOrElse(Duration.ZERO)
|
||||
),
|
||||
tokenResetTime = ZonedDateTime
|
||||
.now()
|
||||
.plus(
|
||||
OpenAiDurationParser
|
||||
.parseDuration(tokenResetTime.head)
|
||||
.getOrElse(Duration.ZERO)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -18,3 +18,13 @@ scala_library(
|
||||
"@maven//:org_json4s_json4s_core_3",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "okhttp_sse_listener",
|
||||
srcs = ["OkHttpSseListener.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"@maven//:com_squareup_okhttp3_okhttp",
|
||||
"@maven//:com_squareup_okhttp3_okhttp_sse",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package net.eagle0.common.sse
|
||||
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.function.Consumer
|
||||
|
||||
import okhttp3.sse.{EventSource, EventSourceListener}
|
||||
import okhttp3.Response
|
||||
|
||||
/**
|
||||
* OkHttp-based SSE listener that wraps our existing SseEventReader logic.
|
||||
*
|
||||
* Key benefit over Java HttpClient: OkHttp supports read timeouts, so if the server stops sending data without properly
|
||||
* closing the connection, we'll get a timeout error instead of waiting forever.
|
||||
*/
|
||||
class OkHttpSseListener(messageDataConsumer: Consumer[String]) extends EventSourceListener {
|
||||
|
||||
private val future: CompletableFuture[Unit] = new CompletableFuture[Unit]
|
||||
|
||||
private val DoneToken = "[DONE]"
|
||||
|
||||
def getFuture: CompletableFuture[Unit] = future
|
||||
|
||||
override def onOpen(eventSource: EventSource, response: Response): Unit = {
|
||||
// Connection established, nothing to do
|
||||
}
|
||||
|
||||
override def onEvent(
|
||||
eventSource: EventSource,
|
||||
id: String,
|
||||
`type`: String,
|
||||
data: String
|
||||
): Unit =
|
||||
try
|
||||
if data != DoneToken then {
|
||||
messageDataConsumer.accept(data)
|
||||
}
|
||||
// If it's [DONE], just ignore - onClosed will be called
|
||||
catch {
|
||||
case e: Exception =>
|
||||
val _ = future.completeExceptionally(e)
|
||||
eventSource.cancel()
|
||||
}
|
||||
|
||||
override def onClosed(eventSource: EventSource): Unit =
|
||||
if !future.isDone then {
|
||||
val _ = future.complete(())
|
||||
}
|
||||
|
||||
override def onFailure(
|
||||
eventSource: EventSource,
|
||||
t: Throwable,
|
||||
response: Response
|
||||
): Unit =
|
||||
if !future.isDone then {
|
||||
if response != null then {
|
||||
println(s"SSE failure with response code ${response.code()}: ${t.getMessage}")
|
||||
} else {
|
||||
println(s"SSE failure: ${t.getMessage}")
|
||||
}
|
||||
val _ = future.completeExceptionally(t)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,8 @@ case class IncompleteClientText(
|
||||
id: ClientTextId,
|
||||
partialText: String,
|
||||
requestedAfterHistoryCount: Int,
|
||||
llmRequest: GeneratedTextRequest
|
||||
llmRequest: GeneratedTextRequest,
|
||||
requestedAtMillis: Long
|
||||
) extends ClientText {
|
||||
def append(newText: String): IncompleteClientText =
|
||||
copy(partialText = partialText + newText)
|
||||
|
||||
@@ -17,6 +17,15 @@ 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,7 +69,8 @@ case class ClientTextStoreImpl(
|
||||
id = id,
|
||||
partialText = "",
|
||||
llmRequest = unrequested.llmRequest,
|
||||
requestedAfterHistoryCount = unrequested.requestedAfterHistoryCount
|
||||
requestedAfterHistoryCount = unrequested.requestedAfterHistoryCount,
|
||||
requestedAtMillis = System.currentTimeMillis()
|
||||
)),
|
||||
unrequestedTexts = unrequestedTexts - id,
|
||||
incompleteTextsAreSaved = false
|
||||
@@ -238,7 +239,8 @@ object ClientTextStoreImpl {
|
||||
id = ict.id,
|
||||
partialText = ict.text,
|
||||
llmRequest = Some(ict.llmRequest),
|
||||
requestedAfterHistoryCount = ict.requestedAfterHistoryCount
|
||||
requestedAfterHistoryCount = ict.requestedAfterHistoryCount,
|
||||
requestedAtMillis = ict.requestedAtMillis
|
||||
)
|
||||
}.toVector,
|
||||
unrequestedTexts = completeSaved.unrequestedTexts.map {
|
||||
@@ -330,7 +332,12 @@ object ClientTextStoreImpl {
|
||||
id = it.id,
|
||||
partialText = it.partialText,
|
||||
llmRequest = it.llmRequest.get,
|
||||
requestedAfterHistoryCount = it.requestedAfterHistoryCount
|
||||
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()
|
||||
)
|
||||
}.toVector,
|
||||
icts.unrequestedTexts.map { it =>
|
||||
|
||||
@@ -48,7 +48,9 @@ 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",
|
||||
@@ -57,18 +59,19 @@ 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:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_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",
|
||||
@@ -149,11 +152,16 @@ 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",
|
||||
],
|
||||
@@ -203,7 +211,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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
@@ -211,17 +219,13 @@ scala_library(
|
||||
"//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/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,7 +7,12 @@ 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.{ActionResultProtoApplier, ActionResultProtoApplierImpl}
|
||||
import net.eagle0.eagle.library.actions.applier.{
|
||||
ActionResultApplierImpl,
|
||||
ActionResultProtoApplier,
|
||||
ActionResultProtoApplierImpl,
|
||||
ActionResultWithResultingState
|
||||
}
|
||||
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
|
||||
import net.eagle0.eagle.library.actions.impl.action.{
|
||||
CheckForFulfilledQuestsAction,
|
||||
@@ -15,11 +20,12 @@ 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, RandomStateProtoSequencer}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
|
||||
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
|
||||
import net.eagle0.eagle.library.util.validations.RuntimeValidator
|
||||
import net.eagle0.eagle.library.util.validations.{RuntimeValidator, ScalaRuntimeValidator}
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EngineImpl.{appliedResults, withUpdateChecks}
|
||||
import net.eagle0.eagle.library.EngineImpl.{appliedResults, appliedResultsScala, 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
|
||||
@@ -43,10 +49,10 @@ object EngineImpl {
|
||||
private def withPhaseAdvancement(
|
||||
engineAndResultsImpl: EngineAndResultsImpl
|
||||
): EngineAndResultsImpl =
|
||||
engineAndResultsImpl.recursiveTransform(eng =>
|
||||
engineAndResultsImpl.recursiveTransformScala(eng =>
|
||||
RoundPhaseAdvancer.checkForPhaseAdvancement(
|
||||
currentState = GameStateConverter.toProto(eng.currentState),
|
||||
actionResultProtoApplier = eng.actionResultProtoApplier,
|
||||
currentState = eng.currentState,
|
||||
actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator)),
|
||||
history = eng.history,
|
||||
availableCommandsFactory = eng.availableCommandsFactory,
|
||||
heroGenerator = eng.heroGenerator,
|
||||
@@ -97,14 +103,31 @@ 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 recursiveTransform(
|
||||
f: EngineImpl => Vector[ActionWithResultingState]
|
||||
def recursiveTransformScala(
|
||||
f: EngineImpl => Vector[ActionResultWithResultingState]
|
||||
): EngineAndResultsImpl = {
|
||||
@tailrec
|
||||
def go(
|
||||
@@ -115,7 +138,7 @@ final case class EngineAndResultsImpl(
|
||||
|
||||
if goResults.isEmpty then EngineAndResultsImpl(eng, acc)
|
||||
else
|
||||
appliedResults(eng, goResults) match {
|
||||
appliedResultsScala(eng, goResults) match {
|
||||
case EngineAndResultsImpl(eng2, res) =>
|
||||
go(eng2, acc ++ res)
|
||||
}
|
||||
@@ -126,13 +149,13 @@ final case class EngineAndResultsImpl(
|
||||
|
||||
def recursiveTransformT(
|
||||
f: EngineImpl => Vector[ActionResultT]
|
||||
): EngineAndResultsImpl = recursiveTransform { eng =>
|
||||
val results = f(eng)
|
||||
RandomStateProtoSequencer(
|
||||
): EngineAndResultsImpl = recursiveTransformScala { eng =>
|
||||
val actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator))
|
||||
RandomStateSequencer(
|
||||
initialState = eng.currentState,
|
||||
actionResultProtoApplier = eng.actionResultProtoApplier,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = SeededRandom(eng.currentState.randomSeed)
|
||||
).withActionResultTs(_ => results).results.newValue
|
||||
).withActionResults(_ => f(eng)).resultsWithStates.newValue
|
||||
}
|
||||
|
||||
def saveNow: EngineAndResultsImpl =
|
||||
@@ -282,37 +305,34 @@ case class EngineImpl(
|
||||
)
|
||||
val availableCommand = availableCommandOpt.get
|
||||
|
||||
val sequencer = RandomStateProtoSequencer(
|
||||
val sequencer = RandomStateSequencer(
|
||||
initialState = this.currentState,
|
||||
actionResultProtoApplier = actionResultProtoApplier,
|
||||
actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator)),
|
||||
functionalRandom = SeededRandom(this.currentState.randomSeed)
|
||||
).withActionResults { gs =>
|
||||
val results = commandFactory
|
||||
.makeCommand(
|
||||
actingFactionId = factionId,
|
||||
gameState = GameStateConverter.fromProto(gs),
|
||||
availableCommand = availableCommand,
|
||||
selectedCommand = selectedCommand
|
||||
)
|
||||
.execute(actionResultProtoApplier)
|
||||
.map(_.actionResult)
|
||||
|
||||
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"
|
||||
).withTCommand { gs =>
|
||||
commandFactory.makeTCommand(
|
||||
actingFactionId = factionId,
|
||||
gameState = gs,
|
||||
availableCommand = availableCommand,
|
||||
selectedCommand = selectedCommand
|
||||
)
|
||||
}.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator(gs))
|
||||
|
||||
results
|
||||
}.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator.fromGameState(gs))
|
||||
// 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"
|
||||
)
|
||||
}
|
||||
internalRequire(
|
||||
firstResult.forall(_.actingFactionId.isDefined),
|
||||
s"Result with type ${firstResult.map(_.actionResultType).getOrElse("unknown")} did not have a player set"
|
||||
)
|
||||
|
||||
appliedResults(
|
||||
appliedResultsScala(
|
||||
engine = this,
|
||||
results = sequencer.results.newValue
|
||||
results = sequencer.resultsWithStates.newValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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
|
||||
@@ -37,6 +40,14 @@ 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,37 +3,30 @@ package net.eagle0.eagle.library
|
||||
import scala.collection.mutable
|
||||
|
||||
import net.eagle0.common.SeededRandom
|
||||
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.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.actions.applier.{ActionResultApplier, ActionResultWithResultingState}
|
||||
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
|
||||
import net.eagle0.eagle.library.actions.impl.action.*
|
||||
import net.eagle0.eagle.library.actions.impl.command.CommandFactory
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.actions.impl.common.VigorXPApplier
|
||||
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalValidated
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.model.proto_converters.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.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
import net.eagle0.eagle.model.state.RoundPhase.*
|
||||
import net.eagle0.eagle.RoundId
|
||||
|
||||
object RoundPhaseAdvancer {
|
||||
private val times: mutable.Map[RoundPhase, Long] =
|
||||
mutable.Map(RoundPhase.values.map(_ -> 0L)*)
|
||||
mutable.Map(RoundPhase.allValues.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)(Ordering.Long.reverse).foreach {
|
||||
times.toVector.sortBy(_._2)(using Ordering.Long.reverse).foreach {
|
||||
case (phase, time) =>
|
||||
val timeInSecs = time.toDouble / 1000.0
|
||||
val msPerRound = time.toDouble / roundId.toDouble
|
||||
@@ -47,360 +40,338 @@ object RoundPhaseAdvancer {
|
||||
|
||||
def checkForPhaseAdvancement(
|
||||
currentState: GameState,
|
||||
actionResultProtoApplier: ActionResultProtoApplier,
|
||||
actionResultApplier: ActionResultApplier,
|
||||
history: GameHistory,
|
||||
availableCommandsFactory: AvailableCommandsFactory,
|
||||
heroGenerator: HeroGenerator,
|
||||
commandFactory: CommandFactory
|
||||
): Vector[ActionWithResultingState] = {
|
||||
): Vector[ActionResultWithResultingState] = {
|
||||
// Lazy conversion to proto for AvailableCommandsFactory calls
|
||||
lazy val currentStateProto: GameStateProto = GameStateConverter.toProto(currentState)
|
||||
|
||||
val currentPhase = currentState.currentPhase
|
||||
val startTime = System.currentTimeMillis
|
||||
|
||||
if print && currentPhase == NEW_ROUND && currentState.currentRoundId % roundsBetweenPrint == 0
|
||||
if print && currentPhase == NewRound && currentState.currentRoundId % roundsBetweenPrint == 0
|
||||
then {
|
||||
printTimings(currentState.currentRoundId)
|
||||
}
|
||||
|
||||
val results: Vector[ActionWithResultingState] = currentPhase match {
|
||||
case UNKNOWN_PHASE =>
|
||||
throw new IllegalStateException(
|
||||
"Somehow we're in game state UNKNOWN_PHASE"
|
||||
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
|
||||
)
|
||||
|
||||
case NEW_ROUND =>
|
||||
NewRoundAction(GameStateConverter.fromProto(currentState), history).execute(actionResultProtoApplier)
|
||||
|
||||
case PRISONER_EXCHANGE =>
|
||||
PrisonerExchangeAction(currentState).execute(actionResultProtoApplier)
|
||||
|
||||
case PROVINCE_EVENTS =>
|
||||
PerformProvinceEventsAction(
|
||||
GameStateConverter.fromProto(currentState)
|
||||
).execute(actionResultProtoApplier)
|
||||
|
||||
case FORCED_TURN_BACK =>
|
||||
PerformForcedTurnBackAction(currentState).execute(
|
||||
actionResultProtoApplier
|
||||
case ProvinceEvents =>
|
||||
actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
PerformProvinceEventsAction(currentState)
|
||||
.results(SeededRandom(currentState.randomSeed))
|
||||
)
|
||||
|
||||
case PROVINCE_MOVE_RESOLUTION =>
|
||||
PerformProvinceMoveResolutionAction(
|
||||
GameStateConverter.fromProto(currentState)
|
||||
).execute(actionResultProtoApplier)
|
||||
case ForcedTurnBack =>
|
||||
actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
PerformForcedTurnBackAction(currentState).results
|
||||
)
|
||||
|
||||
case HANDLE_RIOT =>
|
||||
case ProvinceMoveResolution =>
|
||||
val actionResults = PerformProvinceMoveResolutionAction(currentState, actionResultApplier)
|
||||
.results(SeededRandom(currentState.randomSeed))
|
||||
.map(VigorXPApplier.withVigorXp)
|
||||
actionResultApplier.applyActionResults(currentState, actionResults)
|
||||
|
||||
case HandleRiot =>
|
||||
if availableCommandsFactory
|
||||
.hasAvailableHandleRiotPhaseCommands(currentState)
|
||||
.hasAvailableHandleRiotPhaseCommands(currentStateProto)
|
||||
then Vector.empty
|
||||
else
|
||||
EndHandleRiotsPhaseAction(
|
||||
gameState = GameStateConverter.fromProto(currentState),
|
||||
val actionResults = EndHandleRiotsPhaseAction(
|
||||
gameState = currentState,
|
||||
commandsForProvince = pid =>
|
||||
availableCommandsFactory
|
||||
.handleRiotPhaseCommandsForOneProvince(
|
||||
currentState,
|
||||
currentState.provinces(pid)
|
||||
currentStateProto,
|
||||
currentStateProto.provinces(pid)
|
||||
),
|
||||
commandFactory = commandFactory
|
||||
).execute(actionResultProtoApplier)
|
||||
commandFactory = commandFactory,
|
||||
actionResultApplier = actionResultApplier
|
||||
).results(SeededRandom(currentState.randomSeed))
|
||||
.map(VigorXPApplier.withVigorXp)
|
||||
actionResultApplier.applyActionResults(currentState, actionResults)
|
||||
|
||||
case HERO_DEPARTURES =>
|
||||
PerformHeroDeparturesAction(
|
||||
startingState = currentState,
|
||||
functionalRandom = SeededRandom(currentState.randomSeed)
|
||||
).execute(actionResultProtoApplier)
|
||||
case HeroDepartures =>
|
||||
val actionResults = PerformHeroDeparturesAction(gameState = currentState)
|
||||
.results(SeededRandom(currentState.randomSeed))
|
||||
actionResultApplier.applyActionResults(currentState, actionResults)
|
||||
|
||||
case UNAFFILIATED_HERO_ACTIONS =>
|
||||
PerformUnaffiliatedHeroesAction(
|
||||
gameState = GameStateConverter.fromProto(currentState),
|
||||
heroGenerator = heroGenerator
|
||||
).execute(actionResultProtoApplier)
|
||||
case UnaffiliatedHeroActions =>
|
||||
val actionResults = PerformUnaffiliatedHeroesAction(
|
||||
gameState = currentState,
|
||||
heroGenerator = heroGenerator,
|
||||
actionResultApplier = actionResultApplier
|
||||
).results(SeededRandom(currentState.randomSeed))
|
||||
.map(VigorXPApplier.withVigorXp)
|
||||
actionResultApplier.applyActionResults(currentState, actionResults)
|
||||
|
||||
case PLEASE_RECRUIT_ME =>
|
||||
case PleaseRecruitMe =>
|
||||
if availableCommandsFactory
|
||||
.hasAvailablePleaseRecruitMePhaseCommands(currentState)
|
||||
.hasAvailablePleaseRecruitMePhaseCommands(currentStateProto)
|
||||
then Vector.empty
|
||||
else
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
actionResultApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
EndPleaseRecruitMePhaseAction.fromProtoState(currentState).immediateExecute
|
||||
)
|
||||
EndPleaseRecruitMePhaseAction(currentState).immediateExecute
|
||||
)
|
||||
)
|
||||
|
||||
case VASSAL_COMMANDS =>
|
||||
case VassalCommands =>
|
||||
val vassalCommandResults = PerformVassalCommandsPhaseAction(
|
||||
gameState = GameStateConverter.fromProto(currentState),
|
||||
gameState = currentState,
|
||||
commandsForProvince = availableCommandsFactory
|
||||
.commandPhaseCommandsForProvince(currentState, _),
|
||||
commandFactory = commandFactory
|
||||
).execute(actionResultProtoApplier)
|
||||
.commandPhaseCommandsForProvince(currentStateProto, _),
|
||||
commandFactory = commandFactory,
|
||||
actionResultApplier = actionResultApplier
|
||||
).results(SeededRandom(currentState.randomSeed))
|
||||
.map(VigorXPApplier.withVigorXp)
|
||||
val appliedResults = actionResultApplier.applyActionResults(currentState, vassalCommandResults)
|
||||
|
||||
if vassalCommandResults.nonEmpty then vassalCommandResults
|
||||
if appliedResults.nonEmpty then appliedResults
|
||||
else
|
||||
EndVassalCommandsPhaseAction(GameStateConverter.fromProto(currentState)).execute(
|
||||
actionResultProtoApplier
|
||||
)
|
||||
val endResults = EndVassalCommandsPhaseAction(currentState, actionResultApplier)
|
||||
.results(SeededRandom(currentState.randomSeed))
|
||||
.map(VigorXPApplier.withVigorXp)
|
||||
actionResultApplier.applyActionResults(currentState, endResults)
|
||||
|
||||
case PLAYER_COMMANDS =>
|
||||
case PlayerCommands =>
|
||||
if availableCommandsFactory
|
||||
.hasAvailablePlayerCommandsPhaseCommands(
|
||||
currentState
|
||||
currentStateProto
|
||||
)
|
||||
then Vector.empty
|
||||
else
|
||||
actionResultProtoApplier.applyActionResults(
|
||||
actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
EndPlayerCommandsPhaseAction(
|
||||
currentState,
|
||||
ActionResultTApplierImpl(actionResultProtoApplier)
|
||||
).results(functionalRandom = SeededRandom(currentState.randomSeed))
|
||||
.map(
|
||||
ActionResultProtoConverter.toProto(_)
|
||||
)
|
||||
actionResultApplier
|
||||
).randomResults(functionalRandom = SeededRandom(currentState.randomSeed)).newValue
|
||||
)
|
||||
|
||||
case HOSTILE_ARMY_SETUP =>
|
||||
case HostileArmySetup =>
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
actionResultApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
PerformHostileArmySetupAction(
|
||||
GameStateConverter.fromProto(currentState)
|
||||
).immediateExecute
|
||||
)
|
||||
PerformHostileArmySetupAction(currentState).immediateExecute
|
||||
)
|
||||
)
|
||||
|
||||
case FREE_FOR_ALL_DECISION =>
|
||||
case FreeForAllDecision =>
|
||||
if availableCommandsFactory
|
||||
.hasAvailableFreeForAllDecisionPhaseCommands(
|
||||
currentState
|
||||
currentStateProto
|
||||
)
|
||||
then Vector.empty
|
||||
else
|
||||
// There may eventually be VassalAttackDecisions, but for now we're leaving that on the player
|
||||
actionResultProtoApplier.applyActionResults(
|
||||
actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
EndFreeForAllDecisionPhaseAction(currentState).results.map(
|
||||
ActionResultProtoConverter.toProto(_)
|
||||
)
|
||||
EndFreeForAllDecisionPhaseAction(currentState).results
|
||||
)
|
||||
|
||||
case FREE_FOR_ALL_BATTLE_REQUEST =>
|
||||
val requestResults = RequestFreeForAllBattlesAction(
|
||||
currentState
|
||||
).execute(actionResultProtoApplier)
|
||||
val latestState = requestResults.lastOption.map(_.gameState).getOrElse(currentState)
|
||||
requestResults :+ actionResultProtoApplier.applyActionResult(
|
||||
case FreeForAllBattleRequest =>
|
||||
val requestResults = actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
RequestFreeForAllBattlesAction(currentState).results
|
||||
)
|
||||
val latestState = requestResults.lastOption.map(_.resultingState).getOrElse(currentState)
|
||||
requestResults :+ actionResultApplier.applyActionResult(
|
||||
latestState,
|
||||
ActionResultProtoConverter.toProto(EndFreeForAllBattleRequestPhaseAction.immediateExecute)
|
||||
EndFreeForAllBattleRequestPhaseAction.immediateExecute
|
||||
)
|
||||
|
||||
case FREE_FOR_ALL_BATTLE_RESOLUTION =>
|
||||
case FreeForAllBattleResolution =>
|
||||
if currentState.outstandingBattles.isEmpty then
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
actionResultApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(EndFreeForAllBattleResolutionPhaseAction.immediateExecute)
|
||||
EndFreeForAllBattleResolutionPhaseAction.immediateExecute
|
||||
)
|
||||
)
|
||||
else Vector.empty // wait for battles to resolve
|
||||
|
||||
case UNCONTESTED_CONQUEST =>
|
||||
actionResultProtoApplier.applyActionResults(
|
||||
case UncontestedConquest =>
|
||||
actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
PerformUncontestedConquestAction(
|
||||
gameId = currentState.gameId,
|
||||
currentRoundId = currentState.currentRoundId,
|
||||
currentDate = DateConverter.fromProto(currentState.currentDate),
|
||||
provinces = currentState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.map(p => p.id -> p)
|
||||
.toMap,
|
||||
factions = currentState.factions.values
|
||||
.map(FactionConverter.fromProto)
|
||||
.map(f => f.id -> f)
|
||||
.toMap,
|
||||
heroes = currentState.heroes.values
|
||||
.map(HeroConverter.fromProto)
|
||||
.map(h => h.id -> h)
|
||||
.toMap,
|
||||
battalions = currentState.battalions.values
|
||||
.map(BattalionConverter.fromProto)
|
||||
.map(b => (b.id, b))
|
||||
.toMap
|
||||
currentDate = currentState.currentDate.get,
|
||||
provinces = currentState.provinces,
|
||||
factions = currentState.factions,
|
||||
heroes = currentState.heroes,
|
||||
battalions = currentState.battalions
|
||||
).results
|
||||
.map(ActionResultProtoConverter.toProto)
|
||||
)
|
||||
|
||||
case ATTACK_DECISION =>
|
||||
case AttackDecision =>
|
||||
if availableCommandsFactory.hasAvailableAttackDecisionPhaseCommands(
|
||||
currentState
|
||||
currentStateProto
|
||||
)
|
||||
then Vector.empty
|
||||
else {
|
||||
// There may eventually be VassalAttackDecisions, but for now we're leaving that on the player
|
||||
actionResultProtoApplier.applyActionResults(
|
||||
actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
EndAttackDecisionPhaseAction(
|
||||
gameId = currentState.gameId,
|
||||
currentRoundId = currentState.currentRoundId,
|
||||
currentDate = DateConverter.fromProto(currentState.currentDate),
|
||||
provinces = currentState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector
|
||||
).results.map(ActionResultProtoConverter.toProto)
|
||||
currentDate = currentState.currentDate.get,
|
||||
provinces = currentState.provinces.values.toVector
|
||||
).results
|
||||
)
|
||||
}
|
||||
|
||||
case DEFENSE_DECISION =>
|
||||
case DefenseDecision =>
|
||||
if availableCommandsFactory.hasAvailablePlayerDefenseCommands(
|
||||
currentState
|
||||
currentStateProto
|
||||
)
|
||||
then Vector.empty
|
||||
else {
|
||||
val vassalCommandResults = PerformVassalDefenseDecisionsAction(
|
||||
gameState = GameStateConverter.fromProto(currentState),
|
||||
gameState = currentState,
|
||||
commandsForProvince = availableCommandsFactory
|
||||
.defensePhaseCommandsForProvince(currentState, _),
|
||||
commandFactory = commandFactory
|
||||
).execute(actionResultProtoApplier)
|
||||
.defensePhaseCommandsForProvince(currentStateProto, _),
|
||||
commandFactory = commandFactory,
|
||||
actionResultApplier = actionResultApplier
|
||||
).results(SeededRandom(currentState.randomSeed))
|
||||
.map(VigorXPApplier.withVigorXp)
|
||||
val appliedResults = actionResultApplier.applyActionResults(currentState, vassalCommandResults)
|
||||
|
||||
if vassalCommandResults.nonEmpty then vassalCommandResults
|
||||
if appliedResults.nonEmpty then appliedResults
|
||||
else
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
actionResultApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
EndDefenseDecisionPhaseAction.fromProtoState(currentState).immediateExecute
|
||||
)
|
||||
EndDefenseDecisionPhaseAction(currentState).immediateExecute
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
case TRUCE_TURN_BACK =>
|
||||
TruceTurnBackPhaseAction(GameStateConverter.fromProto(currentState)).execute(actionResultProtoApplier)
|
||||
case TruceTurnBack =>
|
||||
val actionResults = TruceTurnBackPhaseAction(currentState, actionResultApplier)
|
||||
.results(SeededRandom(currentState.randomSeed))
|
||||
.map(VigorXPApplier.withVigorXp)
|
||||
actionResultApplier.applyActionResults(currentState, actionResults)
|
||||
|
||||
case BATTLE_REQUEST =>
|
||||
case BattleRequest =>
|
||||
val requestBattlesAction = RequestBattlesAction(
|
||||
gameId = currentState.gameId,
|
||||
currentRoundId = currentState.currentRoundId,
|
||||
currentDate = DateConverter.fromProto(currentState.currentDate),
|
||||
currentDate = currentState.currentDate.get,
|
||||
battleCounter = currentState.battleCounter,
|
||||
heroes = currentState.heroes.view.mapValues(HeroConverter.fromProto).toMap,
|
||||
battalions = currentState.battalions.view
|
||||
.mapValues(BattalionConverter.fromProto)
|
||||
.toMap,
|
||||
provinces = currentState.provinces.view
|
||||
.mapValues(ProvinceConverter.fromProto)
|
||||
.toMap,
|
||||
factions = currentState.factions.view
|
||||
.mapValues(FactionConverter.fromProto)
|
||||
.toMap,
|
||||
battalionTypes = currentState.battalionTypes.map { bt =>
|
||||
val converted = BattalionTypeConverter.fromProto(bt)
|
||||
converted.typeId -> converted
|
||||
}.toMap
|
||||
heroes = currentState.heroes,
|
||||
battalions = currentState.battalions,
|
||||
provinces = currentState.provinces,
|
||||
factions = currentState.factions,
|
||||
battalionTypes = currentState.battalionTypes.map(bt => bt.typeId -> bt).toMap
|
||||
)
|
||||
val requestResults = actionResultProtoApplier.applyActionResults(
|
||||
val requestResults = actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
requestBattlesAction.results.map(
|
||||
ActionResultProtoConverter.toProto(_)
|
||||
)
|
||||
requestBattlesAction.results
|
||||
)
|
||||
val latestState = requestResults.lastOption.map(_.gameState).getOrElse(currentState)
|
||||
requestResults :+ actionResultProtoApplier.applyActionResult(
|
||||
val latestState = requestResults.lastOption.map(_.resultingState).getOrElse(currentState)
|
||||
requestResults :+ actionResultApplier.applyActionResult(
|
||||
latestState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
EndBattleRequestPhaseAction.fromProtoState(latestState).immediateExecute
|
||||
)
|
||||
EndBattleRequestPhaseAction(latestState).immediateExecute
|
||||
)
|
||||
|
||||
case FOOD_CONSUMPTION =>
|
||||
case FoodConsumption =>
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
actionResultApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
PerformFoodConsumptionPhaseAction(GameStateConverter.fromProto(currentState)).immediateExecute
|
||||
)
|
||||
PerformFoodConsumptionPhaseAction(currentState).immediateExecute
|
||||
)
|
||||
)
|
||||
|
||||
case BATTLE_RESOLUTION =>
|
||||
case BattleResolution =>
|
||||
if currentState.outstandingBattles.isEmpty then
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
actionResultApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(EndBattleResolutionPhaseAction.immediateExecute)
|
||||
EndBattleResolutionPhaseAction.immediateExecute
|
||||
)
|
||||
)
|
||||
else Vector.empty // wait for battles to resolve
|
||||
|
||||
case BATTLE_AFTERMATH =>
|
||||
case BattleAftermath =>
|
||||
if currentState.provinces.values
|
||||
.flatMap(_.capturedHeroes)
|
||||
.isEmpty
|
||||
then
|
||||
actionResultProtoApplier.applyActionResults(
|
||||
actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
EndBattleAftermathPhaseAction(
|
||||
currentState,
|
||||
ActionResultTApplierImpl(actionResultProtoApplier)
|
||||
actionResultApplier
|
||||
)
|
||||
.randomResults(
|
||||
SeededRandom(currentState.randomSeed)
|
||||
)
|
||||
.newValue
|
||||
.map(ActionResultProtoConverter.toProto)
|
||||
)
|
||||
else Vector.empty
|
||||
|
||||
case DIPLOMACY_RESOLUTION =>
|
||||
case DiplomacyResolution =>
|
||||
if availableCommandsFactory.hasAvailablePlayerCommands(
|
||||
currentState
|
||||
currentStateProto
|
||||
)
|
||||
then Vector.empty
|
||||
else
|
||||
actionResultProtoApplier.applyActionResults(
|
||||
actionResultApplier.applyActionResults(
|
||||
currentState,
|
||||
EndDiplomacyResolutionPhaseAction(
|
||||
currentState,
|
||||
actionResultTApplier = ActionResultTApplierImpl(actionResultProtoApplier)
|
||||
).randomResults(SeededRandom(currentState.randomSeed))
|
||||
.newValue
|
||||
.map(ActionResultProtoConverter.toProto)
|
||||
actionResultApplier = actionResultApplier
|
||||
).randomResults(SeededRandom(currentState.randomSeed)).newValue
|
||||
)
|
||||
|
||||
case RECON_RESOLUTION =>
|
||||
PerformReconResolutionAction(GameStateConverter.fromProto(currentState)).execute(
|
||||
actionResultProtoApplier
|
||||
)
|
||||
|
||||
case Unrecognized(x) =>
|
||||
throw new IllegalStateException(s"Unknown round phase $x")
|
||||
case ReconResolution =>
|
||||
val actionResults = PerformReconResolutionAction(currentState, actionResultApplier)
|
||||
.results(SeededRandom(currentState.randomSeed))
|
||||
.map(VigorXPApplier.withVigorXp)
|
||||
actionResultApplier.applyActionResults(currentState, actionResults)
|
||||
}
|
||||
val timeSpent = System.currentTimeMillis - startTime
|
||||
val timeSpent = System.currentTimeMillis - startTime
|
||||
times(currentPhase) = times(currentPhase) + timeSpent
|
||||
|
||||
validateResults(results, currentState, availableCommandsFactory)
|
||||
validateResults(results, currentState, currentStateProto, 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[ActionWithResultingState],
|
||||
results: Vector[ActionResultWithResultingState],
|
||||
startingState: GameState,
|
||||
startingStateProto: GameStateProto,
|
||||
availableCommandsFactory: AvailableCommandsFactory
|
||||
): Vector[ActionWithResultingState] =
|
||||
): Vector[ActionResultWithResultingState] =
|
||||
internalValidated(
|
||||
results,
|
||||
(r: Vector[ActionWithResultingState]) =>
|
||||
(r: Vector[ActionResultWithResultingState]) =>
|
||||
r.nonEmpty ||
|
||||
startingState.outstandingBattles.nonEmpty ||
|
||||
availableCommandsFactory.hasAvailablePlayerCommands(startingState),
|
||||
availableCommandsFactory.hasAvailablePlayerCommands(startingStateProto),
|
||||
"No results were found, but we also don't seem to be waiting for anything"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
case class ActionResultWithResultingState(
|
||||
actionResult: ActionResultT,
|
||||
resultingState: GameState
|
||||
)
|
||||
|
||||
trait ActionResultApplier {
|
||||
def xpForStatBump(stat: Int): Int
|
||||
|
||||
def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
): Vector[ActionResultWithResultingState]
|
||||
|
||||
def applyActionResult(
|
||||
startingState: GameState,
|
||||
result: ActionResultT
|
||||
): ActionResultWithResultingState
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.library.util.validations.ScalaValidator
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
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 extension methods
|
||||
import GameStateExtensions.*
|
||||
|
||||
/**
|
||||
* Implementation of ActionResultApplier that applies ActionResultT directly to Scala GameState.
|
||||
*
|
||||
* This uses extension methods on GameState to apply each type of change.
|
||||
*/
|
||||
object ActionResultApplierImpl {
|
||||
def apply(validator: Option[ScalaValidator]): ActionResultApplierImpl = new ActionResultApplierImpl(validator)
|
||||
|
||||
// Type class for single-argument validation
|
||||
trait Validatable[T]:
|
||||
def validate(v: ScalaValidator, value: T): T
|
||||
|
||||
given Validatable[HeroT] with
|
||||
def validate(v: ScalaValidator, value: HeroT): HeroT = v.validate(value)
|
||||
|
||||
given Validatable[GameState] with
|
||||
def validate(v: ScalaValidator, value: GameState): GameState = v.validate(value)
|
||||
|
||||
given Validatable[ActionResultT] with
|
||||
def validate(v: ScalaValidator, value: ActionResultT): ActionResultT = v.validate(value)
|
||||
|
||||
// Type class for validation with GameState context
|
||||
trait ValidatableWithGameState[T]:
|
||||
def validate(v: ScalaValidator, value: T, gs: GameState): T
|
||||
|
||||
given ValidatableWithGameState[BattalionT] with
|
||||
def validate(v: ScalaValidator, value: BattalionT, gs: GameState): BattalionT = v.validate(value, gs)
|
||||
}
|
||||
|
||||
class ActionResultApplierImpl(validator: Option[ScalaValidator]) extends ActionResultApplier {
|
||||
import ActionResultApplierImpl.{Validatable, ValidatableWithGameState, given}
|
||||
|
||||
// Generic single-argument validate
|
||||
def validate[T: Validatable](value: T): T =
|
||||
validator.fold(value)(v => summon[Validatable[T]].validate(v, value))
|
||||
|
||||
// Generic validate with GameState context
|
||||
def validate[T: ValidatableWithGameState](value: T, gs: GameState): T =
|
||||
validator.fold(value)(v => summon[ValidatableWithGameState[T]].validate(v, value, gs))
|
||||
|
||||
// Province validation with RoundPhase (unique signature, no type class needed)
|
||||
def validate(province: ProvinceT, roundPhase: RoundPhase): ProvinceT =
|
||||
validator.fold(province)(_.validate(province, roundPhase))
|
||||
|
||||
override def xpForStatBump(stat: Int): Int = GameStateExtensions.xpForStatBump(stat)
|
||||
|
||||
override def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
): Vector[ActionResultWithResultingState] =
|
||||
results
|
||||
.foldLeft((startingState, Vector.empty[ActionResultWithResultingState])) {
|
||||
case ((gameState, acc), result) =>
|
||||
val awrs = applyActionResult(gameState, result)
|
||||
(awrs.resultingState, acc :+ awrs)
|
||||
}
|
||||
._2
|
||||
|
||||
override def applyActionResult(
|
||||
startingState: GameState,
|
||||
result: ActionResultT
|
||||
): ActionResultWithResultingState = {
|
||||
// Validate the action result
|
||||
validate(result)
|
||||
|
||||
// Get date for applying hero changes - use newDate if present, otherwise current date from state
|
||||
val date: Date = result.newDate
|
||||
.orElse(startingState.currentDate)
|
||||
.getOrElse(
|
||||
throw new IllegalStateException("No date available for applying action result")
|
||||
)
|
||||
|
||||
// Validate new heroes before applying
|
||||
result.newHeroes.foreach(validate)
|
||||
|
||||
// Apply basic state updates
|
||||
val stateAfterBasics = applyBasicStateUpdates(startingState, result)
|
||||
|
||||
// Apply new provinces with validation
|
||||
val stateAfterNewProvinces = result.newProvinces.foldLeft(stateAfterBasics) { (gs, np) =>
|
||||
validate(np, gs.currentPhase)
|
||||
gs.applyNewProvinces(Vector(np))
|
||||
}
|
||||
|
||||
// Apply changed provinces with validation
|
||||
val stateAfterChangedProvinces = result.changedProvinces.foldLeft(stateAfterNewProvinces) { (gs, cp) =>
|
||||
val after = gs.applyChangedProvinces(Vector(cp))
|
||||
validate(after.provinces(cp.provinceId), gs.currentPhase)
|
||||
after
|
||||
}
|
||||
|
||||
// Apply changed battalions with validation
|
||||
val stateAfterChangedBattalions = stateAfterChangedProvinces.applyChangedBattalions(result.changedBattalions)
|
||||
result.changedBattalions.foreach { cb =>
|
||||
stateAfterChangedBattalions.battalions.get(cb.battalionId).foreach { b =>
|
||||
validate(b, stateAfterChangedBattalions)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply all remaining entity changes using extension methods
|
||||
val finalState = stateAfterChangedBattalions
|
||||
.applyProvinceActed(result.provinceIdActed)
|
||||
.applyLastCommand(result.provinceId, result.lastCommandTypeForActingProvince)
|
||||
.applyNewBattalions(result.newBattalions, result.provinceId)
|
||||
.applyDestroyedBattalionIds(result.destroyedBattalionIds)
|
||||
.applyNewHeroes(result.newHeroes)
|
||||
.applyChangedHeroes(result.changedHeroes, date)
|
||||
.applyRemovedHeroes(result.removedHeroIds.toSet)
|
||||
.applyChangedFactions(result.changedFactions)
|
||||
.applyRemovedFactions(result.removedFactionIds.toSet)
|
||||
.applyNewFactions(result.newFactions)
|
||||
.applyNewBattle(result.newBattle)
|
||||
.applyNewNotifications(result.newNotifications)
|
||||
.applyRemovedNotifications(result.removedNotifications)
|
||||
.applyNewSeed(result.newRandomSeed)
|
||||
.applyChronicleEntry(result.newChronicleEntry)
|
||||
.applyCommandCountUpdate(result.actingFactionId)
|
||||
|
||||
// Validate final game state
|
||||
validate(finalState)
|
||||
|
||||
ActionResultWithResultingState(
|
||||
actionResult = result,
|
||||
resultingState = finalState
|
||||
)
|
||||
}
|
||||
|
||||
private def applyBasicStateUpdates(state: GameState, result: ActionResultT): GameState = {
|
||||
val afterRoundId = result.newRoundId
|
||||
.map(rid => state.copy(currentRoundId = rid))
|
||||
.getOrElse(state)
|
||||
|
||||
val afterPhase = result.newRoundPhase
|
||||
.map(phase => afterRoundId.copy(currentPhase = phase))
|
||||
.getOrElse(afterRoundId)
|
||||
|
||||
val afterDate = result.newDate
|
||||
.map(date => afterPhase.copy(currentDate = Some(date)))
|
||||
.getOrElse(afterPhase)
|
||||
|
||||
val afterRunStatus =
|
||||
if result.gameEnded.contains(true) then afterDate.copy(runStatus = RunStatus.Over)
|
||||
else afterDate
|
||||
|
||||
val afterVictor = result.newVictorFactionId
|
||||
.map(factionId => afterRunStatus.copy(victor = Some(factionId)))
|
||||
.getOrElse(afterRunStatus)
|
||||
|
||||
afterVictor.copy(actionResultCount = afterVictor.actionResultCount + 1)
|
||||
}
|
||||
}
|
||||
+16
-12
@@ -1,35 +1,39 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.validations.Validator
|
||||
import net.eagle0.eagle.library.util.validations.ScalaValidator
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
|
||||
object ActionResultTApplierImpl {
|
||||
def apply(validator: Validator): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(new ActionResultProtoApplierImpl(validator))
|
||||
/** Creates an ActionResultTApplierImpl with no validation (for tests) */
|
||||
def apply(): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(new ActionResultApplierImpl(None))
|
||||
|
||||
def apply(protoApplier: ActionResultProtoApplier): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(protoApplier)
|
||||
def apply(validator: ScalaValidator): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(new ActionResultApplierImpl(Some(validator)))
|
||||
|
||||
def apply(baseApplier: ActionResultApplier): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(baseApplier)
|
||||
}
|
||||
|
||||
class ActionResultTApplierImpl(protoApplier: ActionResultProtoApplier) extends ActionResultTApplier {
|
||||
override def xpForStatBump(stat: Int): Int = protoApplier.xpForStatBump(stat)
|
||||
class ActionResultTApplierImpl(baseApplier: ActionResultApplier) extends ActionResultTApplier {
|
||||
override def xpForStatBump(stat: Int): Int = baseApplier.xpForStatBump(stat)
|
||||
|
||||
override def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
): Vector[ActionResultTWithResultingState] = protoApplier
|
||||
): Vector[ActionResultTWithResultingState] = baseApplier
|
||||
.applyActionResults(
|
||||
startingState,
|
||||
results.map(ActionResultProtoConverter.toProto)
|
||||
GameStateConverter.fromProto(startingState),
|
||||
results
|
||||
)
|
||||
.zip(results)
|
||||
.map {
|
||||
case (actionWithResultingState, actionResult) =>
|
||||
ActionResultTWithResultingState(
|
||||
actionResult = actionResult,
|
||||
resultingState = actionWithResultingState.gameState
|
||||
resultingState = GameStateConverter.toProto(actionWithResultingState.resultingState)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,211 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_library")
|
||||
|
||||
scala_library(
|
||||
name = "action_result_applier",
|
||||
srcs = ["ActionResultApplier.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "province_update_helpers",
|
||||
srcs = ["ProvinceUpdateHelpers.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "province_update_helpers2",
|
||||
srcs = ["ProvinceUpdateHelpers2.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//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",
|
||||
"//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/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_province_extensions",
|
||||
srcs = ["GameStateProvinceExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
":province_update_helpers",
|
||||
":province_update_helpers2",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_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/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//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/province/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_battalion_extensions",
|
||||
srcs = ["GameStateBattalionExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
"//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/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_hero_extensions",
|
||||
srcs = ["GameStateHeroExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//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/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/backstory_version",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_faction_extensions",
|
||||
srcs = ["GameStateFactionExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_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/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
|
||||
"//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",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_battle_extensions",
|
||||
srcs = ["GameStateBattleExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_misc_extensions",
|
||||
srcs = ["GameStateMiscExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_extensions",
|
||||
srcs = ["GameStateExtensions.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
":game_state_battalion_extensions",
|
||||
":game_state_battle_extensions",
|
||||
":game_state_faction_extensions",
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//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/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":game_state_battalion_extensions",
|
||||
":game_state_battle_extensions",
|
||||
":game_state_faction_extensions",
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
|
||||
"//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/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "action_result_applier_impl",
|
||||
srcs = ["ActionResultApplierImpl.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
":action_result_applier",
|
||||
":game_state_extensions",
|
||||
],
|
||||
deps = [
|
||||
":action_result_applier",
|
||||
":game_state_extensions",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_validator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//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",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "action_result_proto_applier",
|
||||
srcs = ["ActionResultProtoApplier.scala"],
|
||||
@@ -9,11 +215,10 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//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:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
# "@maven//:com_thesamet_scalapb_lenses_3",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -27,23 +232,22 @@ scala_library(
|
||||
exports = [":action_result_proto_applier"],
|
||||
deps = [
|
||||
":action_result_proto_applier",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//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_relationship_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:run_status_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:run_status_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/settings:xp_for_stat_bump",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_event_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:runtime_validator",
|
||||
# "@maven//:com_thesamet_scalapb_lenses_3",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -66,7 +270,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
# "@maven//:com_thesamet_scalapb_lenses_3",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -78,25 +281,28 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
":action_result_applier",
|
||||
":action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:runtime_validator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_validator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":action_result_proto_applier_impl",
|
||||
":action_result_applier",
|
||||
":action_result_applier_impl",
|
||||
":action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//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:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_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:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:runtime_validator",
|
||||
"//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",
|
||||
# "@maven//:com_thesamet_scalapb_lenses_3",
|
||||
"//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/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.concrete.ChangedBattalionC
|
||||
import net.eagle0.eagle.model.action_result.ChangedBattalionT
|
||||
import net.eagle0.eagle.model.state.{Army, CombatUnit, MovingArmy}
|
||||
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
|
||||
object GameStateBattalionExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyNewBattalions(
|
||||
newBattalions: Vector[BattalionT],
|
||||
provinceId: Option[ProvinceId]
|
||||
): GameState =
|
||||
if newBattalions.isEmpty then gameState
|
||||
else
|
||||
provinceId.map { pid =>
|
||||
val maxCurrentId =
|
||||
if gameState.battalions.isEmpty then 0
|
||||
else gameState.battalions.keys.max
|
||||
val newIds = (maxCurrentId + 1) to (maxCurrentId + newBattalions.size)
|
||||
val newBattalionsWithIds = newIds
|
||||
.zip(newBattalions)
|
||||
.map {
|
||||
case (id, batt) =>
|
||||
id -> assignBattalionId(batt, id)
|
||||
}
|
||||
.toMap
|
||||
val province = gameState.provinces(pid) match {
|
||||
case p: ProvinceC => p
|
||||
case p => throw new EagleInternalException(s"Unknown ProvinceT type: ${p.getClass}")
|
||||
}
|
||||
gameState.copy(
|
||||
battalions = gameState.battalions ++ newBattalionsWithIds,
|
||||
provinces = gameState.provinces.updated(
|
||||
pid,
|
||||
province.copy(battalionIds = province.battalionIds ++ newIds)
|
||||
)
|
||||
)
|
||||
}
|
||||
.getOrElse(
|
||||
gameState.copy(
|
||||
battalions = gameState.battalions ++ newBattalions.map(b => b.id -> b)
|
||||
)
|
||||
)
|
||||
|
||||
def applyChangedBattalions(changedBattalions: Vector[ChangedBattalionT]): GameState =
|
||||
if changedBattalions.isEmpty then gameState
|
||||
else {
|
||||
// Partition based on whether the target battalion has size 0
|
||||
val (zero, nonzero) = changedBattalions.partition {
|
||||
case cb: ChangedBattalionC => cb.to.size == 0
|
||||
case _ => false
|
||||
}
|
||||
val afterDestroyed = applyDestroyedBattalionIds(zero.map(_.battalionId))
|
||||
|
||||
val updatedBattalions = nonzero.collect {
|
||||
case cb: ChangedBattalionC =>
|
||||
val existingName = afterDestroyed.battalions.get(cb.battalionId).map(_.name)
|
||||
cb.battalionId -> preserveName(cb.to, existingName)
|
||||
}.toMap
|
||||
|
||||
afterDestroyed.copy(
|
||||
battalions = afterDestroyed.battalions ++ updatedBattalions
|
||||
)
|
||||
}
|
||||
|
||||
def applyDestroyedBattalionIds(destroyedBattalionIds: Vector[BattalionId]): GameState =
|
||||
if destroyedBattalionIds.isEmpty then gameState
|
||||
else {
|
||||
val destroyedBattalions = destroyedBattalionIds
|
||||
.filterNot(_ == -1)
|
||||
.flatMap(bid => gameState.battalions.get(bid).map(bid -> _))
|
||||
.toMap
|
||||
|
||||
val updatedProvinces = gameState.provinces.map {
|
||||
case (pid, province) =>
|
||||
province match {
|
||||
case p: ProvinceC =>
|
||||
pid -> p.copy(
|
||||
battalionIds = p.battalionIds.filterNot(destroyedBattalionIds.contains),
|
||||
incomingArmies = p.incomingArmies.map(a => movingArmyWithoutBattalions(a, destroyedBattalionIds))
|
||||
)
|
||||
case p => pid -> p
|
||||
}
|
||||
}
|
||||
|
||||
gameState.copy(
|
||||
battalions = gameState.battalions -- destroyedBattalionIds,
|
||||
destroyedBattalions = gameState.destroyedBattalions ++ destroyedBattalions,
|
||||
provinces = updatedProvinces
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def assignBattalionId(batt: BattalionT, id: BattalionId): BattalionT = batt match {
|
||||
case b: BattalionC => b.copy(id = id)
|
||||
case b => throw new EagleInternalException(s"Unknown BattalionT type: ${b.getClass}")
|
||||
}
|
||||
|
||||
private def preserveName(batt: BattalionT, existingName: Option[String]): BattalionT =
|
||||
existingName match {
|
||||
case Some(name) =>
|
||||
batt match {
|
||||
case b: BattalionC => b.copy(name = name)
|
||||
case b => b
|
||||
}
|
||||
case None => batt
|
||||
}
|
||||
|
||||
private def movingArmyWithoutBattalions(
|
||||
movingArmy: MovingArmy,
|
||||
removedBattalionIds: Vector[BattalionId]
|
||||
): MovingArmy = {
|
||||
val newUnits = movingArmy.army.units.map(unitWithRemovedBattalions(_, removedBattalionIds))
|
||||
if newUnits.isEmpty then movingArmy.copy(army = movingArmy.army.copy(units = Vector.empty))
|
||||
else movingArmy.copy(army = movingArmy.army.copy(units = newUnits))
|
||||
}
|
||||
|
||||
private def unitWithRemovedBattalions(
|
||||
u: CombatUnit,
|
||||
removedBattalionIds: Vector[BattalionId]
|
||||
): CombatUnit =
|
||||
u.battalionId match {
|
||||
case Some(bid) if removedBattalionIds.contains(bid) => u.copy(battalionId = None)
|
||||
case _ => u
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.shardok_battle.ShardokBattle
|
||||
|
||||
object GameStateBattleExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyNewBattle(battle: Option[ShardokBattle]): GameState =
|
||||
battle.map { b =>
|
||||
gameState.copy(
|
||||
outstandingBattles = gameState.outstandingBattles :+ b,
|
||||
battleCounter = gameState.battleCounter.max(b.battleIndex)
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
|
||||
def applyResolvedBattle(shardokGameId: Option[ShardokGameId]): GameState =
|
||||
gameState.copy(
|
||||
outstandingBattles = gameState.outstandingBattles.filterNot(batt => shardokGameId.contains(batt.shardokGameId))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.library.settings.{ExtraXpForStatBumpOver100, XpForStatBump}
|
||||
export GameStateBattalionExtensions.*
|
||||
export GameStateBattleExtensions.*
|
||||
export GameStateFactionExtensions.*
|
||||
export GameStateHeroExtensions.*
|
||||
export GameStateMiscExtensions.*
|
||||
// Re-export all extension imports for convenient single import
|
||||
export GameStateProvinceExtensions.*
|
||||
|
||||
object GameStateExtensions {
|
||||
|
||||
def xpForStatBump(stat: Int): Int =
|
||||
if stat <= 99 then XpForStatBump.intValue
|
||||
else XpForStatBump.intValue + ExtraXpForStatBumpOver100.intValue * (stat - 99)
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.concrete.ChangedFactionC
|
||||
import net.eagle0.eagle.model.action_result.ChangedFactionT
|
||||
import net.eagle0.eagle.model.state.faction.concrete.FactionC
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.faction.FactionT.OutgoingOfferRound
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.views.province_view.ProvinceView
|
||||
|
||||
object GameStateFactionExtensions {
|
||||
|
||||
private val trustMax: Int = 100
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyChangedFactions(changedFactions: Vector[ChangedFactionT]): GameState =
|
||||
if changedFactions.isEmpty then gameState
|
||||
else
|
||||
changedFactions.foldLeft(gameState) {
|
||||
case (gs, cf) =>
|
||||
gs.applyChangedFaction(cf)
|
||||
}
|
||||
|
||||
def applyChangedFaction(cf: ChangedFactionT): GameState = cf match {
|
||||
case cfc: ChangedFactionC => applyChangedFactionC(cfc)
|
||||
case _ => throw new EagleInternalException(s"Unknown ChangedFactionT type: ${cf.getClass}")
|
||||
}
|
||||
|
||||
private def applyChangedFactionC(cf: ChangedFactionC): GameState =
|
||||
if !gameState.factions.contains(cf.factionId) then gameState
|
||||
else {
|
||||
val existingFaction = gameState.factions(cf.factionId) match {
|
||||
case f: FactionC => f
|
||||
case f => throw new EagleInternalException(s"Unknown FactionT type: ${f.getClass}")
|
||||
}
|
||||
|
||||
val newFocusProvinceId =
|
||||
if cf.clearFocusProvinceId then None
|
||||
else cf.newFocusProvinceId.orElse(existingFaction.focusProvinceId)
|
||||
|
||||
val newLastActedProvinceIdThisRound =
|
||||
if cf.clearLastActedProvinceId then 0
|
||||
else cf.newLastActedProvinceId.getOrElse(existingFaction.lastActedProvinceIdThisRound)
|
||||
|
||||
// Update relationships with trust level changes
|
||||
val relationshipsAfterChanges = cf.changedFactionRelationships
|
||||
.foldLeft(
|
||||
existingFaction.factionRelationships
|
||||
.filterNot(fr =>
|
||||
cf.changedFactionRelationships.map(_.targetFactionId).contains(fr.targetFactionId) ||
|
||||
cf.removedFactionRelationshipFactionIds.contains(fr.targetFactionId)
|
||||
) ++ cf.changedFactionRelationships
|
||||
) { (relationships, _) =>
|
||||
relationships
|
||||
}
|
||||
|
||||
// Apply trust level updates
|
||||
val relationshipsAfterTrust = cf.trustLevelUpdates.foldLeft(relationshipsAfterChanges) {
|
||||
case (relationships, update) =>
|
||||
val existingRelationship = relationships
|
||||
.find(_.targetFactionId == update.targetFactionId)
|
||||
.getOrElse(
|
||||
FactionRelationship(
|
||||
targetFactionId = update.targetFactionId,
|
||||
relationshipLevel = FactionRelationship.RelationshipLevel.Hostile,
|
||||
trustValue = 0
|
||||
)
|
||||
)
|
||||
val newValue = Math.min(trustMax, update.delta + existingRelationship.trustValue)
|
||||
relationships.filterNot(_.targetFactionId == update.targetFactionId) :+
|
||||
existingRelationship.copy(trustValue = newValue)
|
||||
}
|
||||
|
||||
// Update reconned provinces
|
||||
val newReconnedProvinces = existingFaction.reconnedProvinces
|
||||
.filterNot(pv =>
|
||||
cf.updatedReconnedProvinces.map(_.id).contains(pv.id) ||
|
||||
cf.removedReconnedProvinceIds.contains(pv.id)
|
||||
) ++ cf.updatedReconnedProvinces
|
||||
|
||||
// Update outgoing offer rounds
|
||||
val newLastOutgoingTruceOfferRounds = existingFaction.lastOutgoingTruceOfferRounds
|
||||
.filterNot(oor => cf.newOutgoingTruceOfferFactionIds.map(_.fid).contains(oor.toFactionId)) ++
|
||||
cf.newOutgoingTruceOfferFactionIds.map(wrapper =>
|
||||
OutgoingOfferRound(toFactionId = wrapper.fid, roundId = gameState.currentRoundId)
|
||||
)
|
||||
|
||||
val newLastOutgoingAllianceOfferRounds = existingFaction.lastOutgoingAllianceOfferRounds
|
||||
.filterNot(oor => cf.newOutgoingAllianceOfferFactionIds.map(_.fid).contains(oor.toFactionId)) ++
|
||||
cf.newOutgoingAllianceOfferFactionIds.map(wrapper =>
|
||||
OutgoingOfferRound(toFactionId = wrapper.fid, roundId = gameState.currentRoundId)
|
||||
)
|
||||
|
||||
val newLastOutgoingInvitationRounds = existingFaction.lastOutgoingInvitationRounds
|
||||
.filterNot(oor => cf.newOutgoingInvitationFactionIds.map(_.fid).contains(oor.toFactionId)) ++
|
||||
cf.newOutgoingInvitationFactionIds.map(wrapper =>
|
||||
OutgoingOfferRound(toFactionId = wrapper.fid, roundId = gameState.currentRoundId)
|
||||
)
|
||||
|
||||
val newLastOutgoingRansomOfferRounds = existingFaction.lastOutgoingRansomOfferRounds
|
||||
.filterNot(oor => cf.newOutgoingRansomOfferFactionIds.map(_.fid).contains(oor.toFactionId)) ++
|
||||
cf.newOutgoingRansomOfferFactionIds.map(wrapper =>
|
||||
OutgoingOfferRound(toFactionId = wrapper.fid, roundId = gameState.currentRoundId)
|
||||
)
|
||||
|
||||
val updatedFaction = existingFaction.copy(
|
||||
factionHeadId = cf.newFactionHeadHeroId.getOrElse(existingFaction.factionHeadId),
|
||||
leaderIds = existingFaction.leaderIds.diff(cf.removedLeaderHeroIds) ++ cf.newLeaderHeroIds,
|
||||
prestigeModifiers =
|
||||
existingFaction.prestigeModifiers.diff(cf.removedPrestigeModifiers) ++ cf.newPrestigeModifiers,
|
||||
factionRelationships = relationshipsAfterTrust,
|
||||
incomingDiplomacyOffers = existingFaction.incomingDiplomacyOffers
|
||||
.filterNot(offer => cf.removedIncomingDiplomacyOfferFactionIds.contains(offer.originatingFactionId)) ++
|
||||
cf.newIncomingDiplomacyOffers,
|
||||
focusProvinceId = newFocusProvinceId,
|
||||
reconnedProvinces = newReconnedProvinces,
|
||||
lastActedProvinceIdThisRound = newLastActedProvinceIdThisRound,
|
||||
lastOutgoingTruceOfferRounds = newLastOutgoingTruceOfferRounds,
|
||||
lastOutgoingAllianceOfferRounds = newLastOutgoingAllianceOfferRounds,
|
||||
lastOutgoingInvitationRounds = newLastOutgoingInvitationRounds,
|
||||
lastOutgoingRansomOfferRounds = newLastOutgoingRansomOfferRounds
|
||||
)
|
||||
|
||||
gameState.copy(
|
||||
factions = gameState.factions.updated(cf.factionId, updatedFaction)
|
||||
)
|
||||
}
|
||||
|
||||
def applyRemovedFactions(removedFactionIds: Set[FactionId]): GameState = {
|
||||
val (removed, remaining) = gameState.factions.partition {
|
||||
case (fid, _) =>
|
||||
removedFactionIds.contains(fid)
|
||||
}
|
||||
gameState.copy(
|
||||
destroyedFactions = gameState.destroyedFactions ++ removed,
|
||||
factions = remaining
|
||||
)
|
||||
}
|
||||
|
||||
def applyNewFactions(newFactions: Vector[FactionT]): GameState =
|
||||
gameState.copy(
|
||||
factions = gameState.factions ++ newFactions.map(f => f.id -> f)
|
||||
)
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ChangedHeroC, StatAbsolute, StatDelta, StatNoChange}
|
||||
import net.eagle0.eagle.model.action_result.ChangedHeroT
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.backstory_version.BackstoryVersion
|
||||
import net.eagle0.eagle.model.state.hero.concrete.HeroC
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
|
||||
object GameStateHeroExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyNewHeroes(newHeroes: Vector[HeroT]): GameState =
|
||||
if newHeroes.isEmpty then gameState
|
||||
else {
|
||||
newHeroes.foreach(h =>
|
||||
internalRequire(
|
||||
!gameState.heroes.contains(h.id),
|
||||
s"Got a new hero update for existing hero ${h.id}"
|
||||
)
|
||||
)
|
||||
gameState.copy(
|
||||
heroes = gameState.heroes ++ newHeroes.map(h => h.id -> h)
|
||||
)
|
||||
}
|
||||
|
||||
def applyChangedHeroes(changedHeroes: Vector[ChangedHeroT], date: Date): GameState =
|
||||
if changedHeroes.isEmpty then gameState
|
||||
else
|
||||
changedHeroes.foldLeft(gameState) {
|
||||
case (gs, ch) =>
|
||||
gs.applyChangedHero(ch, date)
|
||||
}
|
||||
|
||||
def applyChangedHero(ch: ChangedHeroT, date: Date): GameState = ch match {
|
||||
case chc: ChangedHeroC => applyChangedHeroC(chc, date)
|
||||
case _ => throw new EagleInternalException(s"Unknown ChangedHeroT type: ${ch.getClass}")
|
||||
}
|
||||
|
||||
private def applyChangedHeroC(ch: ChangedHeroC, date: Date): GameState = {
|
||||
val existingHero = gameState.heroes(ch.heroId) match {
|
||||
case h: HeroC => h
|
||||
case h => throw new EagleInternalException(s"Unknown HeroT type: ${h.getClass}")
|
||||
}
|
||||
|
||||
val newFactionId = ch.newFactionId.orElse(
|
||||
if ch.clearFactionId then None else existingHero.factionId
|
||||
)
|
||||
|
||||
val newRoundIdJoined =
|
||||
if ch.newFactionId.isDefined then Some(gameState.currentRoundId)
|
||||
else if ch.clearFactionId then None
|
||||
else existingHero.roundIdJoined
|
||||
|
||||
val newVigor = ch.vigorChange match {
|
||||
case StatDelta(d) =>
|
||||
(existingHero.vigor + d).max(0.0).min(existingHero.constitution)
|
||||
case StatAbsolute(va) =>
|
||||
internalRequire(va >= 0, s"Got a negative new vigor of $va")
|
||||
internalRequire(
|
||||
va <= existingHero.constitution,
|
||||
s"Got a new vigor of $va with ${existingHero.constitution} constitution"
|
||||
)
|
||||
va
|
||||
case StatNoChange => existingHero.vigor
|
||||
}
|
||||
|
||||
val newLoyalty = ch.loyaltyChange match {
|
||||
case StatDelta(ld) =>
|
||||
Math.min(100.0, Math.max(0, existingHero.loyalty + ld))
|
||||
case StatAbsolute(la) =>
|
||||
internalRequire(la >= 0, s"Got a negative absolute loyalty of $la")
|
||||
internalRequire(la <= 100.0, s"Got an absolute loyalty of $la")
|
||||
la
|
||||
case StatNoChange => existingHero.loyalty
|
||||
}
|
||||
|
||||
val newBackstoryVersions = existingHero.backstoryVersions ++ ch.newBackstoryTextId.map { textId =>
|
||||
BackstoryVersion(textId = textId, date = date)
|
||||
}
|
||||
|
||||
val newBackstoryEvents =
|
||||
if ch.clearEventsForHeroBackstory then Vector.empty
|
||||
else existingHero.backstoryEvents ++ ch.newEventsForHeroBackstory
|
||||
|
||||
val updatedHero = existingHero.copy(
|
||||
factionId = newFactionId,
|
||||
roundIdJoined = newRoundIdJoined,
|
||||
vigor = newVigor,
|
||||
loyalty = newLoyalty,
|
||||
strengthXp = existingHero.strengthXp + ch.strengthXpDelta.getOrElse(0),
|
||||
agilityXp = existingHero.agilityXp + ch.agilityXpDelta.getOrElse(0),
|
||||
wisdomXp = existingHero.wisdomXp + ch.wisdomXpDelta.getOrElse(0),
|
||||
charismaXp = existingHero.charismaXp + ch.charismaXpDelta.getOrElse(0),
|
||||
constitutionXp = existingHero.constitutionXp + ch.constitutionXpDelta.getOrElse(0),
|
||||
strength = existingHero.strength + ch.strengthDelta.getOrElse(0),
|
||||
agility = existingHero.agility + ch.agilityDelta.getOrElse(0),
|
||||
wisdom = existingHero.wisdom + ch.wisdomDelta.getOrElse(0),
|
||||
charisma = existingHero.charisma + ch.charismaDelta.getOrElse(0),
|
||||
constitution = existingHero.constitution + ch.constitutionDelta.getOrElse(0),
|
||||
profession = ch.newProfession.getOrElse(existingHero.profession),
|
||||
backstoryVersions = newBackstoryVersions,
|
||||
backstoryEvents = newBackstoryEvents
|
||||
)
|
||||
|
||||
gameState.copy(
|
||||
heroes = gameState.heroes.updated(ch.heroId, updatedHero)
|
||||
)
|
||||
}
|
||||
|
||||
def applyRemovedHeroes(removedHeroIds: Set[HeroId]): GameState = {
|
||||
val (removed, remaining) = gameState.heroes.partition {
|
||||
case (k, _) =>
|
||||
removedHeroIds.contains(k)
|
||||
}
|
||||
val killedHeroes = removed.map {
|
||||
case (hid, hero) =>
|
||||
hero match {
|
||||
case h: HeroC => hid -> h.copy(factionId = None)
|
||||
case h => hid -> h
|
||||
}
|
||||
}
|
||||
gameState.copy(
|
||||
killedHeroes = gameState.killedHeroes ++ killedHeroes,
|
||||
heroes = remaining
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.model.action_result.NotificationT
|
||||
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object GameStateMiscExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyAccumulatedDetails(notification: Option[NotificationT]): GameState =
|
||||
notification
|
||||
.map(note => gameState.copy(deferredNotifications = gameState.deferredNotifications :+ note))
|
||||
.getOrElse(gameState)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
def applyRemovedNotifications(notifications: Vector[NotificationT]): GameState =
|
||||
gameState.copy(
|
||||
deferredNotifications = gameState.deferredNotifications.diff(notifications)
|
||||
)
|
||||
|
||||
def applyNewSeed(newSeed: Option[Long]): GameState =
|
||||
newSeed.map(s => gameState.copy(randomSeed = s)).getOrElse(gameState)
|
||||
|
||||
def applyChronicleEntry(chronicleEntry: Option[ChronicleEntry]): GameState =
|
||||
chronicleEntry.map { ce =>
|
||||
val updatedEntries = gameState.chronicleEntries.indexWhere(_.date == ce.date) match {
|
||||
case -1 => gameState.chronicleEntries :+ ce
|
||||
case idx => gameState.chronicleEntries.updated(idx, ce)
|
||||
}
|
||||
gameState.copy(chronicleEntries = updatedEntries)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
|
||||
def applyCommandCountUpdate(actingFactionId: Option[FactionId]): GameState =
|
||||
actingFactionId.map { factionId =>
|
||||
val existingCount = gameState.factionCommandCounts.getOrElse(factionId, 0)
|
||||
gameState.copy(
|
||||
factionCommandCounts = gameState.factionCommandCounts.updated(factionId, existingCount + 1)
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
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
|
||||
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
|
||||
|
||||
object GameStateProvinceExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyNewProvinces(newProvinces: Vector[ProvinceT]): GameState =
|
||||
if newProvinces.isEmpty then gameState
|
||||
else {
|
||||
newProvinces.foreach(p =>
|
||||
internalRequire(
|
||||
!gameState.provinces.contains(p.id),
|
||||
s"Got a new province update for existing province ${p.id}"
|
||||
)
|
||||
)
|
||||
gameState.copy(
|
||||
provinces = gameState.provinces ++ newProvinces.map(p => p.id -> p)
|
||||
)
|
||||
}
|
||||
|
||||
def applyChangedProvinces(changedProvinces: Vector[ChangedProvinceT]): GameState =
|
||||
if changedProvinces.isEmpty then gameState
|
||||
else
|
||||
changedProvinces.foldLeft(gameState) {
|
||||
case (gs, cp) =>
|
||||
gs.applyChangedProvince(cp)
|
||||
}
|
||||
|
||||
def applyChangedProvince(cp: ChangedProvinceT): GameState = cp match {
|
||||
case cpc: ChangedProvinceC => applyChangedProvinceC(cpc)
|
||||
case _ =>
|
||||
throw new EagleInternalException(s"Unknown ChangedProvinceT type: ${cp.getClass}")
|
||||
}
|
||||
|
||||
private def applyChangedProvinceC(cp: ChangedProvinceC): GameState = {
|
||||
val provinceBefore = gameState.provinces(cp.provinceId) match {
|
||||
case p: ProvinceC => p
|
||||
case p => throw new EagleInternalException(s"Unknown ProvinceT type: ${p.getClass}")
|
||||
}
|
||||
|
||||
val factionLeaderIds = gameState.factions.values.flatMap(_.leaderIds).toVector
|
||||
|
||||
// Use helper files to avoid compiler crash
|
||||
val p1 = ProvinceUpdateHelpers.applyRulingAndBattalions(gameState, provinceBefore, cp, factionLeaderIds)
|
||||
val p2 = ProvinceUpdateHelpers.applyArmiesAndResources(p1, cp)
|
||||
val p3 = ProvinceUpdateHelpers2.applyHeroesAndEvents(p2, cp)
|
||||
val provinceApplied = ProvinceUpdateHelpers2.applyMiscFields(gameState, p3, cp)
|
||||
|
||||
val provinceFixedForRuler = ProvinceUpdateHelpers2.fixRulerIfNeeded(gameState, provinceApplied)
|
||||
|
||||
gameState.copy(
|
||||
provinces = gameState.provinces.updated(cp.provinceId, provinceFixedForRuler)
|
||||
)
|
||||
}
|
||||
|
||||
def applyProvinceActed(actedProvince: Option[ProvinceId]): GameState = {
|
||||
internalRequire(!actedProvince.contains(0), "Province acted has id 0")
|
||||
actedProvince.map { id =>
|
||||
val province = gameState.provinces(id) match {
|
||||
case p: ProvinceC => p
|
||||
case p => throw new EagleInternalException(s"Unknown ProvinceT type: ${p.getClass}")
|
||||
}
|
||||
gameState.copy(
|
||||
provinces = gameState.provinces.updated(id, province.copy(hasActed = true))
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
|
||||
def applyLastCommand(
|
||||
provinceId: Option[ProvinceId],
|
||||
lastCommand: Option[SelectedCommand]
|
||||
): GameState =
|
||||
provinceId
|
||||
.filter(_ => lastCommand.exists(!_.isEmpty))
|
||||
.map { pid =>
|
||||
val province = gameState.provinces(pid) match {
|
||||
case p: ProvinceC => p
|
||||
case p => throw new EagleInternalException(s"Unknown ProvinceT type: ${p.getClass}")
|
||||
}
|
||||
gameState.copy(
|
||||
provinces = gameState.provinces.updated(pid, province.copy(lastCommand = lastCommand))
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.{ChangedProvinceC, HostileArmyStatusChange}
|
||||
import net.eagle0.eagle.model.state.{HostileArmyGroup, MovingArmy}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
|
||||
// Helper object 1 for province updates - split to avoid compiler crash
|
||||
object ProvinceUpdateHelpers {
|
||||
|
||||
def applyRulingAndBattalions(
|
||||
gameState: GameState,
|
||||
p: ProvinceC,
|
||||
cp: ChangedProvinceC,
|
||||
factionLeaderIds: Vector[HeroId]
|
||||
): ProvinceC =
|
||||
p.copy(
|
||||
rulingFactionId =
|
||||
if cp.clearRulingFactionId then None
|
||||
else cp.newRulingFactionId.orElse(p.rulingFactionId),
|
||||
rulingFactionHeroIds = (p.rulingFactionHeroIds
|
||||
.diff(cp.removedRulingFactionHeroIds) ++ cp.newRulingFactionHeroIds)
|
||||
.map(hid => gameState.heroes(hid))
|
||||
.sorted(using HeroUtils.sortOrdering(factionLeaderIds))
|
||||
.map(_.id),
|
||||
battalionIds = p.battalionIds.diff(cp.removedBattalionIds) ++ cp.newBattalionIds,
|
||||
incomingArmies = modifyArmySet(
|
||||
existingArmies = p.incomingArmies,
|
||||
removedIds = cp.removedIncomingArmyIds,
|
||||
addedArmies = cp.newIncomingArmies
|
||||
),
|
||||
hostileArmies = modifyArmyGroupSet(
|
||||
existingGroups = p.hostileArmies,
|
||||
removedFactionIds = cp.removedHostileArmyFactionIds,
|
||||
addedArmies = cp.newHostileArmies,
|
||||
statusChanges = cp.hostileArmyStatusChanges
|
||||
)
|
||||
)
|
||||
|
||||
def applyArmiesAndResources(
|
||||
p: ProvinceC,
|
||||
cp: ChangedProvinceC
|
||||
): ProvinceC = {
|
||||
val incomingShipments = {
|
||||
val holdovers = p.incomingShipments
|
||||
.filterNot(sh => cp.removedIncomingShipmentIds.contains(sh.id))
|
||||
val nextId = holdovers.map(_.id).maxOption.getOrElse(0) + 1
|
||||
holdovers ++ cp.newIncomingShipments.zipWithIndex.map {
|
||||
case (ship, index) =>
|
||||
ship.copy(id = index + nextId)
|
||||
}
|
||||
}
|
||||
|
||||
val defendingArmy =
|
||||
if cp.clearDefendingArmy then None
|
||||
else
|
||||
cp.newDefendingArmy.orElse(
|
||||
p.defendingArmy.map { da =>
|
||||
val newUnits = da.units
|
||||
.filterNot(u => cp.removedRulingFactionHeroIds.contains(u.heroId))
|
||||
.map { u =>
|
||||
u.copy(battalionId = u.battalionId.filterNot(cp.removedBattalionIds.contains))
|
||||
}
|
||||
da.copy(units = newUnits)
|
||||
}
|
||||
)
|
||||
|
||||
val newGold = p.gold + cp.goldDelta.getOrElse(0)
|
||||
internalRequire(newGold >= 0, s"Got a goldDelta of ${cp.goldDelta} but only ${p.gold} available")
|
||||
|
||||
val newFood = p.food + cp.foodDelta.getOrElse(0)
|
||||
internalRequire(newFood >= 0, s"Got a foodDelta of ${cp.foodDelta} but only ${p.food} available")
|
||||
|
||||
p.copy(
|
||||
incomingShipments = incomingShipments,
|
||||
defendingArmy = defendingArmy,
|
||||
gold = newGold,
|
||||
food = newFood,
|
||||
priceIndex = cp.newPriceIndex.getOrElse(p.priceIndex),
|
||||
economy = (p.economy + cp.economyDelta.getOrElse(0.0)).max(0.0).min(100.0),
|
||||
agriculture = (p.agriculture + cp.agricultureDelta.getOrElse(0.0)).max(0.0).min(100.0),
|
||||
infrastructure = (p.infrastructure + cp.infrastructureDelta.getOrElse(0.0)).max(0.0).min(100.0),
|
||||
economyDevastation = (p.economyDevastation + cp.economyDevastationDelta.getOrElse(0.0)).max(0.0).min(p.economy),
|
||||
agricultureDevastation =
|
||||
(p.agricultureDevastation + cp.agricultureDevastationDelta.getOrElse(0.0)).max(0.0).min(p.agriculture),
|
||||
infrastructureDevastation =
|
||||
(p.infrastructureDevastation + cp.infrastructureDevastationDelta.getOrElse(0.0)).max(0.0).min(p.infrastructure),
|
||||
support = (p.support + cp.supportDelta.getOrElse(0.0)).max(0.0).min(100.0)
|
||||
)
|
||||
}
|
||||
|
||||
private def modifyArmySet(
|
||||
existingArmies: Vector[MovingArmy],
|
||||
removedIds: Vector[Int],
|
||||
addedArmies: Vector[MovingArmy]
|
||||
): Vector[MovingArmy] = {
|
||||
val holdovers = existingArmies.filterNot(ma => removedIds.contains(ma.id))
|
||||
val nextId = holdovers.map(_.id).maxOption.getOrElse(0) + 1
|
||||
holdovers ++ addedArmies.zipWithIndex.map {
|
||||
case (army, index) =>
|
||||
army.copy(id = index + nextId)
|
||||
}
|
||||
}
|
||||
|
||||
private def modifyArmyGroupSet(
|
||||
existingGroups: Vector[HostileArmyGroup],
|
||||
removedFactionIds: Vector[FactionId],
|
||||
addedArmies: Vector[HostileArmyGroup],
|
||||
statusChanges: Vector[HostileArmyStatusChange]
|
||||
): Vector[HostileArmyGroup] =
|
||||
statusChanges
|
||||
.foldLeft(existingGroups.filterNot(g => removedFactionIds.contains(g.factionId))) {
|
||||
case (groups, HostileArmyStatusChange(factionId, newStatus)) =>
|
||||
groups.indexWhere(_.factionId == factionId) match {
|
||||
case -1 => groups
|
||||
case index => groups.updated(index, groups(index).copy(status = newStatus))
|
||||
}
|
||||
} ++ addedArmies
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.{BeastsEvent, ImminentRiotEvent, ProvinceOrderType}
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
|
||||
// Helper object 2 for province updates - split to avoid compiler crash
|
||||
object ProvinceUpdateHelpers2 {
|
||||
|
||||
def applyHeroesAndEvents(
|
||||
p: ProvinceC,
|
||||
cp: ChangedProvinceC
|
||||
): ProvinceC = {
|
||||
val unaffiliatedHeroes =
|
||||
p.unaffiliatedHeroes
|
||||
.filterNot(uh => cp.removedUnaffiliatedHeroIds.contains(uh.heroId))
|
||||
.filterNot(uh => cp.changedUnaffiliatedHeroes.map(_.heroId).contains(uh.heroId))
|
||||
++ cp.changedUnaffiliatedHeroes ++ cp.newUnaffiliatedHeroes
|
||||
|
||||
val capturedHeroes =
|
||||
p.capturedHeroes
|
||||
.filterNot(ch => cp.removedCapturedHeroIds.contains(ch.heroId))
|
||||
.map(ch =>
|
||||
if cp.recruitmentAtteptedCapturedHeroIds.contains(ch.heroId)
|
||||
then ch.copy(recruitmentAttempted = true)
|
||||
else ch
|
||||
) ++ cp.newCapturedHeroes
|
||||
|
||||
val lockedImprovementType = cp.newLockedImprovementType match {
|
||||
case Some(net.eagle0.eagle.model.action_result.changed_province.concrete.NewLockedImprovementType.None) =>
|
||||
None
|
||||
case Some(net.eagle0.eagle.model.action_result.changed_province.concrete.NewLockedImprovementType.New(v)) =>
|
||||
Some(v)
|
||||
case None => p.lockedImprovementType
|
||||
}
|
||||
|
||||
p.copy(
|
||||
hasActed = cp.setHasActed.getOrElse(p.hasActed),
|
||||
rulerIsTraveling = cp.setRulerIsTraveling.getOrElse(p.rulerIsTraveling),
|
||||
unaffiliatedHeroes = unaffiliatedHeroes,
|
||||
capturedHeroes = capturedHeroes,
|
||||
provinceOrders = cp.newProvinceOrders.getOrElse(p.provinceOrders),
|
||||
activeEvents = cp.newProvinceEvents.getOrElse(p.activeEvents),
|
||||
lockedImprovementType = lockedImprovementType
|
||||
)
|
||||
}
|
||||
|
||||
def applyMiscFields(
|
||||
gameState: GameState,
|
||||
p: ProvinceC,
|
||||
cp: ChangedProvinceC
|
||||
): ProvinceC =
|
||||
p.copy(
|
||||
lastBeastsDate =
|
||||
if cp.newProvinceEvents.exists(_.exists(_.isInstanceOf[BeastsEvent]))
|
||||
then gameState.currentDate
|
||||
else p.lastBeastsDate,
|
||||
lastRiotDate =
|
||||
if cp.newProvinceEvents.exists(_.exists(_.isInstanceOf[ImminentRiotEvent]))
|
||||
then gameState.currentDate
|
||||
else p.lastRiotDate,
|
||||
incomingEndTurnActions = (p.incomingEndTurnActions ++ cp.newIncomingEndTurnActions)
|
||||
.diff(cp.removedIncomingEndTurnActions),
|
||||
deferredChanges = cp.removedDeferredChangeIndex
|
||||
.map(idx => p.deferredChanges.patch(idx, Nil, 1))
|
||||
.getOrElse(p.deferredChanges) ++ cp.newDeferredChange.toVector,
|
||||
battleRevelations = p.battleRevelations.diff(cp.removedBattleRevelations) ++ cp.newBattleRevelations
|
||||
)
|
||||
|
||||
def fixRulerIfNeeded(gameState: GameState, p: ProvinceC): ProvinceC =
|
||||
if gameState.currentPhase != RoundPhase.PlayerCommands &&
|
||||
(p.rulingFactionId.isEmpty || p.rulingFactionHeroIds.isEmpty)
|
||||
then
|
||||
p.copy(
|
||||
rulingFactionId = None,
|
||||
rulingHeroId = None,
|
||||
rulingFactionHeroIds = Vector.empty,
|
||||
support = 0,
|
||||
provinceOrders = ProvinceOrderType.UnknownOrderType,
|
||||
unaffiliatedHeroes = p.unaffiliatedHeroes.map(uh =>
|
||||
uh.copy(
|
||||
recruitmentInfo = uh.unaffiliatedHeroType match {
|
||||
case UnaffiliatedHeroType.Prisoner => RecruitmentInfo.Prisoner
|
||||
case UnaffiliatedHeroType.MovingPrisoner => RecruitmentInfo.MovingPrisoner
|
||||
case UnaffiliatedHeroType.ReturningPrisoner => RecruitmentInfo.NoRulerInProvince
|
||||
case UnaffiliatedHeroType.Outlaw => RecruitmentInfo.Outlaw
|
||||
case UnaffiliatedHeroType.Traveler => RecruitmentInfo.Traveler
|
||||
case UnaffiliatedHeroType.Resident => RecruitmentInfo.NoRulerInProvince
|
||||
case UnaffiliatedHeroType.Unknown =>
|
||||
throw new EagleInternalException("Unknown unaffiliated hero type")
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
else
|
||||
p.copy(
|
||||
rulingHeroId = p.rulingFactionHeroIds.headOption
|
||||
)
|
||||
}
|
||||
+2
-1
@@ -24,6 +24,7 @@ import net.eagle0.eagle.library.actions.impl.command.AvailableCommandTypeMap
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.IncomingArmyUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
|
||||
object AvailableCommandsFactory {
|
||||
def shouldFollow(
|
||||
@@ -492,7 +493,7 @@ class AvailableCommandsFactory(
|
||||
for {
|
||||
faction <- gs.factions.get(fid)
|
||||
pleaseRecruitMeCommand <- AvailablePleaseRecruitMeCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
.availableCommand(GameStateConverter.fromProto(gs), faction.id)
|
||||
oneProvinceAvailableCommand <- optionalOneProvinceAvailableCommands(
|
||||
gs = gs,
|
||||
0,
|
||||
|
||||
+11
-7
@@ -1,22 +1,26 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.api.available_command.{OneProvincePleaseRecruitMe, PleaseRecruitMeAvailableCommand}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.MinSupportForTaxes
|
||||
import net.eagle0.eagle.library.util.unaffiliated_hero.LegacyUnaffiliatedHeroUtils
|
||||
import net.eagle0.eagle.library.util.unaffiliated_hero.UnaffiliatedHeroUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object AvailablePleaseRecruitMeCommandFactory {
|
||||
|
||||
private def forOneProvince(
|
||||
p: Province,
|
||||
p: ProvinceT,
|
||||
fid: FactionId,
|
||||
gs: GameState
|
||||
): Option[OneProvincePleaseRecruitMe] = {
|
||||
// Convert to proto only for ExpandedUnaffiliatedHeroUtils which still requires proto types
|
||||
val gsProto = GameStateConverter.toProto(gs)
|
||||
val availableHeroes = for {
|
||||
targetUH <- p.unaffiliatedHeroes.filter(uh =>
|
||||
LegacyUnaffiliatedHeroUtils.willPleaseRecruitMe(
|
||||
UnaffiliatedHeroUtils.willPleaseRecruitMe(
|
||||
gameState = gs,
|
||||
factionId = fid,
|
||||
unaffiliatedHero = uh
|
||||
@@ -26,8 +30,8 @@ object AvailablePleaseRecruitMeCommandFactory {
|
||||
// FIXME: We should be generating the LLM request and its textID here instead of earlier in the round
|
||||
ExpandedUnaffiliatedHeroUtils
|
||||
.expandedUnaffiliatedHero(
|
||||
gs = gs,
|
||||
uh = targetUH
|
||||
gs = gsProto,
|
||||
uh = UnaffiliatedHeroConverter.toProto(targetUH)
|
||||
)
|
||||
|
||||
Option.when(availableHeroes.nonEmpty)(
|
||||
|
||||
@@ -63,6 +63,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -499,7 +501,13 @@ scala_library(
|
||||
"//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/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero:legacy_unaffiliated_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_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/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -172,32 +172,26 @@ 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/impl/common:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_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_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier_impl",
|
||||
"//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: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",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:truce_months_from_returning_leader",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero:legacy_unaffiliated_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
@@ -215,19 +209,21 @@ 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: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/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//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/proto_converters/view/province:province_view",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -280,21 +276,15 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//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:protoless_simple_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",
|
||||
"//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/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/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_battle_request_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -332,6 +322,7 @@ 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",
|
||||
@@ -340,20 +331,17 @@ 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_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_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/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_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",
|
||||
@@ -366,20 +354,17 @@ 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",
|
||||
@@ -402,21 +387,15 @@ 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_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_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/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: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",
|
||||
@@ -434,18 +413,11 @@ 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/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/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//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/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
@@ -461,22 +433,15 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//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:protoless_simple_action",
|
||||
"//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/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/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_defense_decision_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:army_converter",
|
||||
"//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/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -494,28 +459,13 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
":withdrawn_army_returns_home_action",
|
||||
"//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/eagle:eagle_pkg",
|
||||
"//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:protoless_sequential_results_action",
|
||||
"//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/action_result/types:end_free_for_all_decision_phase_result_type",
|
||||
"//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/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -529,36 +479,29 @@ 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:random_sequential_results_action",
|
||||
"//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: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/impl/command:command_factory",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:legacy_handle_riot_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
|
||||
"//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: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: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_choice_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_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",
|
||||
"//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",
|
||||
@@ -628,7 +571,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//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:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
@@ -641,8 +583,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:notification_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_please_recruit_me_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -714,6 +656,7 @@ scala_library(
|
||||
"//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",
|
||||
"//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",
|
||||
@@ -733,7 +676,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:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
":check_for_faction_changes_action",
|
||||
@@ -742,9 +685,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_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/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/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",
|
||||
@@ -756,11 +699,7 @@ 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_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/proto_converters:battalion_type_converter",
|
||||
"//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",
|
||||
@@ -858,7 +797,7 @@ scala_library(
|
||||
"//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:protoless_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
@@ -866,9 +805,12 @@ scala_library(
|
||||
"//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/proto_converters/hero",
|
||||
"//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",
|
||||
"//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/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -915,23 +857,18 @@ 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:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_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_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/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/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",
|
||||
@@ -940,10 +877,9 @@ 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:legacy_province_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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
@@ -956,18 +892,13 @@ 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",
|
||||
],
|
||||
@@ -1052,34 +983,26 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
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:deterministic_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
|
||||
],
|
||||
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/eagle/internal:province_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/util:shattered_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:winter_supplies_loss",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_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",
|
||||
"//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/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/proto_converters:action_result_proto_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:army_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_forced_turn_back_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:weather_forced_supplies_back_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:weather_forced_supplies_lost_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:weather_forced_turn_back_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//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/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
@@ -1092,22 +1015,35 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
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:deterministic_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_from_departure",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:loyalty_threshold",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_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:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//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_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:notification_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_hero_departure_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:heroes_departed_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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:event_for_hero_backstory_trait",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1197,16 +1133,12 @@ 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:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_event_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/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:base_beasts_count",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:beasts_duration_months",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:beasts_event_chance",
|
||||
@@ -1241,12 +1173,23 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:months_between_riots_support_multiplier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:riot_event_chance",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:beast_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:date_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_event_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//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_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_faction_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:province_events_changed_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:beast_info",
|
||||
"//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/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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1258,41 +1201,21 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
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:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
":end_province_move_resolution_phase_action",
|
||||
":friendly_move_action",
|
||||
":shipment_arrived_action",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_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_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:t_random_sequential_results_action",
|
||||
"//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/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:army_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:supplies_converter",
|
||||
"//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:army",
|
||||
"//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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1343,15 +1266,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:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//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:eagle_pkg",
|
||||
"//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/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/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",
|
||||
@@ -1360,18 +1283,17 @@ 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/proto_converters/view/province:province_view",
|
||||
"//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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1383,26 +1305,20 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
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:t_random_sequential_results_action",
|
||||
"//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/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_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/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/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",
|
||||
@@ -1433,10 +1349,6 @@ 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",
|
||||
@@ -1456,37 +1368,31 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//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:random_sequential_results_action",
|
||||
"//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/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:round_phase_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_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/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: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/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/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/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:legacy_province_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",
|
||||
"//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/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/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1498,26 +1404,27 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
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:random_sequential_results_action",
|
||||
"//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/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: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/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:random_sequential_results_action",
|
||||
"//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/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/util/command_choice_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_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/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1529,16 +1436,23 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
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:deterministic_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//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: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:protoless_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_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_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:notification_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_prisoner_exchange_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:prisoners_exchanged_result_type",
|
||||
"//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/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1702,20 +1616,21 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
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:deterministic_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//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:faction_relationship_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:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:eagle_unit",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//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/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:start_battle_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//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/shardok_battle",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1758,8 +1673,6 @@ scala_library(
|
||||
"//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/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/proto_converters:action_result_proto_converter",
|
||||
@@ -1860,17 +1773,17 @@ 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:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_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_trait_applier",
|
||||
"//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: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:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
@@ -1882,10 +1795,9 @@ 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/game_state",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
@@ -1947,7 +1859,12 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:free_hero_move_vigor_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero:legacy_unaffiliated_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_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/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+99
-128
@@ -2,31 +2,11 @@ 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.common.unaffiliated_hero_type.UnaffiliatedHeroType
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
|
||||
UNAFFILIATED_HERO_OUTLAW,
|
||||
UNAFFILIATED_HERO_PRISONER
|
||||
}
|
||||
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
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
|
||||
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, RandomStateTSequencer}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
FactionBiasAgainstFormerOnExile,
|
||||
FactionBiasFromExile,
|
||||
@@ -34,7 +14,7 @@ import net.eagle0.eagle.library.settings.{
|
||||
TruceMonthsFromReturningLeader
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.unaffiliated_hero.LegacyUnaffiliatedHeroUtils
|
||||
import net.eagle0.eagle.library.util.unaffiliated_hero.UnaffiliatedHeroUtils
|
||||
import net.eagle0.eagle.library.util.view_filters.ProvinceViewFilter
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
@@ -42,23 +22,21 @@ 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,
|
||||
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.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.view.province.ProvinceViewConverter
|
||||
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
|
||||
import net.eagle0.eagle.model.state.hero.{
|
||||
CapturedHeroExiledBackstoryEvent,
|
||||
CapturedHeroImprisonedBackstoryEvent,
|
||||
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}
|
||||
|
||||
object EndBattleAftermathPhaseAction {
|
||||
@@ -67,7 +45,7 @@ object EndBattleAftermathPhaseAction {
|
||||
changedProvince: ChangedProvinceC
|
||||
)
|
||||
|
||||
def allDeferredChanges(gameState: GameState): Vector[DeferredChange] =
|
||||
def allDeferredChanges(gameState: GameState): Vector[DeferredChangeT] =
|
||||
gameState.provinces.values
|
||||
.flatMap(_.deferredChanges)
|
||||
.toVector
|
||||
@@ -83,23 +61,25 @@ object EndBattleAftermathPhaseAction {
|
||||
gameState: GameState,
|
||||
functionalRandom: FunctionalRandom,
|
||||
newEventForHeroBackstoryDetails: EventForHeroBackstoryT
|
||||
): RandomState[ActionResultT] =
|
||||
LegacyUnaffiliatedHeroUtils
|
||||
): RandomState[ActionResultT] = {
|
||||
val hero = gameState.heroes(capturedHeroId)
|
||||
val initialUh = UnaffiliatedHeroC(
|
||||
heroId = capturedHeroId,
|
||||
unaffiliatedHeroType = uhType,
|
||||
lastFactionId = hero.factionId,
|
||||
factionBiases = (
|
||||
newFactionBias.map(b => actingFactionId -> b) ++ oldFactionBias.map(b => capturedHeroFactionId -> b)
|
||||
).toMap
|
||||
)
|
||||
|
||||
UnaffiliatedHeroUtils
|
||||
.updatedForQuest(
|
||||
gs = gameState,
|
||||
pid = provinceId,
|
||||
uh = UnaffiliatedHero(
|
||||
heroId = capturedHeroId,
|
||||
`type` = uhType,
|
||||
lastFaction = gameState.heroes(capturedHeroId).factionId,
|
||||
factionBiases = (
|
||||
newFactionBias.map(b => actingFactionId -> b) ++ oldFactionBias.map(b => capturedHeroFactionId -> b)
|
||||
).toMap
|
||||
),
|
||||
hero = gameState.heroes(capturedHeroId),
|
||||
uh = initialUh,
|
||||
hero = hero,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.map(UnaffiliatedHeroConverter.fromProto)
|
||||
.map { uh =>
|
||||
ActionResultC(
|
||||
actionResultType = CapturedHeroResolvedResultType,
|
||||
@@ -121,20 +101,20 @@ object EndBattleAftermathPhaseAction {
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def deferredChangeAR(
|
||||
deferredChange: DeferredChange,
|
||||
deferredChange: DeferredChangeT,
|
||||
gameState: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[ActionResultT] =
|
||||
deferredChange match {
|
||||
case CapturedHeroExiled(
|
||||
case DeferredChange.CapturedHeroExiled(
|
||||
exiledHeroId,
|
||||
provinceId,
|
||||
exilingHeroId,
|
||||
exilingFactionId,
|
||||
prisonerFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
prisonerFactionId
|
||||
) =>
|
||||
val notificationLlmRequest =
|
||||
CapturedHeroPleaGenerator.exiledNotification(
|
||||
@@ -147,7 +127,7 @@ object EndBattleAftermathPhaseAction {
|
||||
provinceId = provinceId
|
||||
)
|
||||
convertToUnaffiliated(
|
||||
uhType = UNAFFILIATED_HERO_OUTLAW,
|
||||
uhType = UnaffiliatedHeroType.Outlaw,
|
||||
actingFactionId = exilingFactionId,
|
||||
provinceId = provinceId,
|
||||
capturedHeroId = exiledHeroId,
|
||||
@@ -157,7 +137,7 @@ object EndBattleAftermathPhaseAction {
|
||||
gameState = gameState,
|
||||
functionalRandom = functionalRandom,
|
||||
newEventForHeroBackstoryDetails = CapturedHeroExiledBackstoryEvent(
|
||||
date = DateConverter.fromProto(gameState.currentDate),
|
||||
date = gameState.currentDate.get,
|
||||
capturingFactionId = exilingFactionId,
|
||||
capturingHeroId = exilingHeroId,
|
||||
provinceId = provinceId
|
||||
@@ -177,13 +157,12 @@ object EndBattleAftermathPhaseAction {
|
||||
)
|
||||
)
|
||||
}
|
||||
case CapturedHeroExecuted(
|
||||
case DeferredChange.CapturedHeroExecuted(
|
||||
capturedHeroId,
|
||||
provinceId,
|
||||
executingHeroId,
|
||||
executingFactionId,
|
||||
prisonerFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
prisonerFactionId
|
||||
) =>
|
||||
val notificationLlmRequest =
|
||||
CapturedHeroPleaGenerator.executedNotification(
|
||||
@@ -224,13 +203,12 @@ object EndBattleAftermathPhaseAction {
|
||||
),
|
||||
functionalRandom
|
||||
)
|
||||
case CapturedHeroImprisoned(
|
||||
case DeferredChange.CapturedHeroImprisoned(
|
||||
capturedHeroId,
|
||||
provinceId,
|
||||
imprisoningHeroId,
|
||||
imprisoningFactionId,
|
||||
prisonerFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
prisonerFactionId
|
||||
) =>
|
||||
val notificationLlmRequest =
|
||||
CapturedHeroPleaGenerator.imprisonedNotification(
|
||||
@@ -243,7 +221,7 @@ object EndBattleAftermathPhaseAction {
|
||||
provinceId = provinceId
|
||||
)
|
||||
convertToUnaffiliated(
|
||||
uhType = UNAFFILIATED_HERO_PRISONER,
|
||||
uhType = UnaffiliatedHeroType.Prisoner,
|
||||
actingFactionId = imprisoningFactionId,
|
||||
provinceId = provinceId,
|
||||
capturedHeroId = capturedHeroId,
|
||||
@@ -253,7 +231,7 @@ object EndBattleAftermathPhaseAction {
|
||||
gameState = gameState,
|
||||
functionalRandom = functionalRandom,
|
||||
newEventForHeroBackstoryDetails = CapturedHeroImprisonedBackstoryEvent(
|
||||
date = DateConverter.fromProto(gameState.currentDate),
|
||||
date = gameState.currentDate.get,
|
||||
capturingFactionId = imprisoningFactionId,
|
||||
capturingHeroId = imprisoningHeroId,
|
||||
provinceId = provinceId
|
||||
@@ -273,22 +251,16 @@ object EndBattleAftermathPhaseAction {
|
||||
)
|
||||
)
|
||||
}
|
||||
case CapturedHeroReturned(
|
||||
case DeferredChange.CapturedHeroReturned(
|
||||
returnedHeroId,
|
||||
actingHeroId,
|
||||
fromProvinceId,
|
||||
fromFactionId,
|
||||
toProvinceId,
|
||||
toFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
toFactionId
|
||||
) =>
|
||||
val allFactionTs =
|
||||
gameState.factions.values.map(FactionConverter.fromProto)
|
||||
val truceEndDate = DateConverter
|
||||
.fromProto(gameState.currentDate)
|
||||
.addMonths(
|
||||
TruceMonthsFromReturningLeader.intValue
|
||||
)
|
||||
val allFactions = gameState.factions.values
|
||||
val truceEndDate = gameState.currentDate.get.addMonths(TruceMonthsFromReturningLeader.intValue)
|
||||
RandomState(
|
||||
ActionResultC(
|
||||
actionResultType = CapturedHeroResolvedResultType,
|
||||
@@ -299,7 +271,7 @@ object EndBattleAftermathPhaseAction {
|
||||
heroId = returnedHeroId,
|
||||
newEventsForHeroBackstory = Vector(
|
||||
CapturedHeroReturnedBackstoryEvent(
|
||||
date = DateConverter.fromProto(gameState.currentDate),
|
||||
date = gameState.currentDate.get,
|
||||
capturingFactionId = fromFactionId,
|
||||
capturingHeroId = actingHeroId,
|
||||
provinceId = fromProvinceId
|
||||
@@ -326,7 +298,7 @@ object EndBattleAftermathPhaseAction {
|
||||
.factionRelationship(
|
||||
by = fromFactionId,
|
||||
of = toFactionId,
|
||||
factions = allFactionTs
|
||||
factions = allFactions
|
||||
)
|
||||
.copy(
|
||||
relationshipLevel = FactionRelationship.RelationshipLevel.Truce,
|
||||
@@ -341,7 +313,7 @@ object EndBattleAftermathPhaseAction {
|
||||
.factionRelationship(
|
||||
by = toFactionId,
|
||||
of = fromFactionId,
|
||||
factions = allFactionTs
|
||||
factions = allFactions
|
||||
)
|
||||
.copy(
|
||||
relationshipLevel = FactionRelationship.RelationshipLevel.Truce,
|
||||
@@ -365,12 +337,10 @@ object EndBattleAftermathPhaseAction {
|
||||
functionalRandom
|
||||
)
|
||||
|
||||
case DeferredChange.Empty =>
|
||||
throw new EagleInternalException("Empty deferred change")
|
||||
|
||||
// the rest are not for this phase
|
||||
case _: EpidemicStarted | _: DroughtStarted | _: DroughtEnded | _: PrisonerMoved | _: PrisonerReturned |
|
||||
_: BlizzardStarted | _: BlizzardEnded =>
|
||||
case _: DeferredChange.EpidemicStarted | _: DeferredChange.DroughtStarted | _: DeferredChange.DroughtEnded |
|
||||
_: DeferredChange.PrisonerMoved | _: DeferredChange.PrisonerReturned | _: DeferredChange.BlizzardStarted |
|
||||
_: DeferredChange.BlizzardEnded =>
|
||||
throw new EagleInternalException(
|
||||
"Event should not be present in EndBattleAftermathPhaseAction"
|
||||
)
|
||||
@@ -379,11 +349,18 @@ object EndBattleAftermathPhaseAction {
|
||||
|
||||
case class EndBattleAftermathPhaseAction(
|
||||
gameState: GameState,
|
||||
actionResultApplier: ActionResultTApplier
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
// Withdrew case still needs proto for withdrawnFromProvinceView (no Scala overload yet)
|
||||
private def revelationChange(
|
||||
battleRevelation: BattleRevelation
|
||||
): RevelationChange =
|
||||
battleRevelation: BattleRevelation,
|
||||
gs: GameState
|
||||
): RevelationChange = {
|
||||
// Lazy conversion to proto only for withdrawnFromProvinceView
|
||||
lazy val gsProto = GameStateConverter.toProto(gs)
|
||||
lazy val provinceProto = gsProto.provinces(battleRevelation.provinceId)
|
||||
|
||||
RevelationChange(
|
||||
changedFaction = battleRevelation.revelationType match {
|
||||
case Unknown =>
|
||||
@@ -392,27 +369,28 @@ case class EndBattleAftermathPhaseAction(
|
||||
ChangedFactionC(
|
||||
factionId = battleRevelation.revealedToFactionId,
|
||||
updatedReconnedProvinces = Vector(
|
||||
ProvinceViewFilter
|
||||
.withdrawnFromProvinceView(
|
||||
province = gameState.provinces(battleRevelation.provinceId),
|
||||
gs = gameState,
|
||||
factionId = battleRevelation.revealedToFactionId
|
||||
)
|
||||
.withAsOf(gameState.currentDate.get)
|
||||
ProvinceViewConverter.toProto(
|
||||
ProvinceViewFilter
|
||||
.withdrawnFromProvinceView(
|
||||
province = provinceProto,
|
||||
gs = gsProto,
|
||||
factionId = battleRevelation.revealedToFactionId
|
||||
)
|
||||
.copy(asOf = gsProto.currentDate.map(d => DateConverter.fromProto(Some(d))))
|
||||
)
|
||||
)
|
||||
)
|
||||
case DidBattle =>
|
||||
ChangedFactionC(
|
||||
factionId = battleRevelation.revealedToFactionId,
|
||||
updatedReconnedProvinces = Vector(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
gameState.provinces(battleRevelation.provinceId),
|
||||
gameState
|
||||
// FIXME: setting factionId to None right now to grab the full info. This
|
||||
// probably isn't exactly what we want.
|
||||
)
|
||||
.withAsOf(gameState.currentDate.get)
|
||||
ProvinceViewConverter.toProto(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
gs.provinces(battleRevelation.provinceId),
|
||||
gs
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -421,28 +399,26 @@ case class EndBattleAftermathPhaseAction(
|
||||
removedBattleRevelations = Vector(battleRevelation)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def revelationChanges(gs: GameState): Vector[RevelationChange] =
|
||||
for {
|
||||
province <- gs.provinces.values.toVector
|
||||
revelation <- province.battleRevelations
|
||||
if gs.factions.contains(revelation.revealedToFactionId)
|
||||
} yield revelationChange(BattleRevelationConverter.fromProto(revelation))
|
||||
} yield revelationChange(revelation, gs)
|
||||
|
||||
def sequencerWithDeferredChanges(
|
||||
initialState: GameState,
|
||||
actionResultApplier: ActionResultTApplier,
|
||||
actionResultApplier: ActionResultApplier,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomStateTSequencer =
|
||||
RandomStateTSequencer
|
||||
.fromProto(
|
||||
initialStateProto = initialState,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.foldIn(
|
||||
EndBattleAftermathPhaseAction.allDeferredChanges(gameState = initialState)
|
||||
)(
|
||||
): RandomStateSequencer =
|
||||
RandomStateSequencer(
|
||||
initialState = initialState,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.foldIn(EndBattleAftermathPhaseAction.allDeferredChanges(initialState))(
|
||||
EndBattleAftermathPhaseAction.deferredChangeAR
|
||||
)
|
||||
|
||||
@@ -468,30 +444,25 @@ case class EndBattleAftermathPhaseAction(
|
||||
.withRandomActionResults((gs, fr) =>
|
||||
CheckForFactionChangesAction(
|
||||
gameId = gs.gameId,
|
||||
factions = gs.factions.values.toVector.map(FactionConverter.fromProto),
|
||||
provinces = gs.provinces.values.toVector.map(ProvinceConverter.fromProto),
|
||||
heroes = gs.heroes.values.toVector.map(HeroConverter.fromProto),
|
||||
factions = gs.factions.values.toVector,
|
||||
provinces = gs.provinces.values.toVector,
|
||||
heroes = gs.heroes.values.toVector,
|
||||
killedHeroIds = gs.killedHeroes.keys.toVector
|
||||
).randomResults(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
|
||||
)
|
||||
.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
|
||||
)
|
||||
}
|
||||
.actionResults
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
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.EndBattleRequestPhaseResultType
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
import net.eagle0.eagle.ProvinceId
|
||||
|
||||
@@ -23,7 +23,7 @@ case class EndBattleRequestPhaseAction(
|
||||
}
|
||||
|
||||
object EndBattleRequestPhaseAction {
|
||||
def fromProtoState(gameState: GameState): EndBattleRequestPhaseAction = {
|
||||
def apply(gameState: GameState): EndBattleRequestPhaseAction = {
|
||||
internalRequire(
|
||||
gameState.provinces.forall {
|
||||
case (_, province) =>
|
||||
|
||||
+12
-12
@@ -1,14 +1,13 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
|
||||
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.EndDefenseDecisionPhaseResultType
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.ArmyConverter
|
||||
import net.eagle0.eagle.model.state.{MovingArmy, RoundPhase}
|
||||
import net.eagle0.eagle.model.state.{HostileArmyGroupStatus, MovingArmy, RoundPhase}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
/** Resolution for a single province that paid tribute to hostile armies */
|
||||
case class PayingProvinceResolution(
|
||||
@@ -52,19 +51,21 @@ case class EndDefenseDecisionPhaseAction(
|
||||
}
|
||||
|
||||
object EndDefenseDecisionPhaseAction {
|
||||
def fromProtoState(gameState: GameState): EndDefenseDecisionPhaseAction = {
|
||||
def apply(gameState: GameState): EndDefenseDecisionPhaseAction = {
|
||||
val resolutions = gameState.provinces.values.flatMap { province =>
|
||||
val tributePaidArmyGroups = province.hostileArmies.filter(
|
||||
_.status.asMessage.sealedValue.isTributePaid
|
||||
)
|
||||
val tributePaidArmyGroups = province.hostileArmies.filter { ag =>
|
||||
ag.status match {
|
||||
case _: HostileArmyGroupStatus.TributePaid => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
if tributePaidArmyGroups.isEmpty then None
|
||||
else {
|
||||
val returningArmies = for {
|
||||
ag <- tributePaidArmyGroups
|
||||
armyProto <- ag.armies
|
||||
} yield {
|
||||
val army = ArmyConverter.fromProto(armyProto)
|
||||
ag <- tributePaidArmyGroups
|
||||
army <- ag.armies
|
||||
} yield
|
||||
// Swap destination/origin, clear flee province, set arrival to next round
|
||||
MovingArmy(
|
||||
id = army.id,
|
||||
@@ -76,7 +77,6 @@ object EndDefenseDecisionPhaseAction {
|
||||
suppliesLoss = army.suppliesLoss,
|
||||
startingPositionIndex = army.startingPositionIndex
|
||||
)
|
||||
}
|
||||
|
||||
Some(
|
||||
PayingProvinceResolution(
|
||||
|
||||
+37
-75
@@ -1,8 +1,7 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
|
||||
import net.eagle0.eagle.library.actions.impl.action.diplomacy_helpers.{
|
||||
AllianceResolutionHelpers,
|
||||
BreakAllianceResolutionHelpers,
|
||||
@@ -10,7 +9,8 @@ import net.eagle0.eagle.library.actions.impl.action.diplomacy_helpers.{
|
||||
RansomResolutionHelpers,
|
||||
TruceResolutionHelpers
|
||||
}
|
||||
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, RandomStateTSequencer}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
|
||||
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,35 +18,30 @@ 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}
|
||||
import net.eagle0.eagle.model.state.diplomacy_offer.status.{Accepted, Imprisoned, Invalidated, Rejected, Unresolved}
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.RoundPhase.ReconResolution
|
||||
|
||||
case class EndDiplomacyResolutionPhaseAction(
|
||||
gameState: GameState,
|
||||
actionResultTApplier: ActionResultTApplier
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
override def randomResults(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] = {
|
||||
import EndDiplomacyResolutionPhaseActionHelpers.*
|
||||
RandomStateTSequencer
|
||||
.fromProto(
|
||||
initialStateProto = gameState,
|
||||
actionResultApplier = actionResultTApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.withActionResults(invalidationResultsForState)
|
||||
.withActionResults(ransomResolutionsForState)
|
||||
.withRandomActionResults(truceResolutionsForState)
|
||||
@@ -56,15 +51,9 @@ case class EndDiplomacyResolutionPhaseAction(
|
||||
.withRandomActionResults { (currentGameState, fr) =>
|
||||
CheckForFactionChangesAction(
|
||||
gameId = currentGameState.gameId,
|
||||
factions = currentGameState.factions.values.toVector.map(
|
||||
FactionConverter.fromProto
|
||||
),
|
||||
provinces = currentGameState.provinces.values.toVector.map(
|
||||
ProvinceConverter.fromProto
|
||||
),
|
||||
heroes = currentGameState.heroes.values.toVector.map(
|
||||
HeroConverter.fromProto
|
||||
),
|
||||
factions = currentGameState.factions.values.toVector,
|
||||
provinces = currentGameState.provinces.values.toVector,
|
||||
heroes = currentGameState.heroes.values.toVector,
|
||||
killedHeroIds = currentGameState.killedHeroes.keys.toVector
|
||||
).randomResults(fr)
|
||||
}
|
||||
@@ -72,20 +61,14 @@ case class EndDiplomacyResolutionPhaseAction(
|
||||
HeroBackstoryUpdateAction(
|
||||
gameId = currentGameState.gameId,
|
||||
roundId = currentGameState.currentRoundId,
|
||||
heroes = currentGameState.heroes.values
|
||||
.map(HeroConverter.fromProto)
|
||||
.toVector,
|
||||
heroes = currentGameState.heroes.values.toVector,
|
||||
visibleToFactionIds = toFid =>
|
||||
FactionUtils.alliedFactions(
|
||||
toFid,
|
||||
currentGameState.factions.values
|
||||
.map(FactionConverter.fromProto)
|
||||
.toVector
|
||||
currentGameState.factions.values.toVector
|
||||
),
|
||||
heroInProvinceOwnedBy = heroId => {
|
||||
val provinces = currentGameState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector
|
||||
val provinces = currentGameState.provinces.values.toVector
|
||||
provinces
|
||||
.find(province =>
|
||||
province.rulingFactionHeroIds.contains(heroId) ||
|
||||
@@ -109,7 +92,7 @@ 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 RandomStateTSequencer, preventing use of stale data from the initial game state.
|
||||
* state as provided by RandomStateSequencer, preventing use of stale data from the initial game state.
|
||||
*/
|
||||
private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
|
||||
@@ -117,11 +100,8 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
def invalidationResultsForState(
|
||||
currentGameState: GameState
|
||||
): Vector[ActionResultT] = {
|
||||
val factions =
|
||||
currentGameState.factions.values.map(FactionConverter.fromProto).toVector
|
||||
val provinces = currentGameState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector
|
||||
val factions = currentGameState.factions.values.toVector
|
||||
val provinces = currentGameState.provinces.values.toVector
|
||||
|
||||
for {
|
||||
faction <- factions
|
||||
@@ -152,8 +132,6 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
currentGameState: GameState
|
||||
): ActionResultT = {
|
||||
val deferredNotifications = currentGameState.deferredNotifications
|
||||
.map(n => NotificationConverter.fromProto(n, deferred = true))
|
||||
.toVector
|
||||
|
||||
ActionResultC(
|
||||
actionResultType = EndDiplomacyResolutionPhaseResultType,
|
||||
@@ -163,9 +141,6 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
)
|
||||
}
|
||||
|
||||
private def factionTs(gs: GameState): Vector[FactionT] =
|
||||
gs.factions.values.map(FactionConverter.fromProto).toVector
|
||||
|
||||
private def resolutionsForType[A](
|
||||
getter: FactionT => Vector[A],
|
||||
resolver: A => Vector[ActionResultT]
|
||||
@@ -192,14 +167,12 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
currentGameState: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] = {
|
||||
val factions = factionTs(currentGameState)
|
||||
val factions = currentGameState.factions.values.toVector
|
||||
randomResolutionsForType(
|
||||
_.incomingDiplomacyOffers.collect { case to: TruceOffer => to },
|
||||
(to: TruceOffer, fr: FunctionalRandom) => {
|
||||
val provinces = currentGameState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector
|
||||
val currentDate = DateConverter.fromProto(currentGameState.currentDate)
|
||||
val provinces = currentGameState.provinces.values.toVector
|
||||
val currentDate = currentGameState.currentDate.get
|
||||
resultsForTruceOffer(
|
||||
to,
|
||||
fr,
|
||||
@@ -217,16 +190,13 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
currentGameState: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] = {
|
||||
val factions = factionTs(currentGameState)
|
||||
val factions = currentGameState.factions.values.toVector
|
||||
randomResolutionsForType(
|
||||
_.incomingDiplomacyOffers.collect { case ao: AllianceOffer => ao },
|
||||
(ao: AllianceOffer, fr: FunctionalRandom) => {
|
||||
val provinces = currentGameState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector
|
||||
val heroes =
|
||||
currentGameState.heroes.values.map(HeroConverter.fromProto).toVector
|
||||
val currentDate = DateConverter.fromProto(currentGameState.currentDate)
|
||||
val provinces = currentGameState.provinces.values.toVector
|
||||
val heroes = currentGameState.heroes.values.toVector
|
||||
val currentDate = currentGameState.currentDate.get
|
||||
resultsForAllianceOffer(
|
||||
ao,
|
||||
fr,
|
||||
@@ -244,14 +214,12 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
currentGameState: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] = {
|
||||
val factions = factionTs(currentGameState)
|
||||
val factions = currentGameState.factions.values.toVector
|
||||
randomResolutionsForType(
|
||||
_.incomingDiplomacyOffers.collect { case ba: BreakAlliance => ba },
|
||||
(ba: BreakAlliance, fr: FunctionalRandom) => {
|
||||
val provinces = currentGameState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector
|
||||
val currentDate = DateConverter.fromProto(currentGameState.currentDate)
|
||||
val provinces = currentGameState.provinces.values.toVector
|
||||
val currentDate = currentGameState.currentDate.get
|
||||
resultsForBreakAlliance(ba, fr, provinces, factions, currentDate)
|
||||
},
|
||||
functionalRandom
|
||||
@@ -262,17 +230,13 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
currentGameState: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] = {
|
||||
val factions = factionTs(currentGameState)
|
||||
val factions = currentGameState.factions.values.toVector
|
||||
randomResolutionsForType(
|
||||
_.incomingDiplomacyOffers.collect { case id: Invitation => id },
|
||||
(id: Invitation, fr: FunctionalRandom) => {
|
||||
val provinces = currentGameState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector
|
||||
val battalions = currentGameState.battalions.values
|
||||
.map(BattalionConverter.fromProto)
|
||||
.toVector
|
||||
val currentDate = DateConverter.fromProto(currentGameState.currentDate)
|
||||
val provinces = currentGameState.provinces.values.toVector
|
||||
val battalions = currentGameState.battalions.values.toVector
|
||||
val currentDate = currentGameState.currentDate.get
|
||||
resultsForInvitation(
|
||||
id,
|
||||
fr,
|
||||
@@ -289,16 +253,14 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
def ransomResolutionsForState(
|
||||
currentGameState: GameState
|
||||
): Vector[ActionResultT] = {
|
||||
val factions = factionTs(currentGameState)
|
||||
val factions = currentGameState.factions.values.toVector
|
||||
resolutionsForType(
|
||||
_.incomingDiplomacyOffers.collect { case ro: RansomOffer => ro },
|
||||
(ro: RansomOffer) => {
|
||||
val provinces = currentGameState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector
|
||||
val provinces = currentGameState.provinces.values.toVector
|
||||
resultsForRansomOffer(
|
||||
ro,
|
||||
DateConverter.fromProto(currentGameState.currentDate),
|
||||
currentGameState.currentDate.get,
|
||||
provinces,
|
||||
currentGameState
|
||||
)
|
||||
@@ -321,7 +283,7 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
|
||||
gameId = gameState.gameId,
|
||||
currentDate = currentDate,
|
||||
currentRoundId = gameState.currentRoundId,
|
||||
previousBackstoryTextIdLookup = hid => gameState.heroes(hid).backstoryVersions.toVector.last.textId
|
||||
previousBackstoryTextIdLookup = hid => gameState.heroes(hid).backstoryVersions.last.textId
|
||||
)
|
||||
)
|
||||
case Rejected =>
|
||||
|
||||
+2
-3
@@ -1,11 +1,10 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
|
||||
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
|
||||
import net.eagle0.eagle.model.action_result.types.EndFreeForAllDecisionPhaseResultType
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
|
||||
case class EndFreeForAllDecisionPhaseAction(gameState: GameState) extends ProtolessSequentialResultsAction {
|
||||
@@ -13,7 +12,7 @@ case class EndFreeForAllDecisionPhaseAction(gameState: GameState) extends Protol
|
||||
override def results: Vector[ActionResultT] =
|
||||
WithdrawnArmiesReturnHomeAction(
|
||||
gameState.currentRoundId,
|
||||
gameState.provinces.values.map(ProvinceConverter.fromProto).toVector
|
||||
gameState.provinces.values.toVector
|
||||
).results :+ ActionResultC(
|
||||
actionResultType = EndFreeForAllDecisionPhaseResultType,
|
||||
newRoundPhase = Some(RoundPhase.FreeForAllBattleRequest)
|
||||
|
||||
+74
-61
@@ -2,79 +2,93 @@ 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.internal.action_result.ActionResult as ActionResultProto
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
|
||||
import net.eagle0.eagle.library.actions.impl.command.{CommandFactory, LegacyHandleRiotUtils}
|
||||
import net.eagle0.eagle.library.actions.impl.common.{RandomSequentialResultsAction, RandomStateProtoSequencer}
|
||||
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.util.command_choice_helpers.CommandChoiceHelpers
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
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: CommandFactory
|
||||
) extends RandomSequentialResultsAction(GameStateConverter.toProto(gameState)) {
|
||||
def vassalCommandResults(
|
||||
ars: RandomStateProtoSequencer
|
||||
): RandomStateProtoSequencer =
|
||||
ars.lastStateProto.provinces.values
|
||||
.filter(LegacyProvinceUtils.hasImminentRiot)
|
||||
commandFactory: TCommandFactory,
|
||||
actionResultApplier: ActionResultApplier
|
||||
) 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)
|
||||
.filterNot(_.hasActed)
|
||||
.foldLeft(ars) {
|
||||
case (sequencer, p) =>
|
||||
commandsForProvince(p.id).map { opac =>
|
||||
sequencer.withRandomAction {
|
||||
case (gs, fr) =>
|
||||
CommandChoiceHelpers
|
||||
.handleRiotSelectedCommand(
|
||||
actingFactionId = p.getRulingFactionId,
|
||||
|
||||
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,
|
||||
availableCommands = opac.commands.toVector,
|
||||
functionalRandom = fr
|
||||
availableCommand = cs.available,
|
||||
selectedCommand = cs.selected
|
||||
)
|
||||
.map { optCS =>
|
||||
optCS.map { cs =>
|
||||
commandFactory
|
||||
.makeCommand(
|
||||
actingFactionId = p.getRulingFactionId,
|
||||
gameState = GameStateConverter.fromProto(gs),
|
||||
availableCommand = cs.available,
|
||||
selectedCommand = cs.selected
|
||||
)
|
||||
}.get
|
||||
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)
|
||||
}
|
||||
}
|
||||
}.get
|
||||
}
|
||||
case (None, nextFr) =>
|
||||
RandomState(Vector.empty[ActionResultT], nextFr)
|
||||
}
|
||||
}
|
||||
}
|
||||
.getOrElse(seq)
|
||||
}
|
||||
}
|
||||
|
||||
private def riotOccurredResults(
|
||||
ars: RandomStateProtoSequencer
|
||||
): RandomStateProtoSequencer =
|
||||
ars.lastStateProto.provinces.values
|
||||
.filter(LegacyProvinceUtils.hasImminentRiot)
|
||||
.foldLeft(ars) {
|
||||
case (ars, p) =>
|
||||
ars.withActionResult(_ =>
|
||||
LegacyHandleRiotUtils
|
||||
.riotOccurredAr(
|
||||
p.getRulingFactionId,
|
||||
ProvinceConverter.fromProto(p)
|
||||
)
|
||||
sequencer: RandomStateSequencer
|
||||
): RandomStateSequencer =
|
||||
provincesWithImminentRiot(sequencer.lastState).foldLeft(sequencer) {
|
||||
case (seq, province) =>
|
||||
seq.withActionResult(_ =>
|
||||
HandleRiotUtils.riotOccurredAr(
|
||||
province.rulingFactionId.get,
|
||||
province
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def endPhaseResult(
|
||||
ars: RandomStateProtoSequencer
|
||||
): RandomStateProtoSequencer =
|
||||
ars.withActionResultT(gs =>
|
||||
private def endPhaseResult(
|
||||
sequencer: RandomStateSequencer
|
||||
): RandomStateSequencer =
|
||||
sequencer.withActionResult { gs =>
|
||||
ActionResultC(
|
||||
actionResultType = EndHandleRiotsPhaseResultType,
|
||||
newRoundPhase = Some(RoundPhase.HeroDepartures),
|
||||
@@ -82,18 +96,17 @@ case class EndHandleRiotsPhaseAction(
|
||||
.filter(_.hasActed)
|
||||
.map(p => ChangedProvinceC(provinceId = p.id, setHasActed = Some(false)))
|
||||
.toVector,
|
||||
removedNotifications = gameState.deferredNotifications,
|
||||
newNotifications = gameState.deferredNotifications.map(_.withDeferred(false))
|
||||
removedNotifications = gs.deferredNotifications,
|
||||
newNotifications = gs.deferredNotifications.map(_.withDeferred(false))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultProtoApplier: ActionResultProtoApplier
|
||||
): RandomState[Vector[ActionResultProto]] =
|
||||
RandomStateProtoSequencer(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultProtoApplier = actionResultProtoApplier,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.withRandomContinuance(vassalCommandResults)
|
||||
|
||||
+73
-122
@@ -1,19 +1,12 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.internal.deferred_change.*
|
||||
import net.eagle0.eagle.internal.deferred_change.DeferredChange.Empty
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.{
|
||||
ProtolessRandomSequentialResultsAction,
|
||||
RandomStateTSequencer,
|
||||
VigorXPApplier
|
||||
}
|
||||
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.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
|
||||
@@ -26,12 +19,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.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.{ProvinceConverter, ProvinceEventConverter}
|
||||
import net.eagle0.eagle.model.state.province.{BlizzardEvent, DroughtEvent, EpidemicEvent}
|
||||
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.unaffiliated_hero.RecruitmentInfo
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroType.{Outlaw, Prisoner}
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
@@ -39,11 +34,11 @@ import net.eagle0.eagle.ProvinceId
|
||||
|
||||
case class EndPlayerCommandsPhaseAction(
|
||||
gameState: GameState,
|
||||
applier: ActionResultTApplier
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
private def endPhaseResult(gs: GameState): ActionResultT = {
|
||||
val abandonedProvinces: Vector[ChangedProvinceT] = gs.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.filter(_.rulingFactionId.isDefined)
|
||||
.flatMap(ProvinceUtils.checkedForAbandonment)
|
||||
.toVector
|
||||
@@ -62,19 +57,15 @@ case class EndPlayerCommandsPhaseAction(
|
||||
)
|
||||
)
|
||||
},
|
||||
removedNotifications = gameState.deferredNotifications.map { note =>
|
||||
NotificationConverter.fromProto(note, deferred = true)
|
||||
}.toVector,
|
||||
newNotifications = gameState.deferredNotifications.map { note =>
|
||||
NotificationConverter.fromProto(note, deferred = false)
|
||||
}.toVector
|
||||
removedNotifications = gs.deferredNotifications.map(_.withDeferred(true)),
|
||||
newNotifications = gs.deferredNotifications.map(_.withDeferred(false))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private def onePrisonerMovedChange(
|
||||
pid: ProvinceId,
|
||||
prisonerMoved: PrisonerMoved,
|
||||
prisonerMoved: DeferredChange.PrisonerMoved,
|
||||
gs: GameState,
|
||||
fr: FunctionalRandom
|
||||
): RandomState[ActionResultT] = {
|
||||
@@ -82,7 +73,6 @@ case class EndPlayerCommandsPhaseAction(
|
||||
.provinces(pid)
|
||||
.unaffiliatedHeroes
|
||||
.find(_.heroId == prisonerMoved.heroId)
|
||||
.map(UnaffiliatedHeroConverter.fromProto)
|
||||
|
||||
fr.nextDouble.map { doubleValue =>
|
||||
val escaped = doubleValue < PrisonerEscapeChance.doubleValue
|
||||
@@ -122,7 +112,7 @@ case class EndPlayerCommandsPhaseAction(
|
||||
|
||||
private def onePrisonerReturnedChange(
|
||||
pid: ProvinceId,
|
||||
prisonerReturned: PrisonerReturned,
|
||||
prisonerReturned: DeferredChange.PrisonerReturned,
|
||||
gs: GameState,
|
||||
fr: FunctionalRandom
|
||||
): RandomState[ActionResultT] =
|
||||
@@ -155,28 +145,28 @@ case class EndPlayerCommandsPhaseAction(
|
||||
|
||||
private def notPrisonerChange(
|
||||
pid: ProvinceId,
|
||||
deferredChange: DeferredChange,
|
||||
deferredChange: DeferredChangeT,
|
||||
gs: GameState,
|
||||
fr: FunctionalRandom
|
||||
): RandomState[ActionResultT] =
|
||||
): RandomState[ActionResultT] = {
|
||||
val currentDate = gs.currentDate.get
|
||||
val province = gs.provinces(pid)
|
||||
|
||||
RandomState(
|
||||
VigorXPApplier.withVigorXp(
|
||||
ActionResultC(
|
||||
actionResultType = deferredChange match {
|
||||
case _: EpidemicStarted => EpidemicTookEffectResultType
|
||||
case _: BlizzardEnded => WeatherTookEffectResultType
|
||||
case _: BlizzardStarted => WeatherTookEffectResultType
|
||||
case _: DroughtStarted => WeatherTookEffectResultType
|
||||
case _: DroughtEnded => WeatherTookEffectResultType
|
||||
case _: PrisonerMoved | _: PrisonerReturned | _: CapturedHeroImprisoned | _: CapturedHeroExecuted |
|
||||
_: CapturedHeroExiled | _: CapturedHeroReturned =>
|
||||
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 =>
|
||||
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(
|
||||
@@ -184,70 +174,31 @@ case class EndPlayerCommandsPhaseAction(
|
||||
provinceId = pid,
|
||||
newProvinceEvents = Some(
|
||||
deferredChange match {
|
||||
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.BlizzardStarted(_, durationMonths) =>
|
||||
province.activeEvents :+ BlizzardEvent(
|
||||
startDate = currentDate,
|
||||
endDate = currentDate.addMonths(durationMonths)
|
||||
)
|
||||
case BlizzardEnded(_, _ /* unknownFieldSet */ ) =>
|
||||
gs.provinces(pid)
|
||||
.activeEvents
|
||||
.filterNot(ProvinceEventUtils.isBlizzardEvent)
|
||||
.map(ProvinceEventConverter.fromProto)
|
||||
.toVector
|
||||
case _: DeferredChange.BlizzardEnded =>
|
||||
province.activeEvents.filter { case _: BlizzardEvent => false; case _ => true }
|
||||
|
||||
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.DroughtStarted(_, durationMonths) =>
|
||||
province.activeEvents :+ DroughtEvent(
|
||||
startDate = currentDate,
|
||||
endDate = currentDate.addMonths(durationMonths)
|
||||
)
|
||||
case _: DeferredChange.DroughtEnded =>
|
||||
province.activeEvents.filter { case _: DroughtEvent => false; case _ => true }
|
||||
|
||||
case DroughtEnded(_, _ /* unknownFieldSet */ ) =>
|
||||
gs.provinces(pid)
|
||||
.activeEvents
|
||||
.filterNot(ProvinceEventUtils.isDroughtEvent)
|
||||
.map(ProvinceEventConverter.fromProto)
|
||||
.toVector
|
||||
case _: DeferredChange.EpidemicStarted =>
|
||||
province.activeEvents :+ EpidemicEvent(startDate = currentDate)
|
||||
|
||||
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"
|
||||
)
|
||||
case _: DeferredChange.PrisonerMoved => Vector()
|
||||
case _: DeferredChange.PrisonerReturned => Vector()
|
||||
case _: DeferredChange.CapturedHeroImprisoned => Vector()
|
||||
case _: DeferredChange.CapturedHeroExecuted => Vector()
|
||||
case _: DeferredChange.CapturedHeroExiled => Vector()
|
||||
case _: DeferredChange.CapturedHeroReturned => Vector()
|
||||
}
|
||||
),
|
||||
removedDeferredChangeIndex = Some(0)
|
||||
@@ -257,26 +208,27 @@ case class EndPlayerCommandsPhaseAction(
|
||||
),
|
||||
fr
|
||||
)
|
||||
}
|
||||
|
||||
private def oneDeferredProvinceChange(
|
||||
pid: ProvinceId,
|
||||
deferredChange: DeferredChange,
|
||||
deferredChange: DeferredChangeT,
|
||||
gs: GameState,
|
||||
fr: FunctionalRandom
|
||||
): RandomState[ActionResultT] =
|
||||
deferredChange match {
|
||||
case prisonerMoved: PrisonerMoved =>
|
||||
case prisonerMoved: DeferredChange.PrisonerMoved =>
|
||||
onePrisonerMovedChange(pid, prisonerMoved, gs, fr)
|
||||
case prisonerReturned: PrisonerReturned =>
|
||||
case prisonerReturned: DeferredChange.PrisonerReturned =>
|
||||
onePrisonerReturnedChange(pid, prisonerReturned, gs, fr)
|
||||
case _ => notPrisonerChange(pid, deferredChange, gs, fr)
|
||||
case _ => notPrisonerChange(pid, deferredChange, gs, fr)
|
||||
}
|
||||
|
||||
private def deferredProvinceChangesResultsForProvince(
|
||||
pid: ProvinceId,
|
||||
arsRS: RandomStateTSequencer
|
||||
): RandomStateTSequencer =
|
||||
arsRS.lastStateProto.provinces(pid).deferredChanges.foldLeft(arsRS) {
|
||||
sequencer: RandomStateSequencer
|
||||
): RandomStateSequencer =
|
||||
sequencer.lastState.provinces(pid).deferredChanges.foldLeft(sequencer) {
|
||||
case (acc, dc) =>
|
||||
acc.withRandomActionResult {
|
||||
case (gs, fr) =>
|
||||
@@ -285,12 +237,12 @@ case class EndPlayerCommandsPhaseAction(
|
||||
}
|
||||
|
||||
private def deferredProvinceChangesResults(
|
||||
ars: RandomStateTSequencer
|
||||
): RandomStateTSequencer =
|
||||
ars.lastStateProto.provinces.keys
|
||||
.foldLeft(ars) {
|
||||
case (newArs, pid) =>
|
||||
deferredProvinceChangesResultsForProvince(pid, newArs)
|
||||
sequencer: RandomStateSequencer
|
||||
): RandomStateSequencer =
|
||||
sequencer.lastState.provinces.keys
|
||||
.foldLeft(sequencer) {
|
||||
case (newSequencer, pid) =>
|
||||
deferredProvinceChangesResultsForProvince(pid, newSequencer)
|
||||
}
|
||||
|
||||
override def randomResults(
|
||||
@@ -303,23 +255,22 @@ case class EndPlayerCommandsPhaseAction(
|
||||
)
|
||||
}
|
||||
|
||||
RandomStateTSequencer
|
||||
.fromProto(
|
||||
initialStateProto = gameState,
|
||||
actionResultApplier = applier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.withRandomActionResults((gs, fr) =>
|
||||
CheckForFactionChangesAction(
|
||||
gameId = gs.gameId,
|
||||
factions = gs.factions.values.toVector.map(FactionConverter.fromProto),
|
||||
provinces = gs.provinces.values.toVector.map(ProvinceConverter.fromProto),
|
||||
heroes = gs.heroes.values.toVector.map(HeroConverter.fromProto),
|
||||
factions = gs.factions.values.toVector,
|
||||
provinces = gs.provinces.values.toVector,
|
||||
heroes = gs.heroes.values.toVector,
|
||||
killedHeroIds = gs.killedHeroes.keys.toVector
|
||||
).randomResults(fr)
|
||||
)
|
||||
.withContinuance(deferredProvinceChangesResults)
|
||||
.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator.fromGameState(gs))
|
||||
.withActionResults(gs => HeroBackstoryUpdateActionGenerator(gs).results)
|
||||
.withActionResult(endPhaseResult)
|
||||
.actionResults
|
||||
}
|
||||
|
||||
+2
-5
@@ -1,12 +1,11 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
|
||||
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
|
||||
import net.eagle0.eagle.model.action_result.types.EndPleaseRecruitMePhaseResultType
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.action_result.NotificationT
|
||||
import net.eagle0.eagle.model.proto_converters.NotificationConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
|
||||
case class EndPleaseRecruitMePhaseAction(
|
||||
@@ -24,10 +23,8 @@ case class EndPleaseRecruitMePhaseAction(
|
||||
}
|
||||
|
||||
object EndPleaseRecruitMePhaseAction {
|
||||
def fromProtoState(gameState: GameState): EndPleaseRecruitMePhaseAction =
|
||||
def apply(gameState: GameState): EndPleaseRecruitMePhaseAction =
|
||||
EndPleaseRecruitMePhaseAction(
|
||||
deferredNotifications = gameState.deferredNotifications
|
||||
.map(n => NotificationConverter.fromProto(n, deferred = true))
|
||||
.toVector
|
||||
)
|
||||
}
|
||||
|
||||
+35
-39
@@ -1,24 +1,23 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultTApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.{RandomStateTSequencer, TRandomSequentialResultsAction}
|
||||
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.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.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.proto_converters.BattalionTypeConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
|
||||
case class EndVassalCommandsPhaseAction(gameState: GameState) extends TRandomSequentialResultsAction(gameState) {
|
||||
case class EndVassalCommandsPhaseAction(
|
||||
gameState: GameState,
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultTApplier: ActionResultTApplier
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] = {
|
||||
gameState.provinces.values.foreach { p =>
|
||||
internalRequire(
|
||||
@@ -27,56 +26,53 @@ case class EndVassalCommandsPhaseAction(gameState: GameState) extends TRandomSeq
|
||||
)
|
||||
}
|
||||
|
||||
RandomStateTSequencer(
|
||||
RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultTApplier,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.withProtolessSequentialResultsAction(gs =>
|
||||
.withActionResults(gs =>
|
||||
CheckForFulfilledQuestsAction(
|
||||
gameId = gs.gameId,
|
||||
currentDate = DateConverter.fromProto(gs.currentDate),
|
||||
currentDate = gs.currentDate.get,
|
||||
currentRoundId = gs.currentRoundId,
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
.withProtolessSequentialResultsAction(gs =>
|
||||
.withActionResults(gs =>
|
||||
CheckForFailedQuestsAction(
|
||||
gameId = gs.gameId,
|
||||
currentDate = DateConverter.fromProto(gs.currentDate),
|
||||
currentDate = gs.currentDate.get,
|
||||
currentRoundId = gs.currentRoundId,
|
||||
provinces = gs.provinces.values.map(ProvinceConverter.fromProto).toVector,
|
||||
factions = gs.factions.values
|
||||
.map(FactionConverter.fromProto)
|
||||
.toVector,
|
||||
hid => gs.heroes(hid).backstoryVersions.last.textId
|
||||
)
|
||||
provinces = gs.provinces.values.toVector,
|
||||
factions = gs.factions.values.toVector,
|
||||
hid => gs.heroes(hid).backstoryTextId
|
||||
).results
|
||||
)
|
||||
.withRandomActionResults((gs, fr) =>
|
||||
CheckForFactionChangesAction(
|
||||
gameId = gs.gameId,
|
||||
factions = gs.factions.values.map(FactionConverter.fromProto).toVector,
|
||||
provinces = gs.provinces.values.map(ProvinceConverter.fromProto).toVector,
|
||||
heroes = gs.heroes.values.map(HeroConverter.fromProto).toVector,
|
||||
factions = gs.factions.values.toVector,
|
||||
provinces = gs.provinces.values.toVector,
|
||||
heroes = gs.heroes.values.toVector,
|
||||
killedHeroIds = gs.killedHeroes.keys.toVector
|
||||
).randomResults(fr)
|
||||
)
|
||||
.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator.fromGameState(gs))
|
||||
.withActionResultT(_ =>
|
||||
.withActionResults(gs => HeroBackstoryUpdateActionGenerator(gs).results)
|
||||
.withActionResult { gs =>
|
||||
// Use current state's deferred notifications, not the initial gameState
|
||||
ActionResultC(
|
||||
actionResultType = EndVassalCommandsPhaseResultType,
|
||||
newRoundPhase = Some(RoundPhase.PlayerCommands),
|
||||
removedNotifications = gameState.deferredNotifications,
|
||||
newNotifications = gameState.deferredNotifications.map(_.withDeferred(false))
|
||||
removedNotifications = gs.deferredNotifications.map(_.withDeferred(true)),
|
||||
newNotifications = gs.deferredNotifications.map(_.withDeferred(false))
|
||||
)
|
||||
)
|
||||
}
|
||||
.actionResults
|
||||
}
|
||||
}
|
||||
|
||||
+17
-9
@@ -1,24 +1,32 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object HeroBackstoryUpdateActionGenerator {
|
||||
def fromGameState(gameState: GameState): ProtolessSequentialResultsAction =
|
||||
|
||||
/** Creates action from Scala GameState (preferred) */
|
||||
def apply(gameState: GameState): ProtolessSequentialResultsAction = {
|
||||
val factions = gameState.factions.values.toVector
|
||||
HeroBackstoryUpdateAction(
|
||||
gameId = gameState.gameId,
|
||||
roundId = gameState.currentRoundId,
|
||||
heroes = gameState.heroes.values.map(HeroConverter.fromProto).toVector,
|
||||
visibleToFactionIds = fid => LegacyFactionUtils.alliedFactions(fid, gameState),
|
||||
heroes = gameState.heroes.values.toVector,
|
||||
visibleToFactionIds = fid => FactionUtils.alliedFactions(fid, factions),
|
||||
heroInProvinceOwnedBy = heroId =>
|
||||
gameState.provinces.values
|
||||
.find(province =>
|
||||
province.rulingFactionHeroIds.contains(
|
||||
heroId
|
||||
) || province.unaffiliatedHeroes.exists(_.heroId == heroId)
|
||||
province.rulingFactionHeroIds.contains(heroId) ||
|
||||
province.unaffiliatedHeroes.exists(_.heroId == heroId)
|
||||
)
|
||||
.flatMap(_.rulingFactionId)
|
||||
)
|
||||
}
|
||||
|
||||
/** Creates action from proto GameState (for sequencer callbacks) */
|
||||
def fromGameState(gameState: GameStateProto): ProtolessSequentialResultsAction =
|
||||
apply(GameStateConverter.fromProto(gameState))
|
||||
}
|
||||
|
||||
@@ -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.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.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.settings.{
|
||||
EmptyProvinceMonthlyDevastationDelta,
|
||||
FactionBiasMinimumAdjustmentPerRound,
|
||||
@@ -15,8 +15,7 @@ import net.eagle0.eagle.library.settings.{
|
||||
OverResourceLimitLoss,
|
||||
TrustDeltaPerRound
|
||||
}
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.DateProtoUtils._Date
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.PriceIndexUtils
|
||||
import net.eagle0.eagle.library.GameHistory
|
||||
@@ -32,37 +31,34 @@ 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)
|
||||
extends TRandomSequentialResultsAction(gameState) {
|
||||
|
||||
private val startingState: GameStateProto = GameStateConverter.toProto(gameState)
|
||||
case class NewRoundAction(
|
||||
gameState: GameState,
|
||||
gameHistory: GameHistory,
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
private val chronicleMonths = Vector(11)
|
||||
|
||||
private def isChronicleMonth(newDate: Date): Boolean =
|
||||
chronicleMonths.contains(newDate.month.value)
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultTApplier: ActionResultTApplier
|
||||
override def randomResults(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] = {
|
||||
val newRoundId = startingState.currentRoundId + 1
|
||||
val newRoundId = gameState.currentRoundId + 1
|
||||
|
||||
// Verify no old incoming armies
|
||||
startingState.provinces.foreach {
|
||||
gameState.provinces.foreach {
|
||||
case (pid, province) =>
|
||||
internalRequire(
|
||||
province.incomingArmies.forall(_.arrivalRound >= newRoundId),
|
||||
@@ -74,28 +70,26 @@ case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
|
||||
)
|
||||
}
|
||||
|
||||
val oldDate = DateConverter.fromProto(startingState.currentDate)
|
||||
val oldDate = gameState.currentDate.get
|
||||
val newDate = oldDate.addMonths(1)
|
||||
|
||||
// Build sequencer starting from starting state
|
||||
val initialSequencer = RandomStateTSequencer.fromProto(
|
||||
initialStateProto = startingState,
|
||||
actionResultApplier = actionResultTApplier,
|
||||
val initialSequencer = RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
|
||||
// Optionally add NewYearAction
|
||||
val afterNewYearSequencer =
|
||||
if newDate.month == Date.Month.January then
|
||||
initialSequencer.withActionResultT(_ =>
|
||||
NewYearAction(GameStateConverter.fromProto(startingState)).immediateExecute
|
||||
)
|
||||
initialSequencer.withActionResult(_ => NewYearAction(gameState).immediateExecute)
|
||||
else initialSequencer
|
||||
|
||||
// Add the main new round result
|
||||
val afterNewRoundSequencer = afterNewYearSequencer.withActionResultT { currentState =>
|
||||
val afterNewRoundSequencer = afterNewYearSequencer.withActionResult { currentState =>
|
||||
newRoundResult(
|
||||
startingState = currentState,
|
||||
gs = currentState,
|
||||
newRoundId = newRoundId,
|
||||
newDate = newDate
|
||||
)
|
||||
@@ -103,34 +97,31 @@ case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
|
||||
|
||||
// Add stat gain checks (and profession gain for stats that newly cross the threshold)
|
||||
afterNewRoundSequencer.withRandomActionResults { (latestState, nextRandom) =>
|
||||
val scalaHeroes = latestState.heroes.values.map(HeroConverter.fromProto)
|
||||
val scalaDate = DateConverter.fromProto(Some(DateConverter.toProto(newDate)))
|
||||
HeroStatGainAction(scalaHeroes, latestState.gameId, scalaDate).randomResultsWithState(nextRandom)
|
||||
HeroStatGainAction(latestState.heroes.values, latestState.gameId, newDate).randomResultsWithState(nextRandom)
|
||||
}.actionResults
|
||||
}
|
||||
|
||||
private def chronicleLlmRequests(
|
||||
newDate: Date,
|
||||
startingState: GameStateProto
|
||||
gs: GameState
|
||||
): Vector[LlmRequestT] =
|
||||
if isChronicleMonth(newDate) then
|
||||
Vector(
|
||||
LlmRequestT.ChronicleUpdateMessage(
|
||||
requestId = s"chronicle_update_${newDate.year}_${newDate.month.value}",
|
||||
eagleGameId = startingState.gameId,
|
||||
eagleGameId = gs.gameId,
|
||||
current_date = newDate,
|
||||
previous_entries = startingState.chronicleEntries.map { entry =>
|
||||
previous_entries = gs.chronicleEntries.map { entry =>
|
||||
ChronicleUpdatePreviousEntry(
|
||||
date = DateConverter.fromProto(entry.date),
|
||||
date = entry.date,
|
||||
generatedTextId = entry.generatedTextId
|
||||
)
|
||||
}.toVector,
|
||||
},
|
||||
new_entries = ChronicleEventGenerator
|
||||
.eventTextEntries(
|
||||
gameHistory = gameHistory,
|
||||
since = startingState.chronicleEntries.lastOption
|
||||
.flatMap(_.date)
|
||||
.map(d => DateConverter.fromProto(Some(d)))
|
||||
since = gs.chronicleEntries.lastOption
|
||||
.map(_.date)
|
||||
.getOrElse(Date(year = 0, month = Date.Month.January))
|
||||
)
|
||||
.map(ChronicleEventConverter.fromProto)
|
||||
@@ -138,8 +129,11 @@ case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
|
||||
)
|
||||
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(
|
||||
startingState: GameStateProto,
|
||||
gs: GameState,
|
||||
newRoundId: RoundId,
|
||||
newDate: Date
|
||||
): ActionResultT = {
|
||||
@@ -149,29 +143,28 @@ case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
|
||||
)
|
||||
|
||||
val changes: Iterable[Changes] =
|
||||
startingState.provinces.values.map { p =>
|
||||
gs.provinces.values.map { p =>
|
||||
val changedUHs: Vector[UnaffiliatedHeroT] =
|
||||
p.unaffiliatedHeroes.map { uh =>
|
||||
val tUh = UnaffiliatedHeroConverter.fromProto(uh)
|
||||
tUh.copy(
|
||||
uh.copy(
|
||||
recruitmentAttempted = false,
|
||||
roundsInType = tUh.roundsInType + 1,
|
||||
factionBiases = tUh.factionBiases.map {
|
||||
roundsInType = uh.roundsInType + 1,
|
||||
factionBiases = uh.factionBiases.map {
|
||||
case (fid, v) =>
|
||||
fid -> modifiedFactionBiasValue(v)
|
||||
}
|
||||
)
|
||||
}.toVector
|
||||
val changedHeroes = changedHeroesAfterOverage(startingState, p.id)
|
||||
}
|
||||
val changedHeroes = changedHeroesAfterOverage(gs, p.id)
|
||||
|
||||
// apply caps
|
||||
val capGoldLoss = (OverResourceLimitLoss.doubleValue * Math.max(
|
||||
0,
|
||||
p.gold - LegacyProvinceUtils.goldCap(p)
|
||||
p.gold - ProvinceUtils.goldCap(p)
|
||||
)).ceil.toInt
|
||||
val capFoodLoss = (OverResourceLimitLoss.doubleValue * Math.max(
|
||||
0,
|
||||
p.food - LegacyProvinceUtils.foodCap(p)
|
||||
p.food - ProvinceUtils.foodCap(p)
|
||||
)).ceil.toInt
|
||||
|
||||
val newPriceIndex = PriceIndexUtils.shiftedTowardSteadyState(
|
||||
@@ -228,7 +221,7 @@ case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
|
||||
val heroesAfterStipend: Map[HeroId, ChangedHeroC] =
|
||||
changes.flatMap(_.changedHeroes).map(h => h.heroId -> h).toMap
|
||||
|
||||
val uppedVigorHeroes: Iterable[ChangedHeroC] = startingState.heroes.map {
|
||||
val uppedVigorHeroes: Iterable[ChangedHeroC] = gs.heroes.map {
|
||||
case (hid, h) =>
|
||||
val baseChange = heroesAfterStipend.getOrElse(hid, ChangedHeroC(heroId = hid))
|
||||
if h.vigor >= h.constitution then baseChange
|
||||
@@ -243,10 +236,9 @@ case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
|
||||
val changedHeroes =
|
||||
uppedVigorHeroes.filter(ch => ch.hasChanges).toVector
|
||||
|
||||
val newDateProto = DateConverter.toProto(newDate)
|
||||
val changedFactions = startingState.factions.values.map { f =>
|
||||
val changedFactions = gs.factions.values.map { f =>
|
||||
val removedTruces =
|
||||
f.factionRelationships.filter(fr => fr.resetDate.exists(_ < newDateProto))
|
||||
f.factionRelationships.filter(fr => fr.resetDate.exists(dateBefore(_, newDate)))
|
||||
|
||||
ChangedFactionC(
|
||||
factionId = f.id,
|
||||
@@ -257,15 +249,15 @@ case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
|
||||
resetDate = None,
|
||||
trustValue = fr.trustValue
|
||||
)
|
||||
}.toVector,
|
||||
trustLevelUpdates = startingState.factions.keys.filterNot(_ == f.id).toVector.map { targetFid =>
|
||||
},
|
||||
trustLevelUpdates = gs.factions.keys.filterNot(_ == f.id).toVector.map { targetFid =>
|
||||
TrustLevelUpdate(targetFid, TrustDeltaPerRound.intValue)
|
||||
},
|
||||
clearLastActedProvinceId = true
|
||||
)
|
||||
}.toVector
|
||||
|
||||
val llmRequests = chronicleLlmRequests(newDate, startingState)
|
||||
val llmRequests = chronicleLlmRequests(newDate, gs)
|
||||
|
||||
ActionResultC(
|
||||
actionResultType = NewRoundActionResultType,
|
||||
@@ -286,21 +278,21 @@ case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
|
||||
}
|
||||
|
||||
private def changedHeroesAfterOverage(
|
||||
gameState: GameStateProto,
|
||||
gs: GameState,
|
||||
provinceId: ProvinceId
|
||||
): Vector[ChangedHeroC] = {
|
||||
val province = gameState.provinces(provinceId)
|
||||
val province = gs.provinces(provinceId)
|
||||
val notRecentHeroCount = province.rulingFactionHeroIds
|
||||
.map(gameState.heroes)
|
||||
.map(gs.heroes)
|
||||
.flatMap(_.roundIdJoined)
|
||||
.count(roundId => gameState.currentRoundId - roundId > MinimumRoundsBeforeLoyaltyDegrades.intValue)
|
||||
.count(roundId => gs.currentRoundId - roundId > MinimumRoundsBeforeLoyaltyDegrades.intValue)
|
||||
if province.heroCap >= notRecentHeroCount then Vector.empty
|
||||
else {
|
||||
val loyaltyHit =
|
||||
OverHeroCapLoyaltyDelta.doubleValue * (notRecentHeroCount - province.heroCap)
|
||||
|
||||
province.rulingFactionHeroIds
|
||||
.map(gameState.heroes)
|
||||
.map(gs.heroes)
|
||||
.map(h =>
|
||||
ChangedHeroC(
|
||||
heroId = h.id,
|
||||
@@ -309,7 +301,6 @@ case class NewRoundAction(gameState: GameState, gameHistory: GameHistory)
|
||||
else StatDelta(loyaltyHit)
|
||||
)
|
||||
)
|
||||
.toVector
|
||||
}
|
||||
end if
|
||||
}
|
||||
|
||||
+67
-87
@@ -1,65 +1,50 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.{
|
||||
END_FORCED_TURN_BACK_PHASE,
|
||||
WEATHER_FORCED_SUPPLIES_BACK,
|
||||
WEATHER_FORCED_SUPPLIES_LOST,
|
||||
WEATHER_FORCED_TURN_BACK
|
||||
}
|
||||
import net.eagle0.eagle.common.round_phase.NewRoundPhase
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase.PROVINCE_MOVE_RESOLUTION
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.army.MovingArmy
|
||||
import net.eagle0.eagle.internal.changed_province.ChangedProvince
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.supplies.MovingSupplies
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.DeterministicSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.util.ShatteredArmyUtils
|
||||
import net.eagle0.eagle.library.settings.WinterSuppliesLoss
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.model.action_result.NotificationDetails.ShatteredArmy.Reason.Blizzard
|
||||
import net.eagle0.eagle.model.proto_converters.{ActionResultProtoConverter, ArmyConverter}
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
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
|
||||
import net.eagle0.eagle.model.action_result.types.{
|
||||
EndForcedTurnBackPhaseResultType,
|
||||
WeatherForcedSuppliesBackResultType,
|
||||
WeatherForcedSuppliesLostResultType,
|
||||
WeatherForcedTurnBackResultType
|
||||
}
|
||||
import net.eagle0.eagle.model.state.{MovingArmy, MovingSupplies, RoundPhase, Supplies}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.RoundId
|
||||
|
||||
case class PerformForcedTurnBackAction(gs: GameState) extends DeterministicSequentialResultsAction(gs) {
|
||||
case class PerformForcedTurnBackAction(gameState: GameState) extends ProtolessSequentialResultsAction {
|
||||
|
||||
val endPhaseResult: ActionResult = ActionResult(
|
||||
`type` = END_FORCED_TURN_BACK_PHASE,
|
||||
newRoundPhase = Some(NewRoundPhase(PROVINCE_MOVE_RESOLUTION))
|
||||
val endPhaseResult: ActionResultT = ActionResultC(
|
||||
actionResultType = EndForcedTurnBackPhaseResultType,
|
||||
newRoundPhase = Some(RoundPhase.ProvinceMoveResolution)
|
||||
)
|
||||
|
||||
private def oneTurnBackSuppliesResult(is: MovingSupplies, p: Province) =
|
||||
private def oneTurnBackSuppliesResult(is: MovingSupplies, p: ProvinceT): ActionResultT =
|
||||
is.originProvinceId.map { originPid =>
|
||||
ActionResult(
|
||||
`type` = WEATHER_FORCED_SUPPLIES_BACK,
|
||||
player = Some(is.factionId),
|
||||
province = Some(p.id),
|
||||
ActionResultC(
|
||||
actionResultType = WeatherForcedSuppliesBackResultType,
|
||||
actingFactionId = Some(is.factionId),
|
||||
provinceId = Some(p.id),
|
||||
changedProvinces = Vector(
|
||||
ChangedProvince(
|
||||
id = p.id,
|
||||
ChangedProvinceC(
|
||||
provinceId = p.id,
|
||||
removedIncomingShipmentIds = Vector(is.id)
|
||||
),
|
||||
ChangedProvince(
|
||||
id = originPid,
|
||||
addedIncomingShipments = Vector(
|
||||
ChangedProvinceC(
|
||||
provinceId = originPid,
|
||||
newIncomingShipments = Vector(
|
||||
MovingSupplies(
|
||||
supplies = is.supplies.map {
|
||||
_.update(
|
||||
_.food.modify(f =>
|
||||
Math
|
||||
.floor((1.0 - WinterSuppliesLoss.doubleValue) * f)
|
||||
.toInt
|
||||
),
|
||||
_.gold.modify(g =>
|
||||
Math
|
||||
.floor((1.0 - WinterSuppliesLoss.doubleValue) * g)
|
||||
.toInt
|
||||
)
|
||||
)
|
||||
},
|
||||
arrivalRound = gs.currentRoundId + 1,
|
||||
supplies = Supplies(
|
||||
gold = Math.floor((1.0 - WinterSuppliesLoss.doubleValue) * is.supplies.gold).toInt,
|
||||
food = Math.floor((1.0 - WinterSuppliesLoss.doubleValue) * is.supplies.food).toInt
|
||||
),
|
||||
arrivalRound = gameState.currentRoundId + 1,
|
||||
destinationProvinceId = originPid,
|
||||
originProvinceId = None,
|
||||
factionId = is.factionId,
|
||||
@@ -72,36 +57,36 @@ case class PerformForcedTurnBackAction(gs: GameState) extends DeterministicSeque
|
||||
)
|
||||
}
|
||||
.getOrElse(
|
||||
ActionResult(
|
||||
`type` = WEATHER_FORCED_SUPPLIES_LOST,
|
||||
player = Some(is.factionId),
|
||||
province = Some(p.id),
|
||||
ActionResultC(
|
||||
actionResultType = WeatherForcedSuppliesLostResultType,
|
||||
actingFactionId = Some(is.factionId),
|
||||
provinceId = Some(p.id),
|
||||
changedProvinces = Vector(
|
||||
ChangedProvince(
|
||||
id = p.id,
|
||||
ChangedProvinceC(
|
||||
provinceId = p.id,
|
||||
removedIncomingShipmentIds = Vector(is.id)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
private def oneTurnBackArmyResult(ia: MovingArmy, p: Province): ActionResult =
|
||||
ia.getArmy.fleeProvinceId.map { fleePid =>
|
||||
ActionResult(
|
||||
`type` = WEATHER_FORCED_TURN_BACK,
|
||||
player = Some(ia.getArmy.factionId),
|
||||
province = Some(ia.destinationProvince),
|
||||
affectedPlayers = (Vector(ia.getArmy.factionId) ++ p.rulingFactionId).distinct,
|
||||
private def oneTurnBackArmyResult(ia: MovingArmy, p: ProvinceT): ActionResultT =
|
||||
ia.army.fleeProvinceId.map { fleePid =>
|
||||
ActionResultC(
|
||||
actionResultType = WeatherForcedTurnBackResultType,
|
||||
actingFactionId = Some(ia.army.factionId),
|
||||
provinceId = Some(ia.destinationProvinceId),
|
||||
affectedFactionIds = (Vector(ia.army.factionId) ++ p.rulingFactionId).distinct,
|
||||
changedProvinces = Vector(
|
||||
ChangedProvince(id = p.id, removedIncomingArmyIds = Vector(ia.id)),
|
||||
ChangedProvince(
|
||||
id = fleePid,
|
||||
addedIncomingArmies = Vector(
|
||||
ChangedProvinceC(provinceId = p.id, removedIncomingArmyIds = Vector(ia.id)),
|
||||
ChangedProvinceC(
|
||||
provinceId = fleePid,
|
||||
newIncomingArmies = Vector(
|
||||
MovingArmy(
|
||||
army = ia.army.map(_.clearFleeProvinceId),
|
||||
arrivalRound = gs.currentRoundId + 1,
|
||||
destinationProvince = fleePid,
|
||||
originProvince = p.id,
|
||||
army = ia.army.copy(fleeProvinceId = None),
|
||||
arrivalRound = gameState.currentRoundId + 1,
|
||||
destinationProvinceId = fleePid,
|
||||
originProvinceId = p.id,
|
||||
supplies = ia.supplies,
|
||||
suppliesLoss = ia.suppliesLoss,
|
||||
id = ia.id,
|
||||
@@ -113,35 +98,30 @@ case class PerformForcedTurnBackAction(gs: GameState) extends DeterministicSeque
|
||||
)
|
||||
}
|
||||
.getOrElse(
|
||||
ActionResultProtoConverter.toProto(
|
||||
ShatteredArmyUtils
|
||||
.shatteredArmyResult(
|
||||
currentRoundId = gs.currentRoundId,
|
||||
province = ProvinceConverter.fromProto(p),
|
||||
ma = ArmyConverter.fromProto(ia),
|
||||
reason = Blizzard
|
||||
)
|
||||
ShatteredArmyUtils.shatteredArmyResult(
|
||||
currentRoundId = gameState.currentRoundId,
|
||||
province = p,
|
||||
ma = ia,
|
||||
reason = NotificationDetails.ShatteredArmy.Reason.Blizzard
|
||||
)
|
||||
)
|
||||
|
||||
private def turnBackArmiesResults: Vector[ActionResult] =
|
||||
private def turnBackArmiesResults: Vector[ActionResultT] =
|
||||
(
|
||||
for {
|
||||
p <- gs.provinces.values.filter(LegacyProvinceUtils.hasBlizzard)
|
||||
ia <- p.incomingArmies.filter(_.arrivalRound == gs.currentRoundId)
|
||||
p <- gameState.provinces.values.filter(ProvinceUtils.hasBlizzard)
|
||||
ia <- p.incomingArmies.filter(_.arrivalRound == gameState.currentRoundId)
|
||||
} yield oneTurnBackArmyResult(ia, p)
|
||||
).toVector
|
||||
|
||||
private def turnBackSuppliesResults: Vector[ActionResult] =
|
||||
private def turnBackSuppliesResults: Vector[ActionResultT] =
|
||||
(
|
||||
for {
|
||||
p <- gs.provinces.values.filter(LegacyProvinceUtils.hasBlizzard)
|
||||
is <- p.incomingShipments.filter(_.arrivalRound == gs.currentRoundId)
|
||||
p <- gameState.provinces.values.filter(ProvinceUtils.hasBlizzard)
|
||||
is <- p.incomingShipments.filter(_.arrivalRound == gameState.currentRoundId)
|
||||
} yield oneTurnBackSuppliesResult(is, p)
|
||||
).toVector
|
||||
|
||||
override def results(
|
||||
actionResultProtoApplier: ActionResultProtoApplier
|
||||
): Vector[ActionResult] =
|
||||
override def results: Vector[ActionResultT] =
|
||||
turnBackArmiesResults ++ turnBackSuppliesResults :+ endPhaseResult
|
||||
}
|
||||
|
||||
+91
-99
@@ -1,37 +1,32 @@
|
||||
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.common.action_result_notification_details.{HeroDepartureDetails, Notification}
|
||||
import net.eagle0.eagle.common.action_result_notification_details.Notification.Llm
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.{END_HERO_DEPARTURE_PHASE, HEROES_DEPARTED}
|
||||
import net.eagle0.eagle.common.recruitment_info.RecruitmentInfo
|
||||
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.RECRUITMENT_STATUS_TRAVELER
|
||||
import net.eagle0.eagle.common.round_phase.NewRoundPhase
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase.UNAFFILIATED_HERO_ACTIONS
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.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_province.ChangedProvince
|
||||
import net.eagle0.eagle.internal.event_for_hero_backstory.{EventForHeroBackstory, HeroDepartedBackstoryEvent}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.generated_text_request.{GeneratedTextRequest, HeroDepartureMessage}
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.DeterministicSequentialResultsAction
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.settings.{FactionBiasFromDeparture, LoyaltyThreshold}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.model.action_result.{ActionResultT, NotificationDetails, NotificationT}
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedHeroC, NotificationC}
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT.HeroDepartureMessage
|
||||
import net.eagle0.eagle.model.action_result.types.{EndHeroDeparturePhaseResultType, HeroesDepartedResultType}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroDepartedBackstoryEvent
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
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
|
||||
|
||||
object PerformHeroDeparturesAction {
|
||||
def provinceWithDepartedHeroes(
|
||||
gameState: GameState,
|
||||
provinceId: ProvinceId,
|
||||
province: ProvinceT,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[(ChangedProvince, Vector[HeroId])]] =
|
||||
gameState
|
||||
.provinces(provinceId)
|
||||
.rulingFactionHeroIds
|
||||
): RandomState[Option[(ChangedProvinceT, Vector[HeroId])]] =
|
||||
province.rulingFactionHeroIds
|
||||
.foldLeft(RandomState(Vector.empty[HeroId], functionalRandom)) {
|
||||
case (RandomState(acc, fr), hid) =>
|
||||
heroWillDepart(gameState, hid, fr).map(departed =>
|
||||
@@ -43,28 +38,23 @@ object PerformHeroDeparturesAction {
|
||||
case items if items.isEmpty => None
|
||||
case dh =>
|
||||
Some(
|
||||
LegacyProvinceUtils.afterHeroDeparture(
|
||||
ChangedProvince(
|
||||
id = provinceId,
|
||||
removedRulingPlayerHeroIds = dh,
|
||||
ProvinceUtils.afterHeroDeparture(
|
||||
ChangedProvinceC(
|
||||
provinceId = province.id,
|
||||
removedRulingFactionHeroIds = dh,
|
||||
newUnaffiliatedHeroes = dh.map(hid =>
|
||||
UnaffiliatedHero(
|
||||
UnaffiliatedHeroC(
|
||||
heroId = hid,
|
||||
`type` = UNAFFILIATED_HERO_TRAVELER,
|
||||
unaffiliatedHeroType = UnaffiliatedHeroType.Traveler,
|
||||
recruitmentAttempted = true,
|
||||
factionBiases = Map(
|
||||
gameState
|
||||
.provinces(provinceId)
|
||||
.rulingFactionId
|
||||
.get -> FactionBiasFromDeparture.doubleValue
|
||||
province.rulingFactionId.get -> FactionBiasFromDeparture.doubleValue
|
||||
),
|
||||
recruitmentInfo = Some(
|
||||
RecruitmentInfo(status = RECRUITMENT_STATUS_TRAVELER)
|
||||
)
|
||||
recruitmentInfo = RecruitmentInfo.Traveler
|
||||
)
|
||||
)
|
||||
),
|
||||
gameState
|
||||
province
|
||||
),
|
||||
dh
|
||||
)
|
||||
@@ -77,7 +67,10 @@ object PerformHeroDeparturesAction {
|
||||
): RandomState[Boolean] =
|
||||
functionalRandom.nextOddsChance(
|
||||
departureOdds(
|
||||
LegacyHeroUtils.effectiveLoyalty(heroId, gameState)
|
||||
HeroUtils.effectiveLoyalty(
|
||||
gameState.heroes(heroId),
|
||||
gameState.factions.values.toVector
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -90,45 +83,47 @@ object PerformHeroDeparturesAction {
|
||||
private def notificationForHero(
|
||||
hid: HeroId,
|
||||
pid: ProvinceId,
|
||||
gameState: GameState
|
||||
): Notification =
|
||||
Notification(
|
||||
details = HeroDepartureDetails(
|
||||
rulingFactionId: FactionId,
|
||||
currentRoundId: RoundId
|
||||
): NotificationC =
|
||||
NotificationC(
|
||||
details = NotificationDetails.HeroDeparture(
|
||||
departingHeroId = hid,
|
||||
fromFactionId = gameState.provinces(pid).getRulingFactionId,
|
||||
fromFactionId = rulingFactionId,
|
||||
provinceId = pid
|
||||
),
|
||||
llm = Llm.LlmId(notificationLlmId(gameState.currentRoundId, hid))
|
||||
llm = NotificationT.Llm.Id(notificationLlmId(currentRoundId, hid)),
|
||||
deferred = false
|
||||
)
|
||||
|
||||
private def notificationLlmRequestForHero(
|
||||
hid: HeroId,
|
||||
pid: ProvinceId,
|
||||
gameState: GameState
|
||||
): GeneratedTextRequest =
|
||||
GeneratedTextRequest(
|
||||
id = notificationLlmId(gameState.currentRoundId, hid),
|
||||
eagleGameId = gameState.gameId,
|
||||
details = HeroDepartureMessage(
|
||||
departingHeroId = hid,
|
||||
fromFactionId = gameState.provinces(pid).getRulingFactionId,
|
||||
provinceId = pid
|
||||
)
|
||||
rulingFactionId: FactionId,
|
||||
currentRoundId: RoundId,
|
||||
gameId: Long
|
||||
): LlmRequestT =
|
||||
HeroDepartureMessage(
|
||||
requestId = notificationLlmId(currentRoundId, hid),
|
||||
eagleGameId = gameId,
|
||||
departingHeroId = hid,
|
||||
fromFactionId = rulingFactionId,
|
||||
provinceId = pid
|
||||
)
|
||||
}
|
||||
|
||||
case class PerformHeroDeparturesAction(
|
||||
startingState: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
) extends DeterministicSequentialResultsAction(startingState = startingState) {
|
||||
override def results(
|
||||
actionResultProtoApplier: ActionResultProtoApplier
|
||||
): Vector[ActionResult] =
|
||||
startingState.provinces.values
|
||||
gameState: GameState
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
gameState.provinces.values.toVector
|
||||
.filter(_.rulingFactionId.isDefined)
|
||||
.foldLeft(
|
||||
RandomState(
|
||||
Vector[Option[(ChangedProvince, Vector[HeroId])]](),
|
||||
Vector[Option[(ChangedProvinceT, Vector[HeroId])]](),
|
||||
functionalRandom
|
||||
)
|
||||
) {
|
||||
@@ -137,61 +132,58 @@ case class PerformHeroDeparturesAction(
|
||||
case (acc, fr) =>
|
||||
PerformHeroDeparturesAction
|
||||
.provinceWithDepartedHeroes(
|
||||
startingState,
|
||||
p.id,
|
||||
gameState,
|
||||
p,
|
||||
fr
|
||||
)
|
||||
.map(tup => acc :+ tup)
|
||||
}
|
||||
}
|
||||
.map(_.flatten)
|
||||
.map {
|
||||
_.map {
|
||||
.map { items =>
|
||||
items.map {
|
||||
case (cp, hs) =>
|
||||
ActionResult(
|
||||
`type` = HEROES_DEPARTED,
|
||||
province = Some(cp.id),
|
||||
player = startingState.provinces(cp.id).rulingFactionId,
|
||||
val provinceId = cp.provinceId
|
||||
val rulingFactionId = gameState.provinces(provinceId).rulingFactionId.get
|
||||
ActionResultC(
|
||||
actionResultType = HeroesDepartedResultType,
|
||||
provinceId = Some(provinceId),
|
||||
actingFactionId = Some(rulingFactionId),
|
||||
changedProvinces = Vector(cp),
|
||||
changedHeroes = hs.map { hid =>
|
||||
ChangedHero(
|
||||
id = hid,
|
||||
ChangedHeroC(
|
||||
heroId = hid,
|
||||
clearFactionId = true,
|
||||
newBackstoryEvents = Vector(
|
||||
EventForHeroBackstory(
|
||||
date = startingState.currentDate,
|
||||
details = HeroDepartedBackstoryEvent(
|
||||
departedFromFactionId = startingState.provinces(cp.id).getRulingFactionId,
|
||||
departedFromProvinceId = cp.id
|
||||
)
|
||||
newEventsForHeroBackstory = Vector(
|
||||
HeroDepartedBackstoryEvent(
|
||||
date = gameState.currentDate.get,
|
||||
departedFromFactionId = rulingFactionId,
|
||||
departedFromProvinceId = provinceId
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
notificationsToDeliver = hs.map { hid =>
|
||||
newNotifications = hs.map { hid =>
|
||||
PerformHeroDeparturesAction.notificationForHero(
|
||||
hid = hid,
|
||||
pid = cp.id,
|
||||
gameState = startingState
|
||||
pid = provinceId,
|
||||
rulingFactionId = rulingFactionId,
|
||||
currentRoundId = gameState.currentRoundId
|
||||
)
|
||||
},
|
||||
newGeneratedTextRequests = hs.flatMap { hid =>
|
||||
Vector(
|
||||
PerformHeroDeparturesAction.notificationLlmRequestForHero(
|
||||
hid = hid,
|
||||
pid = cp.id,
|
||||
gameState = startingState
|
||||
)
|
||||
newGeneratedTextRequests = hs.map { hid =>
|
||||
PerformHeroDeparturesAction.notificationLlmRequestForHero(
|
||||
hid = hid,
|
||||
pid = provinceId,
|
||||
rulingFactionId = rulingFactionId,
|
||||
currentRoundId = gameState.currentRoundId,
|
||||
gameId = gameState.gameId
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} match {
|
||||
case RandomState(ars, fr) =>
|
||||
ars :+ ActionResult(
|
||||
`type` = END_HERO_DEPARTURE_PHASE,
|
||||
newRoundPhase = Some(NewRoundPhase(value = UNAFFILIATED_HERO_ACTIONS)),
|
||||
newRandomSeed = Some(fr.seed)
|
||||
} :+ ActionResultC(
|
||||
actionResultType = EndHeroDeparturePhaseResultType,
|
||||
newRoundPhase = Some(RoundPhase.UnaffiliatedHeroActions)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+233
-316
@@ -1,24 +1,7 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType
|
||||
import net.eagle0.eagle.common.beast_info.BeastInfo
|
||||
import net.eagle0.eagle.common.date.Date
|
||||
import net.eagle0.eagle.common.province_event.*
|
||||
import net.eagle0.eagle.common.round_phase.NewRoundPhase
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase.FORCED_TURN_BACK
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult as ActionResultProto
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.changed_faction.ChangedFaction
|
||||
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.changed_province.ChangedProvince.ProvinceEventsReplacement
|
||||
import net.eagle0.eagle.internal.faction.PrestigeModifier
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.internal.province.Province as ProvinceProto
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.RandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
BaseBeastsCount,
|
||||
BeastsDurationMonths,
|
||||
@@ -54,10 +37,33 @@ import net.eagle0.eagle.library.settings.{
|
||||
MonthsBetweenRiotsSupportMultiplier,
|
||||
RiotEventChance
|
||||
}
|
||||
import net.eagle0.eagle.library.util.*
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.BeastUtils
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.concrete.{
|
||||
ActionResultC,
|
||||
ChangedBattalionC,
|
||||
ChangedFactionC,
|
||||
ChangedHeroC,
|
||||
StatDelta
|
||||
}
|
||||
import net.eagle0.eagle.model.action_result.types.ProvinceEventsChangedResultType
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.{BeastInfo, RoundPhase}
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.faction.PrestigeModifier
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.{
|
||||
BeastsEvent,
|
||||
BlizzardEvent,
|
||||
DroughtEvent,
|
||||
EpidemicEvent,
|
||||
FestivalEvent,
|
||||
FloodEvent,
|
||||
ImminentRiotEvent,
|
||||
ProvinceEvent
|
||||
}
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
case class ProvinceEventRolls(
|
||||
@@ -103,20 +109,18 @@ object PerformProvinceEventsAction {
|
||||
}
|
||||
|
||||
def beastsCount(
|
||||
p: ProvinceProto,
|
||||
p: ProvinceT,
|
||||
beastInfo: BeastInfo,
|
||||
beastCountRoll: Double
|
||||
): Int =
|
||||
((BaseBeastsCount.intValue + LegacyProvinceUtils.effectiveEconomy(
|
||||
p
|
||||
) * MaxBeastsIncreasePerEconomy.doubleValue) * beastInfo.maxCountMultiplier * beastCountRoll).ceil.toInt
|
||||
((BaseBeastsCount.intValue + ProvinceUtils.effectiveEconomy(p) * MaxBeastsIncreasePerEconomy.doubleValue) *
|
||||
beastInfo.maxCountMultiplier * beastCountRoll).ceil.toInt
|
||||
}
|
||||
|
||||
case class PerformProvinceEventsAction(
|
||||
gameState: GameState
|
||||
) extends RandomSequentialResultsAction(GameStateConverter.toProto(gameState)) {
|
||||
private val startingState: GameStateProto = GameStateConverter.toProto(gameState)
|
||||
import net.eagle0.eagle.library.util.DateProtoUtils.*
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
import PerformProvinceEventsAction.*
|
||||
|
||||
private val blizzardMonths = Set(12, 1, 2)
|
||||
private val floodMonths = Set(6, 7, 8)
|
||||
@@ -124,17 +128,23 @@ case class PerformProvinceEventsAction(
|
||||
private val epidemicMonths = Set(3, 4, 5, 6, 7, 8, 9, 10)
|
||||
private val droughtMonths = Set(6, 7, 8, 9)
|
||||
|
||||
override def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultProtoApplier: ActionResultProtoApplier
|
||||
): RandomState[Vector[ActionResultProto]] =
|
||||
if startingState.currentRoundId <= MinimumMonthsBeforeFirstProvinceEvents.intValue
|
||||
then
|
||||
private def currentDate: Date = gameState.currentDate.get
|
||||
private def currentMonth: Int = currentDate.month.value
|
||||
|
||||
private def isUnderAttack(p: ProvinceT): Boolean =
|
||||
p.incomingArmies
|
||||
.filter(_.arrivalRound == gameState.currentRoundId)
|
||||
.exists(ma => p.rulingFactionId.forall(_ != ma.army.factionId))
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
if gameState.currentRoundId <= MinimumMonthsBeforeFirstProvinceEvents.intValue then
|
||||
RandomState(
|
||||
Vector(
|
||||
ActionResultProto(
|
||||
`type` = ActionResultType.PROVINCE_EVENTS_CHANGED,
|
||||
newRoundPhase = Some(NewRoundPhase(value = FORCED_TURN_BACK))
|
||||
ActionResultC(
|
||||
actionResultType = ProvinceEventsChangedResultType,
|
||||
newRoundPhase = Some(RoundPhase.ForcedTurnBack)
|
||||
)
|
||||
),
|
||||
functionalRandom
|
||||
@@ -142,14 +152,14 @@ case class PerformProvinceEventsAction(
|
||||
else
|
||||
changedProvinces(functionalRandom).map { cps =>
|
||||
Vector(
|
||||
ActionResultProto(
|
||||
`type` = ActionResultType.PROVINCE_EVENTS_CHANGED,
|
||||
newRoundPhase = Some(NewRoundPhase(value = FORCED_TURN_BACK)),
|
||||
ActionResultC(
|
||||
actionResultType = ProvinceEventsChangedResultType,
|
||||
newRoundPhase = Some(RoundPhase.ForcedTurnBack),
|
||||
changedProvinces = cps,
|
||||
changedBattalions = changedBattalions,
|
||||
changedHeroes = changedHeroes,
|
||||
changedFactions = startingState.provinces.values
|
||||
.foldLeft(Map[FactionId, ChangedFaction]()) {
|
||||
changedFactions = gameState.provinces.values
|
||||
.foldLeft(Map.empty[FactionId, ChangedFactionC]) {
|
||||
case (acc, p) =>
|
||||
changedFaction(acc, p)
|
||||
}
|
||||
@@ -159,229 +169,171 @@ case class PerformProvinceEventsAction(
|
||||
)
|
||||
}
|
||||
|
||||
def changedProvinces(
|
||||
private def changedProvinces(
|
||||
fr: FunctionalRandom
|
||||
): RandomState[Vector[ChangedProvince]] =
|
||||
startingState.provinces.values
|
||||
.foldLeft(RandomState(Vector.empty[ChangedProvince], fr)) {
|
||||
): RandomState[Vector[ChangedProvinceC]] =
|
||||
gameState.provinces.values
|
||||
.foldLeft(RandomState(Vector.empty[ChangedProvinceC], fr)) {
|
||||
case (RandomState(acc, newR), province) =>
|
||||
ProvinceEventRolls.nextRolls(newR).map { rolls =>
|
||||
acc ++ changedProvince(province, rolls)
|
||||
}
|
||||
}
|
||||
|
||||
def changedProvinceFromEvents(
|
||||
p: ProvinceProto,
|
||||
cp: ChangedProvince,
|
||||
private def changedProvinceFromEvents(
|
||||
p: ProvinceT,
|
||||
startingCp: ChangedProvinceC,
|
||||
epidemicEndRoll: Double
|
||||
): ChangedProvince =
|
||||
p.activeEvents.foldLeft(cp) {
|
||||
case (cp, event) =>
|
||||
event match {
|
||||
case _: BlizzardEvent =>
|
||||
cp.update(
|
||||
_.optionalEconomyDevastationDelta.modify(dd =>
|
||||
Some(
|
||||
dd.getOrElse(0.0) + BlizzardEconomyDevastationDelta.doubleValue
|
||||
)
|
||||
),
|
||||
_.optionalAgricultureDevastationDelta.modify(dd =>
|
||||
Some(
|
||||
dd.getOrElse(
|
||||
0.0
|
||||
) + BlizzardAgricultureDevastationDelta.doubleValue
|
||||
)
|
||||
),
|
||||
_.optionalInfrastructureDevastationDelta.modify(dd =>
|
||||
Some(
|
||||
dd.getOrElse(
|
||||
0.0
|
||||
) + BlizzardInfrastructureDevastationDelta.doubleValue
|
||||
)
|
||||
)
|
||||
): ChangedProvinceC =
|
||||
p.activeEvents.foldLeft(startingCp) { (cp: ChangedProvinceC, event: ProvinceEvent) =>
|
||||
event match {
|
||||
case _: BlizzardEvent =>
|
||||
cp.copy(
|
||||
economyDevastationDelta =
|
||||
Some(cp.economyDevastationDelta.getOrElse(0.0) + BlizzardEconomyDevastationDelta.doubleValue),
|
||||
agricultureDevastationDelta =
|
||||
Some(cp.agricultureDevastationDelta.getOrElse(0.0) + BlizzardAgricultureDevastationDelta.doubleValue),
|
||||
infrastructureDevastationDelta = Some(
|
||||
cp.infrastructureDevastationDelta.getOrElse(0.0) + BlizzardInfrastructureDevastationDelta.doubleValue
|
||||
)
|
||||
case _: FloodEvent =>
|
||||
cp.update(
|
||||
_.optionalAgricultureDevastationDelta.modify(dd =>
|
||||
Some(
|
||||
dd.getOrElse(
|
||||
0.0
|
||||
) + (MaxFloodAgricultureDevastationDelta.doubleValue - p.infrastructure * FloodDevastationDeltaReductionPerInfrastructure.doubleValue)
|
||||
.max(0.0)
|
||||
)
|
||||
),
|
||||
_.optionalInfrastructureDevastationDelta.modify(dd =>
|
||||
Some(
|
||||
dd.getOrElse(
|
||||
0.0
|
||||
) + (MaxFloodInfrastructureDevastationDelta.doubleValue - p.infrastructure * FloodDevastationDeltaReductionPerInfrastructure.doubleValue)
|
||||
.max(0.0)
|
||||
)
|
||||
)
|
||||
)
|
||||
case _: FloodEvent =>
|
||||
cp.copy(
|
||||
agricultureDevastationDelta = Some(
|
||||
cp.agricultureDevastationDelta.getOrElse(0.0) +
|
||||
(MaxFloodAgricultureDevastationDelta.doubleValue -
|
||||
p.infrastructure * FloodDevastationDeltaReductionPerInfrastructure.doubleValue).max(0.0)
|
||||
),
|
||||
infrastructureDevastationDelta = Some(
|
||||
cp.infrastructureDevastationDelta.getOrElse(0.0) +
|
||||
(MaxFloodInfrastructureDevastationDelta.doubleValue -
|
||||
p.infrastructure * FloodDevastationDeltaReductionPerInfrastructure.doubleValue).max(0.0)
|
||||
)
|
||||
case BeastsEvent(_, _, _, beastInfo, _ /* unknownFieldSet */ ) =>
|
||||
beastInfo.map { beast =>
|
||||
cp.update(
|
||||
_.optionalEconomyDevastationDelta.modify(dd =>
|
||||
Some(dd.getOrElse(0.0) + beast.economyDevastation)
|
||||
.filterNot(_ == 0.0)
|
||||
),
|
||||
_.optionalAgricultureDevastationDelta.modify(dd =>
|
||||
Some(dd.getOrElse(0.0) + beast.agricultureDevastation)
|
||||
.filterNot(_ == 0.0)
|
||||
),
|
||||
_.optionalInfrastructureDevastationDelta.modify(dd =>
|
||||
Some(dd.getOrElse(0.0) + beast.infrastructureDevastation)
|
||||
.filterNot(_ == 0.0)
|
||||
),
|
||||
_.optionalSupportDelta.modify(sd => Some(sd.getOrElse(0.0) - BeastsSupportDamage.doubleValue))
|
||||
)
|
||||
}
|
||||
.getOrElse(cp)
|
||||
case _: FestivalEvent =>
|
||||
cp.update(
|
||||
_.optionalSupportDelta.modify(sd => Some(sd.getOrElse(0.0) + FestivalSupportDelta.doubleValue))
|
||||
)
|
||||
case _: EpidemicEvent =>
|
||||
cp.update(
|
||||
_.optionalEconomyDevastationDelta.modify(dd =>
|
||||
Some(
|
||||
dd.getOrElse(0.0) + EpidemicEconomyDevastationDelta.doubleValue
|
||||
)
|
||||
),
|
||||
_.optionalNewProvinceEvents := Option.when(
|
||||
!epidemicMonths.contains(startingState.currentDate.get.month) ||
|
||||
epidemicEndRoll < EpidemicEndChance.doubleValue
|
||||
) {
|
||||
ProvinceEventsReplacement(
|
||||
newEvents = cp.newProvinceEvents
|
||||
.map(_.newEvents)
|
||||
.getOrElse(p.activeEvents)
|
||||
.filterNot(ProvinceEventUtils.isEpidemicEvent)
|
||||
)
|
||||
}
|
||||
)
|
||||
case _: DroughtEvent =>
|
||||
cp.update(
|
||||
_.optionalAgricultureDevastationDelta.modify(dd =>
|
||||
Some(
|
||||
dd.getOrElse(
|
||||
0.0
|
||||
) + DroughtAgricultureDevastationDelta.doubleValue
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
case _: ImminentRiotEvent => cp
|
||||
case ProvinceEvent.Empty => cp
|
||||
}
|
||||
)
|
||||
case BeastsEvent(_, _, _, beastInfo) =>
|
||||
cp.copy(
|
||||
economyDevastationDelta =
|
||||
Some(cp.economyDevastationDelta.getOrElse(0.0) + beastInfo.economyDevastation).filterNot(_ == 0.0),
|
||||
agricultureDevastationDelta = Some(
|
||||
cp.agricultureDevastationDelta.getOrElse(0.0) + beastInfo.agricultureDevastation
|
||||
).filterNot(_ == 0.0),
|
||||
infrastructureDevastationDelta = Some(
|
||||
cp.infrastructureDevastationDelta.getOrElse(0.0) + beastInfo.infrastructureDevastation
|
||||
).filterNot(_ == 0.0),
|
||||
supportDelta = Some(cp.supportDelta.getOrElse(0.0) - BeastsSupportDamage.doubleValue)
|
||||
)
|
||||
case _: FestivalEvent =>
|
||||
cp.copy(
|
||||
supportDelta = Some(cp.supportDelta.getOrElse(0.0) + FestivalSupportDelta.doubleValue)
|
||||
)
|
||||
case _: EpidemicEvent =>
|
||||
val shouldEndEpidemic =
|
||||
!epidemicMonths.contains(currentMonth) || epidemicEndRoll < EpidemicEndChance.doubleValue
|
||||
cp.copy(
|
||||
economyDevastationDelta =
|
||||
Some(cp.economyDevastationDelta.getOrElse(0.0) + EpidemicEconomyDevastationDelta.doubleValue),
|
||||
newProvinceEvents =
|
||||
if shouldEndEpidemic then
|
||||
Some(cp.newProvinceEvents.getOrElse(p.activeEvents).filterNot {
|
||||
case _: EpidemicEvent => true; case _ => false
|
||||
})
|
||||
else cp.newProvinceEvents
|
||||
)
|
||||
case _: DroughtEvent =>
|
||||
cp.copy(
|
||||
agricultureDevastationDelta =
|
||||
Some(cp.agricultureDevastationDelta.getOrElse(0.0) + DroughtAgricultureDevastationDelta.doubleValue)
|
||||
)
|
||||
case _: ImminentRiotEvent => cp
|
||||
}
|
||||
}
|
||||
|
||||
def blizzardDurationMonths(currentMonth: Int): FactionId =
|
||||
private def blizzardDurationMonths(currentMonth: Int): Int =
|
||||
(11 + MaxBlizzardDurationMonths.intValue - currentMonth) % 12
|
||||
|
||||
def newBlizzardEvent(
|
||||
province: ProvinceProto,
|
||||
private def newBlizzardEvent(
|
||||
province: ProvinceT,
|
||||
blizzardEventRoll: Double
|
||||
): Option[ProvinceEvent] =
|
||||
Option.when(
|
||||
blizzardMonths
|
||||
.contains(startingState.currentDate.get.month)
|
||||
&& !LegacyProvinceUtils.hasBlizzard(province)
|
||||
blizzardMonths.contains(currentMonth)
|
||||
&& !ProvinceUtils.hasBlizzard(province)
|
||||
&& blizzardEventRoll < BlizzardEventChance.doubleValue
|
||||
) {
|
||||
BlizzardEvent(
|
||||
startDate = Some(startingState.currentDate.get),
|
||||
endDate = Some(
|
||||
startingState.currentDate.get.addMonths(
|
||||
blizzardDurationMonths(startingState.currentDate.get.month)
|
||||
)
|
||||
)
|
||||
startDate = currentDate,
|
||||
endDate = currentDate.addMonths(blizzardDurationMonths(currentMonth))
|
||||
)
|
||||
}
|
||||
|
||||
def newDroughtEvent(
|
||||
province: ProvinceProto,
|
||||
private def newDroughtEvent(
|
||||
province: ProvinceT,
|
||||
droughtEventRoll: Double
|
||||
): Option[ProvinceEvent] =
|
||||
Option.when(
|
||||
droughtMonths
|
||||
.contains(startingState.currentDate.get.month)
|
||||
&& !LegacyProvinceUtils.hasDrought(province)
|
||||
&& !LegacyProvinceUtils.hasFlood(province)
|
||||
droughtMonths.contains(currentMonth)
|
||||
&& !ProvinceUtils.hasDrought(province)
|
||||
&& !ProvinceUtils.hasFlood(province)
|
||||
&& droughtEventRoll < DroughtEventChance.doubleValue
|
||||
) {
|
||||
DroughtEvent(
|
||||
startDate = Some(startingState.currentDate.get),
|
||||
endDate = Some(
|
||||
startingState.currentDate.get.addMonths(
|
||||
DroughtDurationMonths.intValue
|
||||
)
|
||||
)
|
||||
startDate = currentDate,
|
||||
endDate = currentDate.addMonths(DroughtDurationMonths.intValue)
|
||||
)
|
||||
}
|
||||
|
||||
def newFloodEvent(
|
||||
province: ProvinceProto,
|
||||
private def newFloodEvent(
|
||||
province: ProvinceT,
|
||||
floodEventRoll: Double
|
||||
): Option[ProvinceEvent] =
|
||||
Option.when(
|
||||
floodMonths
|
||||
.contains(startingState.currentDate.get.month)
|
||||
&& !LegacyProvinceUtils.hasFlood(province)
|
||||
&& !LegacyProvinceUtils.hasDrought(province)
|
||||
floodMonths.contains(currentMonth)
|
||||
&& !ProvinceUtils.hasFlood(province)
|
||||
&& !ProvinceUtils.hasDrought(province)
|
||||
&& floodEventRoll < FloodEventChance.doubleValue
|
||||
) {
|
||||
FloodEvent(
|
||||
startDate = Some(startingState.currentDate.get),
|
||||
endDate = Some(
|
||||
startingState.currentDate.get.addMonths(FloodDurationMonths.intValue)
|
||||
)
|
||||
startDate = currentDate,
|
||||
endDate = currentDate.addMonths(FloodDurationMonths.intValue)
|
||||
)
|
||||
}
|
||||
|
||||
def newFestivalEvent(
|
||||
province: ProvinceProto,
|
||||
private def newFestivalEvent(
|
||||
province: ProvinceT,
|
||||
festivalEventRoll: Double
|
||||
): Option[ProvinceEvent] =
|
||||
Option.when(
|
||||
festivalMonths
|
||||
.contains(startingState.currentDate.get.month)
|
||||
&& !LegacyProvinceUtils.hasBlizzard(province)
|
||||
&& !LegacyProvinceUtils.hasDrought(province)
|
||||
&& !LegacyProvinceUtils.hasFestival(province)
|
||||
festivalMonths.contains(currentMonth)
|
||||
&& !ProvinceUtils.hasBlizzard(province)
|
||||
&& !ProvinceUtils.hasDrought(province)
|
||||
&& !ProvinceUtils.hasFestival(province)
|
||||
&& festivalEventRoll < FestivalEventChance.doubleValue
|
||||
) {
|
||||
FestivalEvent(
|
||||
startDate = Some(startingState.currentDate.get),
|
||||
endDate = Some(
|
||||
startingState.currentDate.get.addMonths(
|
||||
MaxFestivalDurationMonths.intValue
|
||||
)
|
||||
)
|
||||
startDate = currentDate,
|
||||
endDate = currentDate.addMonths(MaxFestivalDurationMonths.intValue)
|
||||
)
|
||||
}
|
||||
|
||||
def newEpidemicEvent(
|
||||
province: ProvinceProto,
|
||||
private def newEpidemicEvent(
|
||||
province: ProvinceT,
|
||||
epidemicRoll: Double
|
||||
): Option[ProvinceEvent] =
|
||||
Option.when(
|
||||
epidemicMonths
|
||||
.contains(startingState.currentDate.get.month)
|
||||
&& !LegacyProvinceUtils.hasEpidemic(province)
|
||||
&& (epidemicRoll < EpidemicBreakoutChance.doubleValue || (province.neighbors
|
||||
.map(_.provinceId)
|
||||
.map(startingState.provinces)
|
||||
.exists(
|
||||
LegacyProvinceUtils.hasEpidemic
|
||||
) && epidemicRoll < EpidemicSpreadChance.doubleValue))
|
||||
epidemicMonths.contains(currentMonth)
|
||||
&& !ProvinceUtils.hasEpidemic(province)
|
||||
&& (epidemicRoll < EpidemicBreakoutChance.doubleValue ||
|
||||
(province.neighbors
|
||||
.map(_.provinceId)
|
||||
.map(gameState.provinces)
|
||||
.exists(ProvinceUtils.hasEpidemic)
|
||||
&& epidemicRoll < EpidemicSpreadChance.doubleValue))
|
||||
) {
|
||||
EpidemicEvent(startDate = Some(startingState.currentDate.get))
|
||||
EpidemicEvent(startDate = currentDate)
|
||||
}
|
||||
|
||||
def newBeastsEvent(
|
||||
province: ProvinceProto,
|
||||
private def newBeastsEvent(
|
||||
province: ProvinceT,
|
||||
beastsEventRoll: Double,
|
||||
beastTypeRoll: Double,
|
||||
beastCountRoll: Double
|
||||
@@ -390,71 +342,60 @@ case class PerformProvinceEventsAction(
|
||||
province.lastBeastsDate.isEmpty ||
|
||||
province.lastBeastsDate.exists(lbd =>
|
||||
lbd.addMonths(
|
||||
(MonthsBetweenBeastsInfrastructureMultiplier.doubleValue * LegacyProvinceUtils
|
||||
.effectiveInfrastructure(province)).floor.toInt
|
||||
) < startingState.currentDate.get
|
||||
(MonthsBetweenBeastsInfrastructureMultiplier.doubleValue * ProvinceUtils.effectiveInfrastructure(
|
||||
province
|
||||
)).floor.toInt
|
||||
) < currentDate
|
||||
)
|
||||
|
||||
Option.when(
|
||||
hasBeenLongEnough
|
||||
&& beastsEventRoll < BeastsEventChance.doubleValue
|
||||
&& !LegacyProvinceUtils.hasBeasts(province)
|
||||
&& !LegacyProvinceUtils.hasFestival(province)
|
||||
&& !ProvinceUtils.hasBeasts(province)
|
||||
&& !ProvinceUtils.hasFestival(province)
|
||||
&& province.rulingFactionId.isDefined
|
||||
) {
|
||||
val beastInfo = PerformProvinceEventsAction.beastType(beastTypeRoll)
|
||||
BeastsEvent(
|
||||
count = PerformProvinceEventsAction
|
||||
.beastsCount(province, beastInfo, beastCountRoll),
|
||||
startDate = Some(startingState.currentDate.get),
|
||||
endDate = Some(
|
||||
startingState.currentDate.get.addMonths(BeastsDurationMonths.intValue)
|
||||
),
|
||||
beastInfo = Some(beastInfo)
|
||||
count = PerformProvinceEventsAction.beastsCount(province, beastInfo, beastCountRoll),
|
||||
startDate = currentDate,
|
||||
endDate = currentDate.addMonths(BeastsDurationMonths.intValue),
|
||||
beastInfo = beastInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def newImminentRiotEvent(
|
||||
province: ProvinceProto,
|
||||
private def newImminentRiotEvent(
|
||||
province: ProvinceT,
|
||||
riotEventRoll: Double
|
||||
): Option[ProvinceEvent] = {
|
||||
val hasBeenLongEnough =
|
||||
province.lastRiotDate.isEmpty ||
|
||||
province.lastRiotDate.exists(lbd =>
|
||||
lbd.addMonths(
|
||||
province.lastRiotDate.exists(lrd =>
|
||||
lrd.addMonths(
|
||||
(MonthsBetweenRiotsSupportMultiplier.doubleValue * province.support).floor.toInt
|
||||
) < startingState.currentDate.get
|
||||
) < currentDate
|
||||
)
|
||||
|
||||
val hasActiveBeastsWithFutureEnd = province.activeEvents.exists {
|
||||
case BeastsEvent(_, _, endDate, _) => endDate > currentDate
|
||||
case _ => false
|
||||
}
|
||||
|
||||
Option.when(
|
||||
hasBeenLongEnough &&
|
||||
riotEventRoll < RiotEventChance.doubleValue
|
||||
hasBeenLongEnough
|
||||
&& riotEventRoll < RiotEventChance.doubleValue
|
||||
&& province.support >= MinSupportForTaxes.doubleValue
|
||||
&& !IncomingArmyUtils.isUnderAttack(province, startingState)
|
||||
&& !LegacyProvinceUtils.hasEvent(
|
||||
province,
|
||||
{
|
||||
case BeastsEvent(
|
||||
_: Int,
|
||||
_: Option[Date],
|
||||
endDate: Option[Date],
|
||||
_: Option[BeastInfo],
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
endDate.exists(_ > startingState.currentDate.get)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
&& !LegacyProvinceUtils.hasFestival(province)
|
||||
&& !isUnderAttack(province)
|
||||
&& !hasActiveBeastsWithFutureEnd
|
||||
&& !ProvinceUtils.hasFestival(province)
|
||||
) {
|
||||
ImminentRiotEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// We won't riot while there are beasts or a festival
|
||||
def newProvinceEvents(
|
||||
province: ProvinceProto,
|
||||
private def newProvinceEvents(
|
||||
province: ProvinceT,
|
||||
provinceEventRolls: ProvinceEventRolls
|
||||
): Vector[ProvinceEvent] =
|
||||
newBlizzardEvent(province, provinceEventRolls.blizzardRoll)
|
||||
@@ -469,34 +410,28 @@ case class PerformProvinceEventsAction(
|
||||
beastCountRoll = provinceEventRolls.beastCountRoll
|
||||
)
|
||||
)
|
||||
.orElse(
|
||||
newImminentRiotEvent(province, provinceEventRolls.riotRoll)
|
||||
)
|
||||
.orElse(
|
||||
newEpidemicEvent(province, provinceEventRolls.epidemicStartRoll)
|
||||
)
|
||||
.orElse(newImminentRiotEvent(province, provinceEventRolls.riotRoll))
|
||||
.orElse(newEpidemicEvent(province, provinceEventRolls.epidemicStartRoll))
|
||||
.toVector
|
||||
|
||||
def changedFaction(
|
||||
existingCFs: Map[FactionId, ChangedFaction],
|
||||
province: ProvinceProto
|
||||
): Map[FactionId, ChangedFaction] =
|
||||
private def changedFaction(
|
||||
existingCFs: Map[FactionId, ChangedFactionC],
|
||||
province: ProvinceT
|
||||
): Map[FactionId, ChangedFactionC] =
|
||||
province.rulingFactionId.map { fid =>
|
||||
val existingCf =
|
||||
existingCFs.getOrElse(fid, ChangedFaction(id = fid))
|
||||
val existingCf = existingCFs.getOrElse(fid, ChangedFactionC(factionId = fid))
|
||||
|
||||
val newCf = province.activeEvents.foldLeft(existingCf) {
|
||||
case (cf, event) =>
|
||||
event match {
|
||||
case _: FestivalEvent =>
|
||||
cf.update(
|
||||
_.addedPrestigeModifiers :+= PrestigeModifier(
|
||||
`type` = PrestigeModifier.Type.FESTIVAL,
|
||||
value = FestivalPrestigeDelta.doubleValue
|
||||
)
|
||||
val newCf = province.activeEvents.foldLeft(existingCf) { (cf: ChangedFactionC, event: ProvinceEvent) =>
|
||||
event match {
|
||||
case _: FestivalEvent =>
|
||||
cf.copy(
|
||||
newPrestigeModifiers = cf.newPrestigeModifiers :+ PrestigeModifier(
|
||||
prestigeModifierType = PrestigeModifier.Type.Festival,
|
||||
value = FestivalPrestigeDelta.doubleValue
|
||||
)
|
||||
case _ => cf
|
||||
}
|
||||
)
|
||||
case _ => cf
|
||||
}
|
||||
}
|
||||
|
||||
if newCf == existingCf then existingCFs
|
||||
@@ -504,37 +439,37 @@ case class PerformProvinceEventsAction(
|
||||
}
|
||||
.getOrElse(existingCFs)
|
||||
|
||||
def changedHeroes: Vector[ChangedHero] =
|
||||
startingState.provinces.values
|
||||
.filter(LegacyProvinceUtils.hasEpidemic)
|
||||
private def changedHeroes: Vector[ChangedHeroC] =
|
||||
gameState.provinces.values
|
||||
.filter(ProvinceUtils.hasEpidemic)
|
||||
.flatMap(p => p.rulingFactionHeroIds ++ p.unaffiliatedHeroes.map(_.heroId))
|
||||
.map(startingState.heroes)
|
||||
.map(hero =>
|
||||
ChangedHero(
|
||||
id = hero.id,
|
||||
vigor = VigorDelta(EpidemicVigorDelta.doubleValue)
|
||||
.map(hid =>
|
||||
ChangedHeroC(
|
||||
heroId = hid,
|
||||
vigorChange = StatDelta(EpidemicVigorDelta.doubleValue)
|
||||
)
|
||||
)
|
||||
.toVector
|
||||
|
||||
def changedBattalions: Vector[Battalion] =
|
||||
startingState.provinces.values
|
||||
.filter(LegacyProvinceUtils.hasEpidemic)
|
||||
private def changedBattalions: Vector[ChangedBattalionC] =
|
||||
gameState.provinces.values
|
||||
.filter(ProvinceUtils.hasEpidemic)
|
||||
.flatMap(_.battalionIds)
|
||||
.map(startingState.battalions)
|
||||
.map(gameState.battalions)
|
||||
.map(b =>
|
||||
b.update(
|
||||
_.size
|
||||
.modify(s => (s * (1.0 - EpidemicBattalionLossPercentage.doubleValue)).toInt)
|
||||
ChangedBattalionC(
|
||||
to = b.updateWith(
|
||||
size = (b.size * (1.0 - EpidemicBattalionLossPercentage.doubleValue)).toInt
|
||||
)
|
||||
)
|
||||
)
|
||||
.toVector
|
||||
|
||||
def changedProvince(
|
||||
province: ProvinceProto,
|
||||
private[action] def changedProvince(
|
||||
province: ProvinceT,
|
||||
provinceEventRolls: ProvinceEventRolls
|
||||
): Option[ChangedProvince] = {
|
||||
val startingCp = ChangedProvince(id = province.id)
|
||||
): Option[ChangedProvinceC] = {
|
||||
val startingCp = ChangedProvinceC(provinceId = province.id)
|
||||
|
||||
val fromProvinceEvents = changedProvinceFromEvents(
|
||||
province,
|
||||
@@ -543,36 +478,18 @@ case class PerformProvinceEventsAction(
|
||||
)
|
||||
|
||||
val newProvinceEventSet = province.activeEvents.filter {
|
||||
case ProvinceEvent.Empty => ???
|
||||
case DroughtEvent(startDate, endDate, _ /* unknownFieldSet */ ) =>
|
||||
endDate.exists(_ >= startingState.currentDate.get)
|
||||
case BeastsEvent(
|
||||
count,
|
||||
startDate,
|
||||
endDate,
|
||||
beastInfo,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
endDate.exists(_ >= startingState.currentDate.get)
|
||||
case BlizzardEvent(startDate, endDate, _ /* unknownFieldSet */ ) =>
|
||||
endDate.exists(_ >= startingState.currentDate.get)
|
||||
case FestivalEvent(startDate, endDate, _ /* unknownFieldSet */ ) =>
|
||||
endDate.exists(_ >= startingState.currentDate.get)
|
||||
case FloodEvent(startDate, endDate, _ /* unknownFieldSet */ ) =>
|
||||
endDate.exists(_ >= startingState.currentDate.get)
|
||||
case EpidemicEvent(startDate, _ /* unknownFieldSet */ ) => true
|
||||
case ImminentRiotEvent(_ /* unknownFieldSet */ ) => true
|
||||
} ++ newProvinceEvents(
|
||||
province,
|
||||
provinceEventRolls
|
||||
)
|
||||
case DroughtEvent(_, endDate) => endDate >= currentDate
|
||||
case BeastsEvent(_, _, endDate, _) => endDate >= currentDate
|
||||
case BlizzardEvent(_, endDate) => endDate >= currentDate
|
||||
case FestivalEvent(_, endDate) => endDate >= currentDate
|
||||
case FloodEvent(_, endDate) => endDate >= currentDate
|
||||
case _: EpidemicEvent => true
|
||||
case _: ImminentRiotEvent => true
|
||||
} ++ newProvinceEvents(province, provinceEventRolls)
|
||||
|
||||
val withEventsReplacement =
|
||||
if newProvinceEventSet == province.activeEvents then fromProvinceEvents
|
||||
else
|
||||
fromProvinceEvents.withNewProvinceEvents(
|
||||
ProvinceEventsReplacement(newProvinceEventSet)
|
||||
)
|
||||
else fromProvinceEvents.copy(newProvinceEvents = Some(newProvinceEventSet))
|
||||
|
||||
Option.when(withEventsReplacement != startingCp)(withEventsReplacement)
|
||||
}
|
||||
|
||||
+46
-76
@@ -1,94 +1,64 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.internal.army.MovingArmy
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.internal.province.Province as ProvinceProto
|
||||
import net.eagle0.eagle.library.actions.applier.{ActionResultTApplier, ActionResultTWithResultingState}
|
||||
import net.eagle0.eagle.library.actions.impl.common.TRandomSequentialResultsAction
|
||||
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.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.{ArmyConverter, SuppliesConverter}
|
||||
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.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.MovingArmy
|
||||
|
||||
case class PerformProvinceMoveResolutionAction(
|
||||
gameState: GameState
|
||||
) extends TRandomSequentialResultsAction(gameState) {
|
||||
gameState: GameState,
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultTApplier: ActionResultTApplier
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] = {
|
||||
val startingState = GameStateConverter.toProto(gameState)
|
||||
val provincesWithIncomingFriendlies = startingState.provinces.values
|
||||
val provincesWithIncomingFriendlies = gameState.provinces.values
|
||||
.filter(_.rulingFactionId.isDefined)
|
||||
.map(p =>
|
||||
(
|
||||
p,
|
||||
p.incomingArmies.filter(ma =>
|
||||
ma.arrivalRound == startingState.currentRoundId && isFriendlyMove(
|
||||
ma,
|
||||
p
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.filter(_._2.nonEmpty)
|
||||
|
||||
val movingFriendliesStates = provincesWithIncomingFriendlies
|
||||
.foldLeft(Vector(ActionResultTWithResultingState(null, startingState))) {
|
||||
case (acc, (province, armies)) =>
|
||||
armies.foldLeft(acc) {
|
||||
case (acc2, army) =>
|
||||
acc2 :+ actionResultTApplier.applyActionResult(
|
||||
acc2.last.resultingState,
|
||||
FriendlyMoveAction(
|
||||
ArmyConverter.fromProto(army),
|
||||
province.id
|
||||
).immediateExecute
|
||||
)
|
||||
}
|
||||
.flatMap { province =>
|
||||
val friendlyArmies = province.incomingArmies.filter { ma =>
|
||||
ma.arrivalRound == gameState.currentRoundId && isFriendlyMove(ma, province)
|
||||
}
|
||||
friendlyArmies.map(army => (province.id, army))
|
||||
}
|
||||
.toVector
|
||||
|
||||
val provincesWithIncomingShipments = startingState.provinces.values
|
||||
.map(p =>
|
||||
(
|
||||
p,
|
||||
p.incomingShipments
|
||||
.filter(ma => ma.arrivalRound == startingState.currentRoundId)
|
||||
)
|
||||
)
|
||||
.filter(_._2.nonEmpty)
|
||||
val provincesWithIncomingShipments = gameState.provinces.values.flatMap { province =>
|
||||
val arrivingShipments = province.incomingShipments
|
||||
.filter(_.arrivalRound == gameState.currentRoundId)
|
||||
arrivingShipments.map(shipment => (province.id, shipment))
|
||||
}.toVector
|
||||
|
||||
val states = provincesWithIncomingShipments
|
||||
.foldLeft(movingFriendliesStates) {
|
||||
case (acc, (province, shipments)) =>
|
||||
shipments.foldLeft(acc) {
|
||||
case (acc2, supplies) =>
|
||||
acc2 :+ actionResultTApplier.applyActionResult(
|
||||
acc2.last.resultingState,
|
||||
ShipmentArrivedAction(
|
||||
SuppliesConverter.fromProto(supplies),
|
||||
province.id
|
||||
).immediateExecute
|
||||
)
|
||||
}
|
||||
}
|
||||
.drop(1)
|
||||
val sequencer = RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
|
||||
val lastState = states.lastOption.map(_.resultingState).getOrElse(startingState)
|
||||
EndProvinceMoveResolutionPhaseAction(
|
||||
gameId = lastState.gameId,
|
||||
factions = lastState.factions.values.map(FactionConverter.fromProto).toVector,
|
||||
provinces = lastState.provinces.values.toVector.map(ProvinceConverter.fromProto),
|
||||
heroes = lastState.heroes.values.toVector.map(HeroConverter.fromProto)
|
||||
).randomResults(functionalRandom)
|
||||
.map(states.map(_.actionResult) ++ _)
|
||||
val afterFriendlyMoves = provincesWithIncomingFriendlies.foldLeft(sequencer) {
|
||||
case (seq, (provinceId, movingArmy)) =>
|
||||
seq.withProtolessSimpleAction(_ => FriendlyMoveAction(movingArmy, provinceId))
|
||||
}
|
||||
|
||||
val afterShipments = provincesWithIncomingShipments.foldLeft(afterFriendlyMoves) {
|
||||
case (seq, (provinceId, movingSupplies)) =>
|
||||
seq.withProtolessSimpleAction(_ => ShipmentArrivedAction(movingSupplies, provinceId))
|
||||
}
|
||||
|
||||
afterShipments.withRandomActionResults { (gs, fr) =>
|
||||
EndProvinceMoveResolutionPhaseAction(
|
||||
gameId = gs.gameId,
|
||||
factions = gs.factions.values.toVector,
|
||||
provinces = gs.provinces.values.toVector,
|
||||
heroes = gs.heroes.values.toVector
|
||||
).randomResults(fr)
|
||||
}.actionResults
|
||||
}
|
||||
|
||||
private def isFriendlyMove(ma: MovingArmy, p: ProvinceProto): Boolean =
|
||||
ma.army.forall(army => p.rulingFactionId.contains(army.factionId))
|
||||
private def isFriendlyMove(ma: MovingArmy, province: ProvinceT): Boolean =
|
||||
province.rulingFactionId.contains(ma.army.factionId)
|
||||
}
|
||||
|
||||
+36
-47
@@ -1,44 +1,41 @@
|
||||
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.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.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.util.view_filters.ProvinceViewFilter
|
||||
import net.eagle0.eagle.library.util.ReturningHeroes
|
||||
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.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.proto_converters.view.province.ProvinceViewConverter
|
||||
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
|
||||
) extends TRandomSequentialResultsAction(gameState) {
|
||||
|
||||
private val startingGameState: GameStateProto = GameStateConverter.toProto(gameState)
|
||||
gameState: GameState,
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultTApplier: ActionResultTApplier
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
startingGameState.provinces.values
|
||||
gameState.provinces.values
|
||||
.foldLeft(
|
||||
RandomStateTSequencer.fromProto(
|
||||
initialStateProto = startingGameState,
|
||||
actionResultApplier = actionResultTApplier,
|
||||
RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
) {
|
||||
case (sequencer, province) =>
|
||||
province.incomingEndTurnActions
|
||||
.filter(_.action.isRecon)
|
||||
province.incomingEndTurnActions.collect {
|
||||
case action @ IncomingEndTurnAction(_, _, _: IncomingRecon) => action
|
||||
}
|
||||
.foldLeft(sequencer) {
|
||||
case (innerSequencer, action) =>
|
||||
innerSequencer.withRandomActionResult {
|
||||
@@ -55,26 +52,25 @@ case class PerformReconResolutionAction(
|
||||
)
|
||||
}
|
||||
|
||||
def oneResult(
|
||||
// Package-private for testing
|
||||
private[action] def oneResult(
|
||||
provinceId: ProvinceId,
|
||||
incomingEndTurnAction: IncomingEndTurnActionProto,
|
||||
incomingEndTurnAction: IncomingEndTurnAction,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[ActionResultT] = {
|
||||
val originProvince =
|
||||
startingGameState.provinces(incomingEndTurnAction.fromProvinceId)
|
||||
val originProvince = gameState.provinces(incomingEndTurnAction.fromProvinceId)
|
||||
val fromFactionId = incomingEndTurnAction.fromFactionId
|
||||
val targetProvince = startingGameState.provinces(provinceId)
|
||||
val targetProvince = gameState.provinces(provinceId)
|
||||
|
||||
val recon = incomingEndTurnAction.getRecon
|
||||
// Extract heroId from the recon action details
|
||||
val heroId = incomingEndTurnAction.details.asInstanceOf[IncomingRecon].heroId
|
||||
|
||||
ReturningHeroes
|
||||
.heroesReturningToFaction(
|
||||
hids = Vector(recon.heroId),
|
||||
hids = Vector(heroId),
|
||||
factionId = fromFactionId,
|
||||
originProvince = ProvinceConverter.fromProto(originProvince),
|
||||
provinces = startingGameState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector,
|
||||
originProvince = originProvince,
|
||||
provinces = gameState.provinces.values.toVector,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.map { returningHeroes =>
|
||||
@@ -85,35 +81,28 @@ case class PerformReconResolutionAction(
|
||||
// the CP for the destination province
|
||||
ChangedProvinceC(
|
||||
provinceId = provinceId,
|
||||
removedIncomingEndTurnActions = Vector(
|
||||
IncomingEndTurnActionConverter.fromProto(incomingEndTurnAction)
|
||||
)
|
||||
removedIncomingEndTurnActions = Vector(incomingEndTurnAction)
|
||||
),
|
||||
// the CP for the acting hero returning
|
||||
returningHeroes.changedProvince
|
||||
),
|
||||
changedFactions = Vector(
|
||||
ChangedFactionC(
|
||||
factionId = incomingEndTurnAction.fromFactionId,
|
||||
factionId = fromFactionId,
|
||||
updatedReconnedProvinces = Vector(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
targetProvince,
|
||||
startingGameState
|
||||
// FIXME: setting factionId to None right now to grab the full info. This
|
||||
// probably isn't exactly what we want.
|
||||
)
|
||||
.withAsOf(startingGameState.currentDate.get)
|
||||
ProvinceViewConverter.toProto(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
gameState.provinces(provinceId),
|
||||
gameState
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
clientTextVisibilityExtensions = targetProvince.rulingFactionHeroIds.map { hid =>
|
||||
ClientTextVisibilityExtensionC(
|
||||
textId = startingGameState
|
||||
.heroes(hid)
|
||||
.backstoryVersions
|
||||
.last
|
||||
.textId,
|
||||
textId = gameState.heroes(hid).backstoryTextId,
|
||||
recipientFactionIds = Vector(fromFactionId)
|
||||
)
|
||||
}.toVector
|
||||
|
||||
+17
-20
@@ -2,9 +2,10 @@ 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.ActionResultTApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.{RandomStateTSequencer, TRandomSequentialResultsAction}
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
|
||||
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,
|
||||
@@ -26,9 +27,6 @@ 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
|
||||
@@ -105,31 +103,31 @@ object PerformUnaffiliatedHeroesAction {
|
||||
|
||||
case class PerformUnaffiliatedHeroesAction(
|
||||
gameState: GameState,
|
||||
heroGenerator: HeroGenerator
|
||||
) extends TRandomSequentialResultsAction(gameState) {
|
||||
heroGenerator: HeroGenerator,
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
private val currentDate = gameState.currentDate.get
|
||||
private val factions = gameState.factions.values.toVector
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultTApplier: ActionResultTApplier
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
RandomStateTSequencer(
|
||||
RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultTApplier,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
).withRandomActionResults((_, fr) => statusChangeResults(fr))
|
||||
.withRandomActionResults { (gs, fr) =>
|
||||
heroAppearsResults(GameStateConverter.fromProto(gs), fr)
|
||||
heroAppearsResults(gs, fr)
|
||||
}
|
||||
.withRandomContinuance { (randomSequencer: RandomStateTSequencer) =>
|
||||
.withRandomContinuance { (randomSequencer: RandomStateSequencer) =>
|
||||
val newHeroesMap =
|
||||
randomSequencer.actionResults.newValue
|
||||
.flatMap(_.newHeroes)
|
||||
.map(h => h.id -> h)
|
||||
.toMap
|
||||
|
||||
val currentGs = GameStateConverter.fromProto(randomSequencer.lastStateProto)
|
||||
val currentGs = randomSequencer.lastState
|
||||
val qcpsOpt = questChangedProvinces(
|
||||
currentGs = currentGs,
|
||||
newHeroesMap = newHeroesMap,
|
||||
@@ -144,15 +142,14 @@ case class PerformUnaffiliatedHeroesAction(
|
||||
}
|
||||
randomSequencer.withRandomActionResults((_, _) => qcpsOpt)
|
||||
}
|
||||
.withActionResultTs { gs =>
|
||||
.withActionResults { gs =>
|
||||
EndUnaffiliatedHeroActionsPhaseAction(
|
||||
gameId = GameStateConverter.fromProto(gs).gameId,
|
||||
gameId = gs.gameId,
|
||||
currentDate = currentDate,
|
||||
currentRoundId = gs.currentRoundId,
|
||||
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
|
||||
provinces = gs.provinces.values.toVector,
|
||||
heroes = gs.heroes.values.toVector,
|
||||
factions = gs.factions.values.toVector
|
||||
).results
|
||||
}
|
||||
.actionResults
|
||||
|
||||
+68
-51
@@ -5,96 +5,108 @@ import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, RestAvailableCommand}
|
||||
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType.*
|
||||
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.library.actions.impl.command.CommandFactory
|
||||
import net.eagle0.eagle.library.actions.impl.common.{RandomSequentialResultsAction, RandomStateProtoSequencer}
|
||||
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.CommandChooser
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
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: CommandFactory
|
||||
) extends RandomSequentialResultsAction(GameStateConverter.toProto(gameState)) {
|
||||
private val gameStateProto: GameStateProto = GameStateConverter.toProto(gameState)
|
||||
commandFactory: TCommandFactory,
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.*
|
||||
|
||||
override def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultProtoApplier: ActionResultProtoApplier
|
||||
): RandomState[Vector[ActionResultProto]] =
|
||||
gameStateProto.provinces.values
|
||||
private def vassalProvinces(gs: GameState): Vector[ProvinceT] =
|
||||
gs.provinces.values
|
||||
.filter(_.rulingFactionId.isDefined)
|
||||
.filterNot(LegacyProvinceUtils.ruledByFactionLeader(_, gameStateProto))
|
||||
.filterNot(ProvinceUtils.ruledByFactionLeader(_, gs.factions.values.toVector))
|
||||
.filterNot(_.hasActed)
|
||||
.toVector
|
||||
|
||||
override def randomResults(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
vassalProvinces(gameState)
|
||||
.foldLeft(
|
||||
RandomStateProtoSequencer(
|
||||
RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultProtoApplier = actionResultProtoApplier,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
) {
|
||||
case (sequencer, province) =>
|
||||
sequencer.withRandomActionResults {
|
||||
case (gs, fr) =>
|
||||
chooseCommand(province.id, fr)
|
||||
.map(
|
||||
_.map(cs =>
|
||||
commandFactory
|
||||
.makeCommand(
|
||||
actingFactionId = cs.actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommand = cs.available,
|
||||
selectedCommand = cs.selected
|
||||
)
|
||||
.execute(actionResultProtoApplier)
|
||||
.map(_.actionResult)
|
||||
).getOrElse(Vector())
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.getOrElse(sequencer)
|
||||
}
|
||||
.actionResults
|
||||
|
||||
private def maybeRestCommand(
|
||||
actingFactionId: FactionId,
|
||||
gs: GameStateProto,
|
||||
gsProto: GameStateProto,
|
||||
commandOptions: Vector[AvailableCommand],
|
||||
provinceId: ProvinceId,
|
||||
reason: String
|
||||
): Option[CommandSelection] = {
|
||||
val heroes =
|
||||
gs.provinces(provinceId)
|
||||
gsProto
|
||||
.provinces(provinceId)
|
||||
.rulingFactionHeroIds
|
||||
.map(gs.heroes)
|
||||
.map(gsProto.heroes)
|
||||
MoreOption.flatWhen(
|
||||
commandOptions.collectFirst {
|
||||
case ac: RestAvailableCommand =>
|
||||
ac
|
||||
}.isDefined && shouldRest(heroes)
|
||||
) {
|
||||
chosenRestCommand(actingFactionId, gs, commandOptions, reason)
|
||||
chosenRestCommand(actingFactionId, gsProto, commandOptions, reason)
|
||||
}
|
||||
}
|
||||
|
||||
private def selectedCommandFromOrders(
|
||||
actingFactionId: FactionId,
|
||||
gs: GameStateProto,
|
||||
gsProto: GameStateProto,
|
||||
commandOptions: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom,
|
||||
provinceId: ProvinceId
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
gs.provinces(provinceId).provinceOrders match {
|
||||
gsProto.provinces(provinceId).provinceOrders match {
|
||||
case ENTRUST =>
|
||||
chosenEntrustCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gs,
|
||||
gameState = gsProto,
|
||||
availableCommands = commandOptions,
|
||||
actingProvinceId = provinceId,
|
||||
functionalRandom = functionalRandom
|
||||
@@ -102,21 +114,21 @@ case class PerformVassalCommandsPhaseAction(
|
||||
case DEVELOP =>
|
||||
chosenDevelopCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gs,
|
||||
gameState = gsProto,
|
||||
availableCommands = commandOptions,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
case MOBILIZE =>
|
||||
chosenMobilizeCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gs,
|
||||
gameState = gsProto,
|
||||
availableCommands = commandOptions,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
case EXPAND =>
|
||||
chosenExpandCommand(
|
||||
actingFactionId,
|
||||
gs,
|
||||
gsProto,
|
||||
commandOptions,
|
||||
functionalRandom
|
||||
)
|
||||
@@ -125,7 +137,7 @@ case class PerformVassalCommandsPhaseAction(
|
||||
RandomState(
|
||||
chosenRestCommand(
|
||||
actingFactionId,
|
||||
gs,
|
||||
gsProto,
|
||||
commandOptions,
|
||||
"bad orders"
|
||||
),
|
||||
@@ -134,13 +146,17 @@ case class PerformVassalCommandsPhaseAction(
|
||||
}
|
||||
|
||||
def chooseCommand(
|
||||
gs: GameState,
|
||||
provinceId: ProvinceId,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
// Convert to proto for CommandChoiceHelpers which expects proto GameState
|
||||
val gsProto = GameStateConverter.toProto(gs)
|
||||
|
||||
commandsForProvince(provinceId).map { oneProvinceAvailableCommands =>
|
||||
CommandChooser.choose(
|
||||
gameStateProto.provinces(provinceId).getRulingFactionId,
|
||||
gameStateProto,
|
||||
gs.provinces(provinceId).rulingFactionId.get,
|
||||
gsProto,
|
||||
oneProvinceAvailableCommands.commands.toVector,
|
||||
Vector[CommandChooser](
|
||||
resolveTributeSelectedCommand,
|
||||
@@ -148,14 +164,14 @@ case class PerformVassalCommandsPhaseAction(
|
||||
handleRiotSelectedCommand,
|
||||
(
|
||||
fid: FactionId,
|
||||
gs: GameStateProto,
|
||||
gsP: GameStateProto,
|
||||
acs: Vector[AvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
maybeRestCommand(
|
||||
fid,
|
||||
gs,
|
||||
gsP,
|
||||
acs,
|
||||
provinceId,
|
||||
"chosen vassal command: rest"
|
||||
@@ -164,12 +180,13 @@ case class PerformVassalCommandsPhaseAction(
|
||||
),
|
||||
(
|
||||
fid: FactionId,
|
||||
gs: GameStateProto,
|
||||
gsP: GameStateProto,
|
||||
acs: Vector[AvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) => selectedCommandFromOrders(fid, gs, acs, fr, provinceId)
|
||||
) => selectedCommandFromOrders(fid, gsP, acs, fr, provinceId)
|
||||
),
|
||||
functionalRandom
|
||||
)
|
||||
}.get
|
||||
}
|
||||
}
|
||||
|
||||
+76
-60
@@ -2,78 +2,94 @@ 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.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.library.actions.impl.command.CommandFactory
|
||||
import net.eagle0.eagle.library.actions.impl.common.RandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
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.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: CommandFactory
|
||||
) extends RandomSequentialResultsAction(GameStateConverter.toProto(gameState)) {
|
||||
private val gameStateProto: GameStateProto = GameStateConverter.toProto(gameState)
|
||||
commandFactory: TCommandFactory,
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
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))
|
||||
.toVector
|
||||
|
||||
override def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultProtoApplier: ActionResultProtoApplier
|
||||
): RandomState[Vector[ActionResultProto]] =
|
||||
functionalRandom
|
||||
.nextFlatMap(
|
||||
gameStateProto.provinces.values
|
||||
.filter(_.rulingFactionId.isDefined)
|
||||
.filterNot(LegacyProvinceUtils.ruledByFactionLeader(_, gameStateProto))
|
||||
.toVector
|
||||
) {
|
||||
case (p, fr) =>
|
||||
chooseCommand(p.id, fr).map(_.toVector)
|
||||
}
|
||||
.map {
|
||||
_.map(cs =>
|
||||
commandFactory
|
||||
.makeCommand(
|
||||
actingFactionId = cs.actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommand = cs.available,
|
||||
selectedCommand = cs.selected
|
||||
)
|
||||
)
|
||||
}
|
||||
.map(
|
||||
_.flatMap(_.execute(actionResultProtoApplier))
|
||||
.map(_.actionResult)
|
||||
)
|
||||
|
||||
def chooseCommand(
|
||||
provinceId: ProvinceId,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
commandsForProvince(provinceId).map { oneProvinceAvailableCommands =>
|
||||
val commandOptions = oneProvinceAvailableCommands.commands
|
||||
|
||||
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
|
||||
)
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
vassalProvinces(gameState)
|
||||
.foldLeft(
|
||||
RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultApplier,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.getOrElse(sequencer)
|
||||
}
|
||||
.actionResults
|
||||
|
||||
private def chooseCommand(
|
||||
gs: GameState,
|
||||
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)
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
.getOrElse(RandomState(None, functionalRandom))
|
||||
}
|
||||
}
|
||||
|
||||
+35
-41
@@ -3,18 +3,15 @@ package net.eagle0.eagle.library.actions.impl.action
|
||||
import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.common.action_result_notification_details.{Notification, PrisonerExchangeDetails}
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.{END_PRISONER_EXCHANGE_PHASE, PRISONERS_EXCHANGED}
|
||||
import net.eagle0.eagle.common.round_phase.NewRoundPhase
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase.PROVINCE_EVENTS
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.UNAFFILIATED_HERO_PRISONER
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.changed_hero.ChangedHero
|
||||
import net.eagle0.eagle.internal.changed_province.ChangedProvince
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.DeterministicSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
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, ChangedHeroC, NotificationC}
|
||||
import net.eagle0.eagle.model.action_result.types.{EndPrisonerExchangePhaseResultType, PrisonersExchangedResultType}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroType
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
|
||||
object PrisonerExchangeAction {
|
||||
case class ExchangeableHeroInfo(
|
||||
@@ -37,11 +34,11 @@ object PrisonerExchangeAction {
|
||||
p <- gameState.provinces.values.toVector
|
||||
imprisoningFaction <- p.rulingFactionId.toVector
|
||||
uh <- p.unaffiliatedHeroes
|
||||
if uh.`type` == UNAFFILIATED_HERO_PRISONER
|
||||
if uh.unaffiliatedHeroType == UnaffiliatedHeroType.Prisoner
|
||||
hid = uh.heroId
|
||||
prisonerFaction <- uh.lastFaction
|
||||
prisonerFaction <- uh.lastFactionId
|
||||
faction <- gameState.factions.get(prisonerFaction)
|
||||
if faction.leaders.contains(hid)
|
||||
if faction.leaderIds.contains(hid)
|
||||
isFactionHead = faction.factionHeadId == hid
|
||||
} yield ExchangeableHeroInfo(
|
||||
pid = p.id,
|
||||
@@ -82,67 +79,64 @@ object PrisonerExchangeAction {
|
||||
|
||||
def exchangeResults(
|
||||
exchangeMatches: Vector[ExchangeMatch]
|
||||
): Vector[ActionResult] =
|
||||
): Vector[ActionResultT] =
|
||||
exchangeMatches.map {
|
||||
case ExchangeMatch(ehi1, ehi2) =>
|
||||
internalRequire(
|
||||
ehi1.prisonerFactionId == ehi2.imprisoningFactionId && ehi1.imprisoningFactionId == ehi2.prisonerFactionId,
|
||||
s"Mismatched factions in prisoner exchange"
|
||||
)
|
||||
ActionResult(
|
||||
`type` = PRISONERS_EXCHANGED,
|
||||
ActionResultC(
|
||||
actionResultType = PrisonersExchangedResultType,
|
||||
changedHeroes = Vector(
|
||||
ChangedHero(
|
||||
id = ehi1.hid,
|
||||
ChangedHeroC(
|
||||
heroId = ehi1.hid,
|
||||
newFactionId = Some(ehi1.prisonerFactionId)
|
||||
),
|
||||
ChangedHero(
|
||||
id = ehi2.hid,
|
||||
ChangedHeroC(
|
||||
heroId = ehi2.hid,
|
||||
newFactionId = Some(ehi2.prisonerFactionId)
|
||||
)
|
||||
),
|
||||
changedProvinces = Vector(
|
||||
ChangedProvince(
|
||||
id = ehi1.pid,
|
||||
ChangedProvinceC(
|
||||
provinceId = ehi1.pid,
|
||||
removedUnaffiliatedHeroIds = Vector(ehi1.hid),
|
||||
addedRulingPlayerHeroIds = Vector(ehi2.hid)
|
||||
newRulingFactionHeroIds = Vector(ehi2.hid)
|
||||
),
|
||||
ChangedProvince(
|
||||
id = ehi2.pid,
|
||||
ChangedProvinceC(
|
||||
provinceId = ehi2.pid,
|
||||
removedUnaffiliatedHeroIds = Vector(ehi2.hid),
|
||||
addedRulingPlayerHeroIds = Vector(ehi1.hid)
|
||||
newRulingFactionHeroIds = Vector(ehi1.hid)
|
||||
)
|
||||
),
|
||||
notificationsToDeliver = Vector(
|
||||
Notification(
|
||||
details = PrisonerExchangeDetails(
|
||||
newNotifications = Vector(
|
||||
NotificationC(
|
||||
details = NotificationDetails.PrisonerExchange(
|
||||
hero1Id = ehi1.hid,
|
||||
hero1FactionId = ehi1.prisonerFactionId,
|
||||
hero1ProvinceId = ehi1.pid,
|
||||
hero2Id = ehi2.hid,
|
||||
hero2FactionId = ehi2.prisonerFactionId,
|
||||
hero2ProvinceId = ehi2.pid
|
||||
)
|
||||
),
|
||||
deferred = false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val endPhaseAr: ActionResult = ActionResult(
|
||||
`type` = END_PRISONER_EXCHANGE_PHASE,
|
||||
newRoundPhase = Some(NewRoundPhase(PROVINCE_EVENTS))
|
||||
val endPhaseAr: ActionResultT = ActionResultC(
|
||||
actionResultType = EndPrisonerExchangePhaseResultType,
|
||||
newRoundPhase = Some(RoundPhase.ProvinceEvents)
|
||||
)
|
||||
}
|
||||
|
||||
case class PrisonerExchangeAction(startingState: GameState)
|
||||
extends DeterministicSequentialResultsAction(startingState) {
|
||||
|
||||
override def results(
|
||||
actionResultProtoApplier: ActionResultProtoApplier
|
||||
): Vector[ActionResult] =
|
||||
case class PrisonerExchangeAction(gameState: GameState) extends ProtolessSequentialResultsAction {
|
||||
override def results: Vector[ActionResultT] =
|
||||
PrisonerExchangeAction.exchangeResults(
|
||||
PrisonerExchangeAction.exchangeMatches(
|
||||
PrisonerExchangeAction.sortedExchangeableLeaders(startingState)
|
||||
PrisonerExchangeAction.sortedExchangeableLeaders(gameState)
|
||||
)
|
||||
) :+
|
||||
PrisonerExchangeAction.endPhaseAr
|
||||
|
||||
+47
-63
@@ -2,99 +2,83 @@ package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import scala.util.hashing.MurmurHash3
|
||||
|
||||
import net.eagle0.common.victory_condition.VictoryCondition
|
||||
import net.eagle0.eagle.{GameId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.START_BATTLE
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionType
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.army.HostileArmyGroup
|
||||
import net.eagle0.eagle.internal.changed_province.ChangedProvince
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.shardok_battle.{ShardokBattle, ShardokPlayer}
|
||||
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle.BattleType
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.DeterministicSequentialResultsAction
|
||||
import net.eagle0.eagle.library.util.LegacyBattalionUtils
|
||||
import net.eagle0.eagle.shardok_interface.EagleUnit
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
|
||||
import net.eagle0.eagle.library.util.BattalionUtils
|
||||
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.StartBattleResultType
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.{BattalionType, HostileArmyGroup, HostileArmyGroupStatus}
|
||||
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}
|
||||
|
||||
case class RequestFreeForAllBattlesAction(startingGameState: GameState)
|
||||
extends DeterministicSequentialResultsAction(startingGameState) {
|
||||
val ExpandUnit: CombatUnit => EagleUnit =
|
||||
EagleUnit.ExpandUnit(_, startingGameState)
|
||||
case class RequestFreeForAllBattlesAction(gameState: GameState) extends ProtolessSequentialResultsAction {
|
||||
|
||||
override def results(
|
||||
actionResultProtoApplier: ActionResultProtoApplier
|
||||
): Vector[ActionResult] =
|
||||
startingGameState.provinces.values
|
||||
override def results: Vector[ActionResultT] =
|
||||
gameState.provinces.values
|
||||
.filter(
|
||||
_.hostileArmies.exists(
|
||||
_.status.asMessage.sealedValue.isAwaitingFreeForAll
|
||||
_.status == HostileArmyGroupStatus.AwaitingFreeForAll
|
||||
)
|
||||
)
|
||||
.zipWithIndex
|
||||
.map {
|
||||
case (province, index) =>
|
||||
CommenceImminentBattle(
|
||||
eagleGameId = startingGameState.gameId,
|
||||
battleIndex = startingGameState.battleCounter + index + 1,
|
||||
roundId = startingGameState.currentRoundId,
|
||||
battleProvince = startingGameState.provinces(province.id),
|
||||
commenceImminentBattle(
|
||||
eagleGameId = gameState.gameId,
|
||||
battleIndex = gameState.battleCounter + index + 1,
|
||||
roundId = gameState.currentRoundId,
|
||||
battleProvince = gameState.provinces(province.id),
|
||||
armies = province.hostileArmies
|
||||
.filter(_.status.asMessage.sealedValue.isAwaitingFreeForAll)
|
||||
.toVector,
|
||||
battalionTypes = startingGameState.battalionTypes.toVector
|
||||
.filter(_.status == HostileArmyGroupStatus.AwaitingFreeForAll)
|
||||
)
|
||||
}
|
||||
.toVector
|
||||
|
||||
private def CommenceImminentBattle(
|
||||
private def commenceImminentBattle(
|
||||
eagleGameId: GameId,
|
||||
battleIndex: Int,
|
||||
roundId: RoundId,
|
||||
battleProvince: Province,
|
||||
armies: Vector[HostileArmyGroup],
|
||||
battalionTypes: Vector[BattalionType]
|
||||
): ActionResult =
|
||||
ActionResult(
|
||||
`type` = START_BATTLE,
|
||||
battleProvince: ProvinceT,
|
||||
armies: Vector[HostileArmyGroup]
|
||||
): ActionResultT =
|
||||
ActionResultC(
|
||||
actionResultType = StartBattleResultType,
|
||||
changedProvinces = Vector(
|
||||
ChangedProvince(
|
||||
id = battleProvince.id,
|
||||
removedHostileArmyFactionIds = armies.map(_.factionId).toVector,
|
||||
foodDelta = Some(-defenderFood(battleProvince, battalionTypes))
|
||||
ChangedProvinceC(
|
||||
provinceId = battleProvince.id,
|
||||
removedHostileArmyFactionIds = armies.map(_.factionId),
|
||||
foodDelta = Some(-defenderFood(battleProvince))
|
||||
)
|
||||
),
|
||||
province = Some(battleProvince.id),
|
||||
provinceId = Some(battleProvince.id),
|
||||
newBattle = Some(
|
||||
ShardokBattle(
|
||||
shardokGameId =
|
||||
s"${eagleGameId.toHexString}_${battleIndex.toHexString}_${battleHash(battleProvince.id, armies, roundId).toHexString}",
|
||||
battleIndex = battleIndex,
|
||||
battleType = BattleType.BATTLE_TYPE_FREE_FOR_ALL,
|
||||
players = armies.map(shardokPlayer).toVector,
|
||||
hexMapName = battleProvince.hexMapName, // FIXME: this should be fought on a "neutral" map"
|
||||
battleType = BattleType.FreeForAll,
|
||||
players = armies.map(shardokPlayer),
|
||||
hexMapName = battleProvince.hexMapName, // FIXME: this should be fought on a "neutral" map
|
||||
eagleGameId = eagleGameId,
|
||||
defenderProvince = battleProvince.id,
|
||||
roundStarted = startingGameState.currentRoundId
|
||||
roundStarted = gameState.currentRoundId
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def defenderFood(
|
||||
province: Province,
|
||||
battalionTypes: Vector[BattalionType]
|
||||
): Int =
|
||||
private def defenderFood(province: ProvinceT): Int =
|
||||
Math.min(
|
||||
LegacyBattalionUtils.monthlyConsumedFood(
|
||||
BattalionUtils.monthlyConsumedFood(
|
||||
province.defendingArmy
|
||||
.map(_.units)
|
||||
.toVector
|
||||
.flatten
|
||||
.flatMap(_.battalionId)
|
||||
.map(startingGameState.battalions),
|
||||
battalionTypes
|
||||
.map(gameState.battalions),
|
||||
gameState.battalionTypes.toVector
|
||||
),
|
||||
province.food
|
||||
)
|
||||
@@ -105,7 +89,7 @@ case class RequestFreeForAllBattlesAction(startingGameState: GameState)
|
||||
roundId: RoundId
|
||||
): Int =
|
||||
MurmurHash3.arrayHash(
|
||||
armies.map(_.toProtoString).toArray ++
|
||||
armies.map(a => s"${a.factionId}_${a.armies.map(_.id).mkString(",")}").toArray ++
|
||||
Array(
|
||||
battleProvinceId.toString,
|
||||
roundId.toString
|
||||
@@ -118,18 +102,18 @@ case class RequestFreeForAllBattlesAction(startingGameState: GameState)
|
||||
isDefender = false,
|
||||
armyGroup = Some(armyGroup),
|
||||
food = attackerFood(armyGroup),
|
||||
victoryConditions = Vector(VictoryCondition.VICTORY_CONDITION_LAST_PLAYER_STANDING)
|
||||
victoryConditions = Vector(VictoryCondition.LastPlayerStanding)
|
||||
)
|
||||
|
||||
def attackerFood(armyGroup: HostileArmyGroup): Int =
|
||||
private def attackerFood(armyGroup: HostileArmyGroup): Int =
|
||||
Math.min(
|
||||
LegacyBattalionUtils.monthlyConsumedFood(
|
||||
BattalionUtils.monthlyConsumedFood(
|
||||
battalions = armyGroup.armies
|
||||
.flatMap(_.getArmy.units)
|
||||
.flatMap(_.army.units)
|
||||
.flatMap(_.battalionId)
|
||||
.map(startingGameState.battalions),
|
||||
bts = startingGameState.battalionTypes.toVector
|
||||
.map(gameState.battalions),
|
||||
bts = gameState.battalionTypes.toVector
|
||||
),
|
||||
armyGroup.armies.map(_.supplies.map(_.food).getOrElse(0)).sum
|
||||
armyGroup.armies.map(_.supplies.food).sum
|
||||
)
|
||||
}
|
||||
|
||||
+23
-22
@@ -2,23 +2,25 @@ package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.internal.province.Province as ProvinceProto
|
||||
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.LegacyFactionUtils
|
||||
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.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.game_state.GameStateConverter
|
||||
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) extends TRandomSequentialResultsAction(gameState) {
|
||||
private val startingState: GameStateProto = GameStateConverter.toProto(gameState)
|
||||
case class TruceTurnBackPhaseAction(
|
||||
gameState: GameState,
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
private val factions: Vector[FactionT] = gameState.factions.values.toVector
|
||||
|
||||
private def checkOneAttackingArmyGroup(
|
||||
attackingFactionId: FactionId,
|
||||
@@ -26,10 +28,10 @@ case class TruceTurnBackPhaseAction(gameState: GameState) extends TRandomSequent
|
||||
provinceId: ProvinceId
|
||||
): Option[ActionResultT] =
|
||||
Option.when(
|
||||
LegacyFactionUtils.hasTruceOrAlliance(
|
||||
FactionUtils.hasTruceOrAlliance(
|
||||
defendingFid,
|
||||
attackingFactionId,
|
||||
startingState
|
||||
factions
|
||||
)
|
||||
) {
|
||||
ActionResultC(
|
||||
@@ -48,7 +50,7 @@ case class TruceTurnBackPhaseAction(gameState: GameState) extends TRandomSequent
|
||||
)
|
||||
}
|
||||
|
||||
private def checkOneProvince(province: ProvinceProto): Vector[ActionResultT] =
|
||||
private def checkOneProvince(province: ProvinceT): Vector[ActionResultT] =
|
||||
province.rulingFactionId.map { defendingFid =>
|
||||
province.hostileArmies.flatMap { attackingArmy =>
|
||||
checkOneAttackingArmyGroup(
|
||||
@@ -56,27 +58,26 @@ case class TruceTurnBackPhaseAction(gameState: GameState) extends TRandomSequent
|
||||
defendingFid = defendingFid,
|
||||
provinceId = province.id
|
||||
)
|
||||
}.toVector
|
||||
}
|
||||
}
|
||||
.getOrElse(Vector())
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom,
|
||||
actionResultTApplier: ActionResultTApplier
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
RandomStateTSequencer(
|
||||
RandomStateSequencer(
|
||||
initialState = gameState,
|
||||
actionResultApplier = actionResultTApplier,
|
||||
actionResultApplier = actionResultApplier,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.withActionResults(_ => startingState.provinces.values.toVector.flatMap(checkOneProvince))
|
||||
.withProtolessSequentialResultsAction(gs =>
|
||||
.withActionResults(_ => gameState.provinces.values.toVector.flatMap(checkOneProvince))
|
||||
.withActionResults(gs =>
|
||||
WithdrawnArmiesReturnHomeAction(
|
||||
gs.currentRoundId,
|
||||
gs.provinces.values.map(ProvinceConverter.fromProto).toVector
|
||||
)
|
||||
gs.provinces.values.toVector
|
||||
).results
|
||||
)
|
||||
.withActionResultT(_ =>
|
||||
.withActionResult(_ =>
|
||||
ActionResultC(
|
||||
actionResultType = EndTruceTurnBackPhaseResultType,
|
||||
newRoundPhase = Some(RoundPhase.BattleRequest)
|
||||
|
||||
+39
-32
@@ -11,7 +11,9 @@ import net.eagle0.eagle.internal.changed_province.ChangedProvince
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
import net.eagle0.eagle.library.settings.FreeHeroMoveVigorCost
|
||||
import net.eagle0.eagle.library.util.unaffiliated_hero.LegacyUnaffiliatedHeroUtils
|
||||
import net.eagle0.eagle.library.util.unaffiliated_hero.UnaffiliatedHeroUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
|
||||
import net.eagle0.eagle.ProvinceId
|
||||
|
||||
case class UnaffiliatedHeroMovedAction(
|
||||
@@ -40,38 +42,43 @@ case class UnaffiliatedHeroMovedAction(
|
||||
def immediateExecute(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[ActionResult] =
|
||||
LegacyUnaffiliatedHeroUtils
|
||||
.newRecruitmentInfo(
|
||||
gameState = gameState,
|
||||
provinceId = toProvinceId,
|
||||
unaffiliatedHero = uh,
|
||||
hero = gameState.heroes(uh.heroId),
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.map { nriRS =>
|
||||
ActionResult(
|
||||
`type` = HERO_MOVED,
|
||||
changedHeroes = Vector(
|
||||
ChangedHero(
|
||||
id = uh.heroId,
|
||||
vigor = Vigor.VigorDelta(-FreeHeroMoveVigorCost.doubleValue)
|
||||
)
|
||||
computeNewRecruitmentInfo(functionalRandom).map { newRecruitmentInfo =>
|
||||
ActionResult(
|
||||
`type` = HERO_MOVED,
|
||||
changedHeroes = Vector(
|
||||
ChangedHero(
|
||||
id = uh.heroId,
|
||||
vigor = Vigor.VigorDelta(-FreeHeroMoveVigorCost.doubleValue)
|
||||
)
|
||||
),
|
||||
changedProvinces = Vector(
|
||||
ChangedProvince(
|
||||
id = fromProvinceId,
|
||||
removedUnaffiliatedHeroIds = Vector(uh.heroId)
|
||||
),
|
||||
changedProvinces = Vector(
|
||||
ChangedProvince(
|
||||
id = fromProvinceId,
|
||||
removedUnaffiliatedHeroIds = Vector(uh.heroId)
|
||||
),
|
||||
ChangedProvince(
|
||||
id = toProvinceId,
|
||||
newUnaffiliatedHeroes = Vector(
|
||||
uh.update(
|
||||
_.recruitmentInfo := nriRS
|
||||
)
|
||||
ChangedProvince(
|
||||
id = toProvinceId,
|
||||
newUnaffiliatedHeroes = Vector(
|
||||
uh.update(
|
||||
_.recruitmentInfo := UnaffiliatedHeroConverter.recruitmentInfoToProto(newRecruitmentInfo)
|
||||
)
|
||||
)
|
||||
),
|
||||
notificationsToDeliver = notifications
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
notificationsToDeliver = notifications
|
||||
)
|
||||
}
|
||||
|
||||
private def computeNewRecruitmentInfo(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[net.eagle0.eagle.model.state.unaffiliated_hero.RecruitmentInfo] = {
|
||||
val scalaGameState = GameStateConverter.fromProto(gameState)
|
||||
UnaffiliatedHeroUtils.newRecruitmentInfo(
|
||||
gameState = scalaGameState,
|
||||
provinceId = toProvinceId,
|
||||
unaffiliatedHero = UnaffiliatedHeroConverter.fromProto(uh),
|
||||
hero = scalaGameState.heroes(uh.heroId),
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_library")
|
||||
|
||||
scala_library(
|
||||
name = "command_base",
|
||||
srcs = ["Command.scala"],
|
||||
name = "t_command_factory",
|
||||
srcs = ["TCommandFactory.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:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
|
||||
],
|
||||
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/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -29,14 +27,13 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
":command_base",
|
||||
":t_command_factory",
|
||||
],
|
||||
deps = [
|
||||
":alms_command",
|
||||
":apprehend_outlaw_command",
|
||||
":arm_troops_command",
|
||||
":attack_decision_command",
|
||||
":command_base",
|
||||
":control_weather_command",
|
||||
":decline_quest_command",
|
||||
":defend_command",
|
||||
@@ -56,9 +53,6 @@ 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",
|
||||
@@ -73,6 +67,7 @@ scala_library(
|
||||
":start_epidemic_command",
|
||||
":suppress_beasts_command",
|
||||
":swear_brotherhood_command",
|
||||
":t_command_factory",
|
||||
":trade_command",
|
||||
":train_command",
|
||||
":travel_command",
|
||||
@@ -84,7 +79,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_client_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//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/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:quest_fulfillment_utils",
|
||||
@@ -911,101 +908,6 @@ 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: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: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_impl",
|
||||
"//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/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: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: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"],
|
||||
@@ -1179,7 +1081,6 @@ 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/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:notification_concrete",
|
||||
@@ -1208,7 +1109,6 @@ 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/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:notification_concrete",
|
||||
@@ -1298,7 +1198,6 @@ 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/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",
|
||||
@@ -1452,7 +1351,6 @@ scala_library(
|
||||
"//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/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_faction_concrete",
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
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]
|
||||
}
|
||||
@@ -32,7 +32,8 @@ import net.eagle0.eagle.library.actions.impl.command.ManagePrisonersCommand.Pris
|
||||
import net.eagle0.eagle.library.actions.impl.common.{
|
||||
ProtolessRandomSimpleAction,
|
||||
ProtolessSequentialResultsAction,
|
||||
ProtolessSimpleAction
|
||||
ProtolessSimpleAction,
|
||||
TCommand
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
@@ -65,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 {
|
||||
class CommandFactory extends TCommandFactory {
|
||||
private def allProvinces(gameState: GameState): Vector[ProvinceT] =
|
||||
gameState.provinces.values.toVector
|
||||
|
||||
@@ -113,13 +114,34 @@ class CommandFactory {
|
||||
previousBackstoryTextIdLookup = hid => gameState.heroes(hid).backstoryTextId
|
||||
)
|
||||
|
||||
def makeCommand(
|
||||
/**
|
||||
* Creates a T-type command action directly, without proto wrapping.
|
||||
*
|
||||
* Use this when you need to execute commands within T-type action chains (e.g., in TRandomSequentialResultsAction
|
||||
* subclasses).
|
||||
*/
|
||||
def makeTCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
availableCommand: AvailableCommand,
|
||||
selectedCommand: SelectedCommand
|
||||
): Command =
|
||||
((availableCommand, selectedCommand) match {
|
||||
): TCommand =
|
||||
makeCommandInternal(actingFactionId, gameState, availableCommand, selectedCommand) match {
|
||||
case protolessSimpleAction: ProtolessSimpleAction =>
|
||||
TCommand.Simple(protolessSimpleAction)
|
||||
case protolessRandomSimpleAction: ProtolessRandomSimpleAction =>
|
||||
TCommand.RandomSimple(protolessRandomSimpleAction)
|
||||
case protolessSequentialResultsAction: ProtolessSequentialResultsAction =>
|
||||
TCommand.Sequential(protolessSequentialResultsAction)
|
||||
}
|
||||
|
||||
private def makeCommandInternal(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
availableCommand: AvailableCommand,
|
||||
selectedCommand: SelectedCommand
|
||||
): ProtolessSimpleAction | ProtolessRandomSimpleAction | ProtolessSequentialResultsAction =
|
||||
(availableCommand, selectedCommand) match {
|
||||
case (
|
||||
aoac: ApprehendOutlawAvailableCommand,
|
||||
aosc: ApprehendOutlawSelectedCommand
|
||||
@@ -843,26 +865,6 @@ class CommandFactory {
|
||||
throw new EagleInternalException(
|
||||
s"$x failed to match AvailableCommand / SelectedCommand pair: $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
|
||||
}
|
||||
|
||||
private def attackDecision(proto: AttackDecisionType): AttackDecision =
|
||||
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
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
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
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.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 = new ActionResultTApplierImpl(actionResultProtoApplier),
|
||||
functionalRandom = SeededRandom(startingState.randomSeed)
|
||||
).withActionResultTs(_ => arts).actionsWithResultingStates.newValue
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user