Compare commits

..
Author SHA1 Message Date
admin 0d8f05ef99 more cleanup 2025-09-17 07:34:15 -07:00
admin 846d0e85aa new ResolvedEagleUnit and BattalionStatus packages 2025-09-17 07:25:41 -07:00
88 changed files with 4229 additions and 3988 deletions
+10 -22
View File
@@ -9,16 +9,15 @@ This document analyzes all actions and commands in `src/main/scala/net/eagle0/ea
## Summary
Based on BUILD.bazel dependency analysis (2025-09-16, updated 2025-09-17):
Based on BUILD.bazel dependency analysis (2025-09-16):
- **Total Commands Analyzed:** 41
- **Commands Fully Migrated (No Protobuf):** 41 (100%) ✅
- **Commands Still Using Protobuf:** 0 (0%) ✅
- **Total Actions Analyzed:** 48
- **Actions Fully Migrated (No Protobuf):** 5 (10.4%)
- **Actions Partially Migrated:** 19 (39.6%)
- **Actions Fully Migrated (No Protobuf):** 4 (8.3%)
- **Actions Partially Migrated:** 20 (41.7%)
- **Actions Still Using Protobuf:** 24 (50%)
- **Base Classes:** 8 protoless variants available, 6 still use protobuf
- **Shared Components:** `ResolvedEagleUnit` migrated to use `Option[BattalionT]` for proper null handling
## Conversion Insights
@@ -94,7 +93,6 @@ These actions have been successfully migrated to use Scala models only:
| File | Base Class | Notes |
|------|------------|-------|
| HeroBackstoryUpdateAction.scala | ProtolessSequentialResultsAction | Processes hero backstory updates with LLM integration |
| ProvinceConqueredAction.scala | ProtolessSimpleAction | Uses component-based design (gameId, currentRoundId, currentDate, Scala models) |
| ProvinceHeldAction.scala | ProtolessSimpleAction | Uses specific components (gameId, currentRoundId, defendingProvince, etc.) instead of full GameState |
| UnaffiliatedHeroAppearedAction.scala | ProtolessSimpleAction | Handles unaffiliated hero appearance with name generation |
| WithdrawnArmiesReturnHomeAction.scala | ProtolessSequentialResultsAction | Manages army withdrawal and return mechanics |
@@ -117,7 +115,7 @@ These actions use protoless base classes but still have some protobuf dependenci
| FreeForAllDrawAction.scala | ProtolessSimpleAction | Depends on multiple protobuf targets |
| FriendlyMoveAction.scala | ProtolessSimpleAction | Depends on multiple protobuf targets |
| PerformUncontestedConquestAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
| ProvinceConqueredAction.scala | ProtolessSimpleAction | **CONVERTED** - Uses specific components (gameId, currentRoundId, currentDate, Scala models) |
| ProvinceConqueredAction.scala | ProtolessSimpleAction | Depends on `common_unit_scala_proto` |
| SafePassageArmiesProceedAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
| ShipmentArrivedAction.scala | ProtolessSimpleAction | Depends on multiple protobuf targets |
| TruceTurnBackPhaseAction.scala | ProtolessSequentialResultsAction | Depends on multiple protobuf targets |
@@ -248,30 +246,21 @@ Based on the BUILD.bazel dependency analysis, here are the key findings and reco
### 🎯 Action Migration Progress
**Migration Statistics:**
- 5/48 Actions fully migrated (10.4%)
- 4/48 Actions fully migrated (8.3%)
- 20/48 Actions using protoless base classes but with protobuf dependencies (41.7%)
- 24/48 Actions still fully on protobuf (50%)
**Successfully Migrated Actions:**
1. **HeroBackstoryUpdateAction** - LLM integration for hero backstories
2. **ProvinceConqueredAction** - Component-based design with prisoner handling and province conquest
3. **ProvinceHeldAction** - Component-based design pattern (gameId, currentRoundId, specific models)
4. **UnaffiliatedHeroAppearedAction** - Hero appearance with name generation
5. **WithdrawnArmiesReturnHomeAction** - Army withdrawal mechanics
**Recent Migration Updates (2025-09-17):**
- **ResolvedEagleUnit** - Changed `battalion: BattalionT` to `battalion: Option[BattalionT]`
- Properly handles units without battalions (battalion ID -1)
- Updated `ShardokInterfaceGrpcClient` to check for `defaultBattalionId` and use `None`
- Updated `ResolveBattleAction`, `ProvinceConqueredAction`, `RequestBattlesAction`
- All tests updated to handle optional battalions
2. **ProvinceHeldAction** - Component-based design pattern (gameId, currentRoundId, specific models)
3. **UnaffiliatedHeroAppearedAction** - Hero appearance with name generation
4. **WithdrawnArmiesReturnHomeAction** - Army withdrawal mechanics
**Key Migration Patterns:**
- ✅ Use specific components instead of full GameState (see ProvinceHeldAction, ProvinceConqueredAction)
- ✅ Use specific components instead of full GameState (see ProvinceHeldAction)
- ✅ Convert protobuf models to Scala models at Action boundaries
- ✅ Update BUILD.bazel to remove protobuf dependencies
- ✅ Update all call sites and tests
- ✅ Use `Option[T]` for optional fields instead of special sentinel values (e.g., battalion ID -1)
**Next Migration Candidates (Simple Actions with Protoless Base):**
1. **FreeForAllDrawAction** - Already uses ProtolessSimpleAction
@@ -301,5 +290,4 @@ Based on the BUILD.bazel dependency analysis, here are the key findings and reco
---
*Updated on 2025-09-17 - Analysis based on BUILD.bazel dependencies and code review*
*Latest update: ResolvedEagleUnit migrated to use Option[BattalionT] for proper battalion handling*
*Updated on 2025-09-16 - Analysis based on BUILD.bazel dependencies and code review*
@@ -48,7 +48,6 @@ namespace eagle {
private PlayerId _playerId;
private int _recommendedFood;
private int _recommendedGold;
private List<AvailableDestinationProvince> _sortedAvailableDestinations;
public override AvailableCommand.SealedValueOneofCase CommandType =>
AvailableCommand.SealedValueOneofCase.MarchCommand;
@@ -121,13 +120,9 @@ namespace eagle {
DestinationProvince is { RulingFactionId : not null } &&
DestinationProvince.RulingFactionId.Value != _playerId;
private bool RequiresRiverCrossing => SelectedDestinationProvinceIndex is {}
idx && _sortedAvailableDestinations != null && idx < _sortedAvailableDestinations.Count &&
_sortedAvailableDestinations[idx].RequiresRiverCrossing;
public override bool WarnOnCommitButton =>
WouldAbandonProvince || NotEnoughFood || MarchingOnTrucePartner ||
BadlyConstructedUnits || NoBattalionsIntoEnemy || RequiresRiverCrossing;
public override bool WarnOnCommitButton => WouldAbandonProvince || NotEnoughFood ||
MarchingOnTrucePartner ||
BadlyConstructedUnits || NoBattalionsIntoEnemy;
public override string CommitWarningText {
get {
if (!(WarnOnCommitButton)) return null;
@@ -152,9 +147,6 @@ namespace eagle {
txt += "Some of your heroes are leading battalion types that would restrict their abilities\n\n";
}
}
if (RequiresRiverCrossing) {
txt += $"Assaulting {DestinationProvince.Name} may require crossing a river\n\n";
}
return txt;
}
@@ -218,12 +210,9 @@ namespace eagle {
}
private void SetUpDestinationProvinceDropdown() {
_sortedAvailableDestinations =
SelectedOneProvinceCommand.AvailableDestinationProvinces
.OrderBy(adp => _model.Provinces[adp.ProvinceId].Name)
.ToList();
_sortedDestinationProvinceIds =
_sortedAvailableDestinations.Select(adp => adp.ProvinceId).ToList();
_sortedDestinationProvinceIds = SelectedOneProvinceCommand.AvailableDestinationProvinces
.OrderBy(pid => _model.Provinces[pid].Name)
.ToList();
var destinationProvinceNames = _sortedDestinationProvinceIds
.Select(id => GUIUtils.DarkColoredProvinceName(
_model.Provinces[id],
@@ -1,8 +1,6 @@
using System;
using Net.Eagle0.Eagle.Api;
using Net.Eagle0.Eagle.Views;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace eagle {
@@ -23,9 +21,6 @@ namespace eagle {
private TradeAvailableCommand TradeCommand => _availableCommand.TradeCommand;
private ProvinceId ActingProvinceId => TradeCommand.ActingProvinceId;
private ProvinceView ActingProvince => _model.Provinces[ActingProvinceId];
private int ProvinceGoldCap => ActingProvince.FullInfo.GoldCap;
private int ProvinceFoodCap => ActingProvince.FullInfo.FoodCap;
private int MaxFood =>
TradeCommand.FoodAvailable +
@@ -53,8 +48,7 @@ namespace eagle {
SetTextFields();
}
public override bool EnableCommitButton =>
!Mathf.Approximately(slider.value, TradeCommand.FoodAvailable);
public override bool EnableCommitButton => slider.value != TradeCommand.FoodAvailable;
public override string DisabledCommitButtonReason => "No change";
private bool IsBuy => FoodAmount > TradeCommand.FoodAvailable;
@@ -74,10 +68,7 @@ namespace eagle {
public void SetTextFields() {
goldText.text = GoldAmount.ToString();
goldText.color = GoldAmount > ProvinceGoldCap ? Color.red : Color.black;
foodText.text = FoodAmount.ToString();
foodText.color = FoodAmount > ProvinceFoodCap ? Color.red : Color.black;
description.text = IsBuy ? $"Buying {NetFood} food for {-NetGold} gold"
: $"Selling {-NetFood} food for {NetGold} gold";
@@ -244,11 +244,6 @@ message ManagePrisonersAvailableCommand {
repeated PrisonerToManage prisoners = 2;
}
message AvailableDestinationProvince {
int32 province_id = 1;
bool requires_river_crossing = 2;
}
message MarchCommandFromOneProvince {
map<int32, .net.eagle0.eagle.api.command.util.SuitableBattalions> suitable_battalions_for_heroes = 1;
@@ -259,7 +254,7 @@ message MarchCommandFromOneProvince {
int32 food_available = 6;
repeated AvailableDestinationProvince available_destination_provinces = 7;
repeated int32 available_destination_provinces = 7;
repeated int32 available_hero_ids = 8;
repeated .net.eagle0.eagle.api.command.util.BattalionWithFoodCost available_battalions = 10;
@@ -35,7 +35,9 @@ object FixLeaderAloneCommandSelector {
)
) { opmc => fr =>
opmc.availableDestinationProvinces
.map(dest => gameState.provinces(dest.provinceId))
.map(
gameState.provinces
)
.filter(_.rulingFactionId.contains(actingFactionId))
.filter(_.rulingFactionHeroIds.size == 1)
.filter(dest =>
@@ -27,7 +27,7 @@ object MarchTowardProvinceCommandChooser {
): Option[CommandSelection] = {
val bestCommand =
opmc.availableDestinationProvinces
.map(dest => (opmc, gs.provinces(dest.provinceId)))
.map(dest => (opmc, gs.provinces(dest)))
.flatMap { case (opmc, p) =>
ProvinceDistances
.distanceThroughFriendliesOption(
@@ -1063,7 +1063,7 @@ object MidGameAIClient {
.intersect(factionLeaders)
.maxBy(hid => LegacyHeroUtils.seniorityOrder(faction, hid))
val validDestinations = opmc.availableDestinationProvinces
.map(dest => gameState.provinces(dest.provinceId))
.map(gameState.provinces)
.filter(
_.rulingFactionId.contains(actingFactionId)
) // only send to a province we own
@@ -62,7 +62,7 @@ object MoveLeaderToBetterProvinceCommandChooser {
ProvinceDistances
.closestProvinceThroughFriendliesOption(
desiredPid,
mcfop.availableDestinationProvinces.map(_.provinceId).toVector,
mcfop.availableDestinationProvinces.toVector,
actingFactionId,
gs
)
@@ -48,7 +48,7 @@ object SeekMoreLeadersCommandChooser {
.get
val bestDestination =
opmc.availableDestinationProvinces
.map(dest => (opmc, gameState.provinces(dest.provinceId)))
.map(dest => (opmc, gameState.provinces(dest)))
.flatMap { case (opmc, p) =>
ProvinceDistances
.distanceThroughFriendliesOption(
@@ -68,19 +68,17 @@ scala_library(
"//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:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/battalion",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/shardok_battle:shardok_battle_converter",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
],
@@ -217,18 +215,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
)
@@ -29,12 +29,11 @@ import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
import net.eagle0.eagle.library.util.validations.RuntimeValidator
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.proto_converters.battalion.BattalionConverter
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.proto_converters.shardok_battle.ShardokBattleConverter
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
import net.eagle0.eagle.views.action_result_view.ActionResultView
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
@@ -232,25 +231,9 @@ case class EngineImpl(
results = actionResultProtoApplier.applyActionResults(
startingState = currentState,
results = ResolveBattleAction(
gameId = gameId,
currentRoundId = currentState.currentRoundId,
currentDate = DateConverter.fromProto(currentState.currentDate),
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,
outstandingBattles = currentState.outstandingBattles.toVector.map(
ShardokBattleConverter.fromProto
),
startingGameState = currentState,
resolvedBattles = Vector(battleResolution)
).results.map(_.asInstanceOf[ActionResult])
).execute(actionResultProtoApplier).map(_.actionResult)
)
)
@@ -17,12 +17,7 @@ import net.eagle0.eagle.library.util.EagleRequire.internalValidated
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
import net.eagle0.eagle.model.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.state.battalion.BattalionT
import scala.collection.mutable
@@ -197,27 +192,7 @@ object RoundPhaseAdvancer {
case UNCONTESTED_CONQUEST =>
actionResultProtoApplier.applyActionResults(
currentState,
PerformUncontestedConquestAction(
gameId = currentState.gameId,
currentRoundId = currentState.currentRoundId,
currentDate = DateConverter.fromProto(currentState.currentDate),
provinces = currentState.provinces.values
.map(ProvinceConverter.fromProto)
.map(p => p.id -> p)
.toMap,
factions = currentState.factions.values
.map(FactionConverter.fromProto)
.map(f => f.id -> f)
.toMap,
heroes = currentState.heroes.values
.map(HeroConverter.fromProto)
.map(h => h.id -> h)
.toMap,
battalions = currentState.battalions.values
.map(BattalionConverter.fromProto)
.map(b => (b.id, b))
.toMap
).results
PerformUncontestedConquestAction(currentState).results
.map(ActionResultProtoConverter.toProto)
)
@@ -265,33 +240,8 @@ object RoundPhaseAdvancer {
TruceTurnBackPhaseAction(currentState).execute(actionResultProtoApplier)
case BATTLE_REQUEST =>
val requestBattlesAction = RequestBattlesAction(
gameId = currentState.gameId,
currentRoundId = currentState.currentRoundId,
currentDate = DateConverter.fromProto(currentState.currentDate),
battleCounter = currentState.battleCounter,
heroes =
currentState.heroes.view.mapValues(HeroConverter.fromProto).toMap,
battalions = currentState.battalions.view
.mapValues(BattalionConverter.fromProto)
.toMap,
provinces = currentState.provinces.view
.mapValues(ProvinceConverter.fromProto)
.toMap,
factions = currentState.factions.view
.mapValues(FactionConverter.fromProto)
.toMap,
battalionTypes = currentState.battalionTypes.map { bt =>
val converted = BattalionTypeConverter.fromProto(bt)
converted.typeId -> converted
}.toMap
)
val requestResults = actionResultProtoApplier.applyActionResults(
currentState,
requestBattlesAction.results.map(
ActionResultProtoConverter.toProto(_)
)
)
val requestResults =
RequestBattlesAction(currentState).execute(actionResultProtoApplier)
requestResults ++ EndBattleRequestPhaseAction(
requestResults.lastOption.map(_.gameState).getOrElse(currentState)
).execute(actionResultProtoApplier)
@@ -1,7 +1,6 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.api.available_command.{
AvailableDestinationProvince,
MarchAvailableCommand,
MarchCommandFromOneProvince,
RecommendedBattleConfig
@@ -21,13 +20,11 @@ import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.library.util.{
BattalionSuitability,
LegacyBattalionUtils,
ShardokMapInfo
LegacyBattalionUtils
}
import net.eagle0.eagle.{FactionId, ProvinceId}
object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
private def oneOriginProvinceCommand(
gameState: GameState,
originProvince: Province
@@ -36,23 +33,7 @@ object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
.map(_.provinceId)
.flatMap(gameState.provinces.get)
.filterNot(LegacyProvinceUtils.hasBlizzard)
.map { destinationProvince =>
val startingPositionIndex = destinationProvince.neighbors
.find(n => n.provinceId == originProvince.id)
.get
.startingPositionIndex
val mapInfo = ShardokMapInfo.shardokMapInfos(destinationProvince.id)
val destinationCastleCount = mapInfo.castleCount
val requiresRiverCrossing = mapInfo.waterCrossingRequirementsByPosition(
startingPositionIndex
) >= 10
AvailableDestinationProvince(
provinceId = destinationProvince.id,
requiresRiverCrossing = requiresRiverCrossing
)
}
.map(_.id)
.toVector
val availableHeroes = originProvince.rulingFactionHeroIds
@@ -446,7 +446,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_march",
"//src/main/scala/net/eagle0/eagle/library/util:battalion_suitability",
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
"//src/main/scala/net/eagle0/eagle/library/util:shardok_map_info",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:combat_unit_selector",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:march_supplies_helpers",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
@@ -341,7 +341,6 @@ scala_library(
"//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/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",
@@ -357,8 +356,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types: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/battalion",
"//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",
@@ -725,7 +724,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/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",
"//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",
@@ -752,6 +751,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
],
deps = [
"//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/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
@@ -766,13 +767,9 @@ 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:free_for_all_draw_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/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
],
)
@@ -1066,13 +1063,17 @@ scala_library(
"//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_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/action:province_conquered_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
"//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/faction_utils:legacy_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",
@@ -1086,16 +1087,10 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_uncontested_conquest_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:sent_supplies_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: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/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
)
@@ -1428,6 +1423,10 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
],
deps = [
"//src/main/protobuf/net/eagle0/common:common_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_scala_proto",
"//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/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
@@ -1435,7 +1434,9 @@ scala_library(
"//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/settings:freed_prisoner_loyalty",
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero:legacy_unaffiliated_hero_utils",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
@@ -1450,23 +1451,18 @@ 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:province_conquered_result_type",
"//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/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:captured_hero",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:event_for_hero_backstory_trait",
"//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/province:orders",
"//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/unit_status",
"//src/main/scala/net/eagle0/eagle/shardok_interface:eagle_unit",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
],
)
@@ -1501,9 +1497,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:province_held_result_type",
"//src/main/scala/net/eagle0/eagle/model/state:captured_hero",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/province",
@@ -1520,37 +1513,32 @@ scala_library(
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_sequential_results_action",
],
deps = [
":province_conquered_action",
"//src/main/protobuf/net/eagle0/common:common_unit_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: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:protoless_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//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/action_result/types:sent_supplies_result_type",
"//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:battalion_type_id",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//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/shardok_battle",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
"//src/main/scala/net/eagle0/eagle/shardok_interface:eagle_unit",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
],
)
@@ -1591,20 +1579,30 @@ 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:deterministic_single_result_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
],
deps = [
":free_for_all_draw_action",
":province_conquered_action",
":province_held_action",
":won_free_for_all_action",
"//src/main/protobuf/net/eagle0/common:common_unit_scala_proto",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_scala_grpc",
"//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/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//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_single_result_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:battle_agriculture_devastation_delta",
"//src/main/scala/net/eagle0/eagle/library/settings:battle_economy_devastation_delta",
"//src/main/scala/net/eagle0/eagle/library/settings:battle_infrastructure_devastation_delta",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//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",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:legacy_battalion_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",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
@@ -1615,14 +1613,16 @@ 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_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:changed_province_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//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/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
"//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/shardok_interface:battle_resolution",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_shardok_player",
@@ -1897,6 +1897,7 @@ 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:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
@@ -1911,14 +1912,9 @@ 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:won_free_for_all_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/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
],
)
@@ -11,10 +11,7 @@ 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.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentChecker
import net.eagle0.eagle.library.util.ransom_validity.RansomValidity
@@ -26,11 +23,9 @@ 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.action_result.{ActionResultT, NotificationT}
import net.eagle0.eagle.model.proto_converters.NotificationConverter
import net.eagle0.eagle.model.proto_converters.battalion.BattalionConverter
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
@@ -61,100 +56,79 @@ case class EndDiplomacyResolutionPhaseAction(
actionResultTApplier: ActionResultTApplier
) extends ProtolessRandomSequentialResultsAction {
private lazy val provinceTs: Vector[ProvinceT] =
gameState.provinces.values.map(ProvinceConverter.fromProto).toVector
private lazy val factionTs: Vector[FactionT] =
gameState.factions.values.map(FactionConverter.fromProto).toVector
private lazy val heroTs: Vector[HeroT] =
gameState.heroes.values.map(HeroConverter.fromProto).toVector
private lazy val battalionTs: Vector[BattalionT] =
gameState.battalions.values.map(BattalionConverter.fromProto).toVector
private lazy val currentDateT: Date =
DateConverter.fromProto(gameState.currentDate)
private lazy val deferredNotificationTs: Vector[NotificationT] =
gameState.deferredNotifications
.map(n => NotificationConverter.fromProto(n, deferred = true))
.toVector
override def randomResults(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
import EndDiplomacyResolutionPhaseActionHelpers.*
RandomStateTSequencer(gameState, actionResultTApplier, functionalRandom)
.withActionResults(invalidationResultsForState)
.withActionResults(ransomResolutionsForState)
.withRandomActionResults(truceResolutionsForState)
.withRandomActionResults(allianceResolutionsForState)
.withRandomActionResults(breakAllianceResolutionsForState)
.withRandomActionResults(inviteResolutionsForState)
.withRandomActionResults { (currentGameState, fr) =>
RandomState(invalidationResults, functionalRandom)
.map(_ ++ ransomResolutions)
.continue((acc, fr) => truceResolutions(fr).map(acc ++ _))
.continue((acc, fr) => allianceResolutions(fr).map(acc ++ _))
.continue((acc, fr) => breakAllianceResolutions(fr).map(acc ++ _))
.continue((acc, fr) => inviteResolutions(fr).map(acc ++ _))
.continue((acc, fr) =>
CheckForFactionChangesAction(
gameId = currentGameState.gameId,
factions = currentGameState.factions.values.toVector.map(
FactionConverter.fromProto
),
provinces = currentGameState.provinces.values.toVector.map(
gameId = gameState.gameId,
factions =
gameState.factions.values.toVector.map(FactionConverter.fromProto),
provinces = gameState.provinces.values.toVector.map(
ProvinceConverter.fromProto
),
heroes = currentGameState.heroes.values.toVector.map(
HeroConverter.fromProto
),
killedHeroIds = currentGameState.killedHeroes.keys.toVector
).randomResults(fr)
}
.withActionResults { currentGameState =>
HeroBackstoryUpdateAction(
gameId = currentGameState.gameId,
roundId = currentGameState.currentRoundId,
heroes = currentGameState.heroes.values
.map(HeroConverter.fromProto)
.toVector,
visibleToFactionIds = toFid =>
FactionUtils.alliedFactions(
toFid,
currentGameState.factions.values
.map(FactionConverter.fromProto)
.toVector
),
heroInProvinceOwnedBy = heroId => {
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
provinces
heroes =
gameState.heroes.values.toVector.map(HeroConverter.fromProto),
killedHeroIds = gameState.killedHeroes.keys.toVector
).randomResults(fr).map(acc ++ _)
)
.map(
_ ++ HeroBackstoryUpdateAction(
gameId = gameState.gameId,
roundId = gameState.currentRoundId,
heroes = heroTs,
visibleToFactionIds =
toFid => FactionUtils.alliedFactions(toFid, factionTs),
heroInProvinceOwnedBy = heroId =>
provinceTs
.find(province =>
province.rulingFactionHeroIds.contains(heroId) ||
province.unaffiliatedHeroes.exists(_.heroId == heroId)
province.rulingFactionHeroIds.contains(
heroId
) || province.unaffiliatedHeroes.exists(_.heroId == heroId)
)
.flatMap(_.rulingFactionId)
}
).results
}
.withActionResult(endPhaseResultForState)
.actionResults
)
.map(_ :+ endPhaseResult)
}
}
/** Companion object for EndDiplomacyResolutionPhaseAction containing helper
* methods.
*
* IMPORTANT: This is intentionally named differently from the case class to
* avoid Scala naming conflicts that prevent the class from being imported in
* other packages. When a case class and its companion object share the same
* name, the 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.
*/
private object EndDiplomacyResolutionPhaseActionHelpers {
// Methods that use the updated game state from the sequencer
def invalidationResultsForState(
currentGameState: GameState
): Vector[ActionResultT] = {
val factions =
currentGameState.factions.values.map(FactionConverter.fromProto).toVector
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
// Check that unresolved offers ought to be invalidated and invalidate them
private def invalidationResults: Vector[ActionResultT] =
for {
faction <- factions
faction <- factionTs
diploOffer <- faction.incomingDiplomacyOffers.filter(
_.status == Unresolved
)
} yield {
diploOffer match {
case ro: RansomOffer
if !RansomValidity.isStillValid(diploOffer, provinces) =>
if !RansomValidity.isStillValid(diploOffer, provinceTs) =>
ActionResultC(
actionResultType = RansomInvalidatedResultType,
changedFactions = Vector(
@@ -173,178 +147,18 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
)
}
}
}
def endPhaseResultForState(
currentGameState: GameState
): ActionResultT = {
val deferredNotifications = currentGameState.deferredNotifications
.map(n => NotificationConverter.fromProto(n, deferred = true))
.toVector
ActionResultC(
actionResultType = EndDiplomacyResolutionPhaseResultType,
newRoundPhase = Some(ReconResolution),
removedNotifications = deferredNotifications,
newNotifications = deferredNotifications.map(_.withDeferred(false))
)
}
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]
)(factions: Vector[FactionT]): Vector[ActionResultT] =
(for {
faction <- factions
outgoingDiplo <- getter(faction)
} yield resolver(outgoingDiplo)).flatten
private def randomResolutionsForType[A](
getter: FactionT => Vector[A],
resolver: (A, FunctionalRandom) => RandomState[Vector[ActionResultT]],
functionalRandom: FunctionalRandom
)(factions: Vector[FactionT]): RandomState[Vector[ActionResultT]] =
functionalRandom.nextFlatMap(factions) { case (faction, fr) =>
fr.nextMap(getter(faction)) { case (outgoingDiplo, innerFr) =>
resolver(outgoingDiplo, innerFr)
}.map(_.flatten)
}
def truceResolutionsForState(
currentGameState: GameState,
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
val factions = factionTs(currentGameState)
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)
resultsForTruceOffer(
to,
fr,
provinces,
factions,
currentDate,
currentGameState
)
},
functionalRandom
)(factions)
}
def allianceResolutionsForState(
currentGameState: GameState,
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
val factions = factionTs(currentGameState)
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)
resultsForAllianceOffer(
ao,
fr,
provinces,
factions,
heroes,
currentDate
)
},
functionalRandom
)(factions)
}
def breakAllianceResolutionsForState(
currentGameState: GameState,
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
val factions = factionTs(currentGameState)
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)
resultsForBreakAlliance(ba, fr, provinces, factions, currentDate)
},
functionalRandom
)(factions)
}
def inviteResolutionsForState(
currentGameState: GameState,
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
val factions = factionTs(currentGameState)
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)
resultsForInvitation(
id,
fr,
provinces,
factions,
battalions,
currentDate
)
},
functionalRandom
)(factions)
}
def ransomResolutionsForState(
currentGameState: GameState
): Vector[ActionResultT] = {
val factions = factionTs(currentGameState)
resolutionsForType(
_.incomingDiplomacyOffers.collect { case ro: RansomOffer => ro },
(ro: RansomOffer) => {
val provinces = currentGameState.provinces.values
.map(ProvinceConverter.fromProto)
.toVector
resultsForRansomOffer(
ro,
DateConverter.fromProto(currentGameState.currentDate),
provinces,
currentGameState
)
}
)(factions)
}
private def resultsForRansomOffer(
ransomOffer: RansomOffer,
currentDate: Date,
provinces: Vector[ProvinceT],
gameState: GameState
): Vector[ActionResultT] = ransomOffer.status match {
ransomOffer: RansomOffer
): Vector[ActionResultT] = (ransomOffer.status match {
case Accepted =>
RansomResolutionHelpers.acceptedRansomResults(
ransomOffer,
currentDate,
allProvinces = provinces,
DateConverter.fromProto(gameState.currentDate),
allProvinces = provinceTs,
questFulfillmentChecker = new QuestFulfillmentChecker(
gameId = gameState.gameId,
currentDate = currentDate,
currentDate = currentDateT,
currentRoundId = gameState.currentRoundId,
previousBackstoryTextIdLookup =
hid => gameState.heroes(hid).backstoryVersions.toVector.last.textId
@@ -366,24 +180,20 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
Vector(
RansomResolutionHelpers.invalidatedRansomResult(ransomOffer)
)
}
})
private def resultsForTruceOffer(
truceOffer: TruceOffer,
functionalRandom: FunctionalRandom,
provinces: Vector[ProvinceT],
factions: Vector[FactionT],
currentDate: Date,
gameState: GameState
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] =
truceOffer.status match {
(truceOffer.status match {
case Accepted =>
TruceResolutionHelpers
.acceptedTruceResult(
truceOffer = truceOffer,
provinces = provinces,
factions = factions,
currentDate = currentDate,
provinces = provinceTs,
factions = factionTs,
currentDate = currentDateT,
functionalRandom = functionalRandom
)
.map(Vector(_))
@@ -391,8 +201,8 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
TruceResolutionHelpers
.rejectedTruceResults(
truceOffer = truceOffer,
provinces = provinces,
currentDate = currentDate,
provinces = provinceTs,
currentDate = currentDateT,
eagleGameId = gameState.gameId,
functionalRandom = functionalRandom
)
@@ -400,9 +210,9 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
RandomState(
TruceResolutionHelpers.imprisonedAmbassadorResults(
truceOffer = truceOffer,
provinces = provinces,
factions = factions,
currentDate = currentDate,
provinces = provinceTs,
factions = factionTs,
currentDate = currentDateT,
eagleGameId = gameState.gameId
),
functionalRandom
@@ -411,8 +221,8 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
TruceResolutionHelpers
.invalidatedTruceResults(
truceOffer = truceOffer,
provinces = provinces,
currentDate = currentDate,
provinces = provinceTs,
currentDate = currentDateT,
eagleGameId = gameState.gameId,
functionalRandom = functionalRandom
)
@@ -420,33 +230,29 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
throw new EagleInternalException(
"UNRESOLVED truce offer at the end of the round"
)
}
})
private def resultsForAllianceOffer(
allianceOffer: AllianceOffer,
functionalRandom: FunctionalRandom,
provinces: Vector[ProvinceT],
factions: Vector[FactionT],
heroes: Vector[HeroT],
currentDate: Date
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
allianceOffer.status match {
case Accepted =>
AllianceResolutionHelpers
.acceptedAllianceResult(
allianceOffer = allianceOffer,
currentDate = currentDate,
provinces = provinces,
heroes = heroes,
factions = factions,
currentDate = currentDateT,
provinces = provinceTs,
heroes = heroTs,
factions = factionTs,
functionalRandom = functionalRandom
)
case Rejected =>
AllianceResolutionHelpers
.rejectedAllianceResult(
allianceOffer = allianceOffer,
currentDate = currentDate,
provinces = provinces,
currentDate = currentDateT,
provinces = provinceTs,
functionalRandom = functionalRandom
)
.map(Vector(_))
@@ -455,9 +261,9 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
AllianceResolutionHelpers
.imprisonedAmbassadorResult(
allianceOffer = allianceOffer,
currentDate = currentDate,
provinces = provinces,
factions = factions
currentDate = currentDateT,
provinces = provinceTs,
factions = factionTs
),
functionalRandom
).map(Vector(_))
@@ -465,7 +271,7 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
AllianceResolutionHelpers
.invalidatedAllianceResults(
allianceOffer = allianceOffer,
provinces = provinces,
provinces = provinceTs,
functionalRandom = functionalRandom
)
.map(Vector(_))
@@ -478,19 +284,16 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
private def resultsForBreakAlliance(
breakAllianceOffer: BreakAlliance,
functionalRandom: FunctionalRandom,
provinces: Vector[ProvinceT],
factions: Vector[FactionT],
currentDate: Date
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = {
breakAllianceOffer.status match {
case Accepted =>
BreakAllianceResolutionHelpers
.acceptedResult(
breakAllianceOffer = breakAllianceOffer,
currentDate = currentDate,
provinces = provinces,
factions = factions,
currentDate = currentDateT,
provinces = provinceTs,
factions = factionTs,
functionalRandom = functionalRandom
)
.map(Vector(_))
@@ -507,9 +310,9 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
BreakAllianceResolutionHelpers
.imprisonedAmbassadorResult(
breakAllianceOffer = breakAllianceOffer,
currentDate = currentDate,
provinces = provinces,
factions = factions
currentDate = currentDateT,
provinces = provinceTs,
factions = factionTs
),
functionalRandom
).map(Vector(_))
@@ -521,7 +324,7 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
BreakAllianceResolutionHelpers
.invalidatedBreakAllianceResult(
offer = breakAllianceOffer,
provinces = provinces,
provinces = provinceTs,
functionalRandom = functionalRandom
)
.map(Vector(_))
@@ -530,27 +333,23 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
private def resultsForInvitation(
invitation: Invitation,
functionalRandom: FunctionalRandom,
provinces: Vector[ProvinceT],
factions: Vector[FactionT],
battalions: Vector[BattalionT],
currentDate: Date
): RandomState[Vector[ActionResultT]] = invitation.status match {
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] = (invitation.status match {
case Accepted =>
InvitationResolutionHelpers.acceptedInvitationResults(
acceptedInvitation = invitation,
currentDate = currentDate,
provinces = provinces,
factions = factions,
battalions = battalions,
currentDate = currentDateT,
provinces = provinceTs,
factions = factionTs,
battalions = battalionTs,
functionalRandom = functionalRandom
)
case Rejected =>
InvitationResolutionHelpers
.rejectedInvitationResult(
rejectedInvitation = invitation,
currentDate = currentDate,
provinces = provinces,
currentDate = currentDateT,
provinces = provinceTs,
functionalRandom = functionalRandom
)
.map(Vector(_))
@@ -559,9 +358,9 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
Vector(
InvitationResolutionHelpers.imprisonedAmbassadorResult(
invitation = invitation,
currentDate = currentDate,
provinces = provinces,
factions = factions
currentDate = currentDateT,
provinces = provinceTs,
factions = factionTs
)
),
functionalRandom
@@ -574,9 +373,80 @@ private object EndDiplomacyResolutionPhaseActionHelpers {
InvitationResolutionHelpers
.invalidatedInvitationResult(
invalidatedInvitation = invitation,
provinces = provinces,
provinces = provinceTs,
functionalRandom = functionalRandom
)
.map(Vector(_))
}
})
// Remove any outstanding truce/invite offers; we may have been unable to accept because
// of lack of provinces, for instance.
private def endPhaseResult: ActionResultT = ActionResultC(
actionResultType = EndDiplomacyResolutionPhaseResultType,
newRoundPhase = Some(ReconResolution),
removedNotifications = deferredNotificationTs,
newNotifications = deferredNotificationTs.map(_.withDeferred(false))
)
private def resolutionsForType[A](
getter: FactionT => Vector[A],
resolver: A => Vector[ActionResultT]
): Vector[ActionResultT] =
(for {
faction <- factionTs
outgoingDiplo <- getter(faction)
} yield resolver(outgoingDiplo)).flatten
private def randomResolutionsForType[A](
getter: FactionT => Vector[A],
resolver: (A, FunctionalRandom) => RandomState[Vector[ActionResultT]],
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] =
functionalRandom.nextFlatMap(factionTs) { case (faction, fr) =>
fr.nextMap(getter(faction)) { case (outgoingDiplo, innerFr) =>
resolver(outgoingDiplo, innerFr)
}.map(_.flatten)
}
private def truceResolutions(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] =
randomResolutionsForType(
_.incomingDiplomacyOffers.collect { case to: TruceOffer => to },
resultsForTruceOffer,
functionalRandom
)
private def allianceResolutions(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] =
randomResolutionsForType(
_.incomingDiplomacyOffers.collect { case ao: AllianceOffer => ao },
resultsForAllianceOffer,
functionalRandom
)
private def breakAllianceResolutions(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] =
randomResolutionsForType(
_.incomingDiplomacyOffers.collect { case ba: BreakAlliance => ba },
resultsForBreakAlliance,
functionalRandom
)
private def inviteResolutions(
functionalRandom: FunctionalRandom
): RandomState[Vector[ActionResultT]] =
randomResolutionsForType(
getter = _.incomingDiplomacyOffers.collect { case id: Invitation => id },
resolver = resultsForInvitation,
functionalRandom = functionalRandom
)
private def ransomResolutions: Vector[ActionResultT] =
resolutionsForType(
_.incomingDiplomacyOffers.collect { case ro: RansomOffer => ro },
resultsForRansomOffer
)
}
@@ -13,10 +13,8 @@ import net.eagle0.eagle.library.actions.impl.common.{
}
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.{
ActionResultProtoConverter,
BattalionConverter
}
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.proto_converters.battalion.BattalionConverter
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
@@ -1,22 +1,28 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.internal.army.MovingArmy as MovingArmyProto
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
import net.eagle0.eagle.model.action_result.ActionResultT
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.FreeForAllDrawResultType
import net.eagle0.eagle.model.proto_converters.ArmyConverter
import net.eagle0.eagle.model.state.MovingArmy
import net.eagle0.eagle.shardok_interface.ResolvedEagleUnit
import net.eagle0.eagle.ProvinceId
case class FreeForAllDrawAction(
defenderProvince: ProvinceId,
armiesFromPlayers: Vector[MovingArmy],
startingState: GameState,
battle: ShardokBattle,
remainingUnits: Seq[ResolvedEagleUnit]
) extends ProtolessSimpleAction {
private val changedProvincesFromRemainingUnits: Vector[ChangedProvinceC] = {
val mas = armiesFromPlayers.map(reversedMovingArmy)
val mas = for {
p <- battle.players
ma <- p.getArmyGroup.armies
} yield reversedMovingArmy(ma)
mas
.groupBy(_.destinationProvinceId)
@@ -32,21 +38,22 @@ case class FreeForAllDrawAction(
override def immediateExecute: ActionResultT = {
ActionResultC(
actionResultType = FreeForAllDrawResultType,
provinceId = Some(defenderProvince),
provinceId = Some(battle.defenderProvince),
changedProvinces = changedProvincesFromRemainingUnits
)
}
private def reversedMovingArmy(ma: MovingArmy): MovingArmy =
ma.copy(
originProvinceId = ma.destinationProvinceId,
destinationProvinceId = ma.destinationProvinceId,
arrivalRound = ma.arrivalRound + 1,
army = ma.army.copy(
units = ma.army.units.filter(cu =>
remainingUnits.exists(_.hero.id == cu.heroId)
),
fleeProvinceId = None
private def reversedMovingArmy(ma: MovingArmyProto): MovingArmy =
ArmyConverter.fromProto(
ma.update(
_.originProvince := ma.destinationProvince,
_.destinationProvince := ma.destinationProvince,
_.arrivalRound.modify(_ + 1),
_.army.units
.modify(
_.filter(cu => remainingUnits.exists(_.hero.id == cu.heroId))
),
_.army.optionalFleeProvinceId := None
)
)
}
@@ -1,8 +1,11 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.internal.army.HostileArmyGroup as HostileArmyGroupProto
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province as ProvinceProto
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
@@ -11,49 +14,31 @@ import net.eagle0.eagle.model.action_result.types.{
SentSuppliesResultType,
WithdrawalForTruceResultType
}
import net.eagle0.eagle.model.state.{HostileArmyGroup, RoundPhase}
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.date.Date as DateT
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.{
BattalionId,
FactionId,
GameId,
HeroId,
ProvinceId,
RoundId
}
import net.eagle0.eagle.model.proto_converters.ArmyConverter
import net.eagle0.eagle.model.state.RoundPhase
case class PerformUncontestedConquestAction(
gameId: GameId,
currentRoundId: RoundId,
currentDate: DateT,
provinces: Map[ProvinceId, ProvinceT],
factions: Map[FactionId, FactionT],
heroes: Map[HeroId, HeroT],
battalions: Map[BattalionId, BattalionT]
) extends ProtolessSequentialResultsAction {
case class PerformUncontestedConquestAction(gameState: GameState)
extends ProtolessSequentialResultsAction {
private val endPhaseAction: ActionResultT = ActionResultC(
actionResultType = EndUncontestedConquestPhaseResultType,
newRoundPhase = Some(RoundPhase.AttackDecision)
)
override def results: Vector[ActionResultT] =
provinces.values
override def results: Vector[ActionResultT] = {
gameState.provinces.values
.flatMap(conquestOrBounceResult)
.toVector :+ endPhaseAction
}
private def conquestOrBounceResult(
p: ProvinceT
): Vector[ActionResultT] =
p: ProvinceProto
): Vector[ActionResultT] = {
if p.rulingFactionId.nonEmpty then Vector.empty
else
p.hostileArmies match {
case items if items.isEmpty => Vector()
case Seq(hostileArmy: HostileArmyGroup) =>
case items if items.isEmpty => Vector()
case Seq(hostileArmy: HostileArmyGroupProto) =>
Vector(
ActionResultC(
actionResultType = SentSuppliesResultType,
@@ -62,118 +47,55 @@ case class PerformUncontestedConquestAction(
provinceId = p.id,
removedHostileArmyFactionIds = Vector(hostileArmy.factionId),
goldDelta = Some(
hostileArmy.armies.map(_.supplies.gold).sum
hostileArmy.armies.flatMap(_.supplies).map(_.gold).sum
),
foodDelta = Some(hostileArmy.armies.map(_.supplies.food).sum)
foodDelta =
Some(hostileArmy.armies.flatMap(_.supplies).map(_.food).sum)
)
)
),
ProvinceConqueredAction
.uncontestedConquest(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
conqueredProvince = p,
attackers = hostileArmy.armies
.flatMap(_.army.units)
.toVector,
factions = factions,
heroes = heroes,
battalions = battalions,
provinces = provinces
startingState = gameState,
conqueredProvince = gameState.provinces(p.id),
attackers =
hostileArmy.armies.flatMap(_.army).flatMap(_.units).toVector
)
.immediateExecute
)
case multipleArmies: Seq[HostileArmyGroup] =>
case multipleArmies: Seq[HostileArmyGroupProto] =>
val fids = multipleArmies.map(_.factionId).toVector
if areMutuallyAllied(fids) then
// Allies attacking together - treat as single army
Vector(
ActionResultC(
actionResultType = SentSuppliesResultType,
changedProvinces = Vector(
ChangedProvinceC(
provinceId = p.id,
removedHostileArmyFactionIds = fids,
goldDelta = Some(
multipleArmies.flatMap(_.armies.map(_.supplies.gold)).sum
),
foodDelta = Some(
multipleArmies.flatMap(_.armies.map(_.supplies.food)).sum
internalRequire(
LegacyFactionUtils.factionsAreMutuallyAllied(
fids = fids,
gs = gameState
),
s"Attacking armies $fids are not mutually allied"
)
Vector(
ActionResultC(
actionResultType = WithdrawalForTruceResultType,
changedProvinces = (ChangedProvinceC(
provinceId = p.id,
removedHostileArmyFactionIds = fids
) +: (for {
armyGroup <- multipleArmies
army <- armyGroup.armies
} yield ChangedProvinceC(
provinceId = army.originProvince,
newIncomingArmies = Vector(
ArmyConverter.fromProto(
army.update(
_.originProvince := p.id,
_.arrivalRound := gameState.currentRoundId + 1,
_.army.optionalFleeProvinceId := None
)
)
)
),
ProvinceConqueredAction
.uncontestedConquest(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
conqueredProvince = p,
attackers = multipleArmies
.flatMap(_.armies)
.flatMap(_.army.units)
.toVector,
factions = factions,
heroes = heroes,
battalions = battalions,
provinces = provinces
)
.immediateExecute
))).toVector
)
else if areMutuallyTruced(fids) then
// Truce factions attacking together - bounce them back
Vector(
ActionResultC(
actionResultType = WithdrawalForTruceResultType,
changedProvinces = (ChangedProvinceC(
provinceId = p.id,
removedHostileArmyFactionIds = fids
) +: (for {
armyGroup <- multipleArmies
army <- armyGroup.armies
} yield ChangedProvinceC(
provinceId = army.originProvinceId,
newIncomingArmies = Vector(
army.copy(
originProvinceId = p.id,
arrivalRound = currentRoundId + 1,
army = army.army.copy(fleeProvinceId = None)
)
)
))).toVector
)
)
else
// Hostile factions attacking together - not allowed
internalRequire(
false,
s"Attacking armies $fids are not mutually allied"
)
Vector.empty // This won't be reached due to internalRequire
end if
)
}
private def areMutuallyAllied(fids: Vector[FactionId]): Boolean =
fids.forall { fid1 =>
fids.forall { fid2 =>
fid1 == fid2 || FactionUtils.hasAlliance(
fid1,
fid2,
factions.values.toVector
)
}
}
private def areMutuallyTruced(fids: Vector[FactionId]): Boolean =
fids.forall { fid1 =>
fids.forall { fid2 =>
fid1 == fid2 || FactionUtils.hasTruce(
fid1,
fid2,
factions.values.toVector
)
}
}
}
}
@@ -1,17 +1,29 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.model.state.CombatUnit
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroType.{
Outlaw,
Prisoner
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.common.diplomacy_offer.{
DiplomacyOffer,
PrisonerToBeRansomed,
RansomOfferDetails
}
import net.eagle0.eagle.model.state.unaffiliated_hero.RecruitmentInfo
import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus
import net.eagle0.eagle.model.state.diplomacy_offer.RansomOffer
import net.eagle0.eagle.model.state.diplomacy_offer.status.Invalidated
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
UNAFFILIATED_HERO_OUTLAW,
UNAFFILIATED_HERO_PRISONER
}
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
import net.eagle0.eagle.library.actions.generated_text_request_generators.captured_hero_helpers.CapturedHeroPleaGenerator
import net.eagle0.eagle.library.settings.FreedPrisonerLoyalty
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
import net.eagle0.eagle.library.util.ProvinceDistances
import net.eagle0.eagle.library.util.hero.HeroUtils
import net.eagle0.eagle.library.util.unaffiliated_hero.LegacyUnaffiliatedHeroUtils
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.{
ActionResultC,
@@ -22,92 +34,62 @@ import net.eagle0.eagle.model.action_result.concrete.{
}
import net.eagle0.eagle.model.action_result.types.ProvinceConqueredResultType
import net.eagle0.eagle.model.action_result.{ActionResultT, NotificationDetails}
import net.eagle0.eagle.model.state.diplomacy_offer.RansomOffer
import net.eagle0.eagle.model.state.diplomacy_offer.status.Invalidated
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.date.Date as DateT
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.faction.FactionRelationship.RelationshipLevel
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.diplomacy_offer.DiplomacyOfferConverter
import net.eagle0.eagle.model.state.CapturedHero
import net.eagle0.eagle.model.state.diplomacy_offer.status.Invalidated
import net.eagle0.eagle.model.state.hero.ReleasedByProvinceCaptureBackstoryEvent
import net.eagle0.eagle.model.state.province.ProvinceOrderType.Entrust
import net.eagle0.eagle.shardok_interface.ResolvedEagleUnit
import net.eagle0.eagle.{
BattalionId,
ClientTextId,
FactionId,
GameId,
HeroId,
ProvinceId,
RoundId
ProvinceId
}
object ProvinceConqueredAction {
def uncontestedConquest(
gameId: GameId,
currentRoundId: RoundId,
currentDate: DateT,
conqueredProvince: ProvinceT,
attackers: Vector[CombatUnit],
factions: Map[FactionId, FactionT],
heroes: Map[HeroId, HeroT],
battalions: Map[BattalionId, BattalionT],
provinces: Map[ProvinceId, ProvinceT]
): ProvinceConqueredAction = {
val resolvedAttackers = attackers.map { unit =>
ResolvedEagleUnit(
hero = heroes(unit.heroId),
battalion = unit.battalionId.map(battalions),
status = UnitStatus.Normal
)
}
startingState: GameState,
conqueredProvince: Province,
attackers: Vector[CombatUnit]
): ProvinceConqueredAction =
ProvinceConqueredAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
startingState = startingState,
conqueredProvince = conqueredProvince,
notFledConquerors = resolvedAttackers,
notFledConquerors =
attackers.map(u => ResolvedEagleUnit(u, startingState)),
notFledDefenders = Vector.empty,
destroyedBattalions = Vector.empty,
defendingFactionId = None,
factions = factions,
heroes = heroes,
battalions = battalions,
provinces = provinces
defendingFactionId = None
)
}
}
// Changes the conquered province to belong to the conquering player, sets up the invading armies and battalions there,
// and imprisons any of the former ruling heroes.
case class ProvinceConqueredAction(
gameId: GameId,
currentRoundId: RoundId,
currentDate: DateT,
conqueredProvince: ProvinceT,
startingState: GameState,
conqueredProvince: Province,
notFledConquerors: Vector[ResolvedEagleUnit],
notFledDefenders: Vector[ResolvedEagleUnit],
destroyedBattalions: Vector[BattalionId],
defendingFactionId: Option[FactionId],
factions: Map[FactionId, FactionT],
heroes: Map[HeroId, HeroT],
battalions: Map[BattalionId, BattalionT],
provinces: Map[ProvinceId, ProvinceT]
defendingFactionId: Option[FactionId]
) extends ProtolessSimpleAction {
private def invalidatedRansomFactions: Vector[ChangedFactionC] = {
conqueredProvince.rulingFactionId
.map { rfid =>
val incomingRansomOffers = factions
.get(rfid)
.map(_.incomingDiplomacyOffers)
.getOrElse(Vector.empty)
.collect {
case offer: RansomOffer
if offer.prisonerToBeRansomed.provinceIdForPrisoner == conqueredProvince.id =>
offer
}
val incomingRansomOffers =
startingState
.factions(rfid)
.incomingDiplomacyOffers
.map(DiplomacyOfferConverter.fromProto)
.collect {
case ransomOffer: RansomOffer
if ransomOffer.prisonerToBeRansomed.provinceIdForPrisoner == conqueredProvince.id =>
ransomOffer
}
if incomingRansomOffers.nonEmpty then {
Vector(
@@ -128,15 +110,15 @@ case class ProvinceConqueredAction(
}
override def immediateExecute: ActionResultT = {
val attackingPlayerId = notFledConquerors.head.hero.factionId.get
val attackingPlayerId = notFledConquerors.head.hero.getFactionId
val seniorConquerorHeroId = notFledConquerors
.map(_.hero)
.min(HeroUtils.sortOrdering(factions(attackingPlayerId).leaderIds))
.id
.map(_.hero.id)
.sortWith(LegacyHeroUtils.sortOrderer(startingState))
.min
CapturedHeroPleaGenerator.withPleasForFirstNewCapturedHero(
gameId = gameId,
currentRoundId = currentRoundId,
gameId = startingState.gameId,
currentRoundId = startingState.currentRoundId,
capturingHeroId = seniorConquerorHeroId,
capturingFactionId = attackingPlayerId,
actionResult = afterUpdatingUnaffiliatedHeroes(
@@ -146,7 +128,7 @@ case class ProvinceConqueredAction(
actingFactionId = Some(attackingPlayerId),
provinceId = Some(conqueredProvince.id),
destroyedBattalionIds = destroyedBattalionIds.toVector,
changedFactions = factions.keys
changedFactions = startingState.factions.keys
.map(fid =>
ChangedFactionC(
factionId = fid,
@@ -156,7 +138,7 @@ case class ProvinceConqueredAction(
.toVector ++ invalidatedRansomFactions ++ Option
.when(
conqueredProvince.rulingFactionId
.flatMap(factions.get)
.map(startingState.factions)
.flatMap(_.focusProvinceId)
.contains(conqueredProvince.id)
) {
@@ -186,7 +168,8 @@ case class ProvinceConqueredAction(
CapturedHero(
heroId = hid,
recruitmentAttempted = false,
previousFactionId = heroes(hid).factionId.get
previousFactionId =
startingState.heroes(hid).factionId.get
)
)
.toVector,
@@ -203,7 +186,7 @@ case class ProvinceConqueredAction(
provinceId = conqueredProvince.id,
conqueringFactionId = attackingPlayerId,
otherAttackingFactionIds = notFledConquerors
.map(_.hero.factionId.get)
.map(_.hero.getFactionId)
.distinct
.filterNot(_ == attackingPlayerId)
.toVector,
@@ -234,7 +217,7 @@ case class ProvinceConqueredAction(
private def destroyedBattalionIds: Vector[BattalionId] =
notFledDefenders
.flatMap(_.optionalBattalionId)
.filter(battalions.contains)
.filter(startingState.battalions.contains)
private def afterFreeingAlliedPrisonersFromOneFaction(
actionResult: ActionResultT,
@@ -242,18 +225,18 @@ case class ProvinceConqueredAction(
): ActionResultT = {
val prisonersFromFaction =
conqueredProvince.unaffiliatedHeroes
.filter(_.unaffiliatedHeroType == Prisoner)
.filter(_.lastFactionId.contains(alliedFid))
.filter(_.`type` == UNAFFILIATED_HERO_PRISONER)
.filter(_.lastFaction.contains(alliedFid))
ProvinceDistances
.closestProvinceOption(
to = conqueredProvince.id,
candidates = provinces.values
.filter(_.rulingFactionId.contains(alliedFid))
candidates = LegacyFactionUtils
.provinces(alliedFid, startingState)
.map(_.id)
.toVector,
eligibleNeighbors =
pid => provinces(pid).neighbors.map(_.provinceId).toVector
eligibleNeighbors = pid =>
startingState.provinces(pid).neighbors.toVector.map(_.provinceId)
)
.map(_.provinceId)
.map { targetPid =>
@@ -281,7 +264,7 @@ case class ProvinceConqueredAction(
loyaltyChange = StatAbsolute(FreedPrisonerLoyalty.doubleValue),
newEventsForHeroBackstory = Vector(
ReleasedByProvinceCaptureBackstoryEvent(
date = currentDate,
date = DateConverter.fromProto(startingState.currentDate),
capturingFactionId = actionResult.actingFactionId.get,
releasedInProvinceId = conqueredProvince.id
)
@@ -298,7 +281,9 @@ case class ProvinceConqueredAction(
case (headCp: ChangedProvinceC) +: tail =>
headCp.withChangedUnaffiliatedHeroes(prisonersFromFaction.map {
uh =>
uh.copy(unaffiliatedHeroType = Outlaw)
UnaffiliatedHeroConverter.fromProto(
uh.withType(UNAFFILIATED_HERO_OUTLAW)
)
}.toVector) +: tail
case _ =>
throw new EagleInternalException(
@@ -314,7 +299,7 @@ case class ProvinceConqueredAction(
loyaltyChange = StatAbsolute(FreedPrisonerLoyalty.doubleValue),
newEventsForHeroBackstory = Vector(
ReleasedByProvinceCaptureBackstoryEvent(
date = currentDate,
date = DateConverter.fromProto(startingState.currentDate),
capturingFactionId = actionResult.actingFactionId.get,
releasedInProvinceId = conqueredProvince.id
)
@@ -328,10 +313,10 @@ case class ProvinceConqueredAction(
private def afterFreeingAllAlliedPrisoners(
actionResult: ActionResultT
): ActionResultT =
factions(actionResult.actingFactionId.get).factionRelationships
.filter(
_.relationshipLevel == RelationshipLevel.Ally
)
startingState
.factions(actionResult.actingFactionId.get)
.factionRelationships
.filter(_.relationshipLevel == FactionRelationship.RelationshipLevel.ALLY)
.map(_.targetFactionId)
.foldLeft(actionResult) { case (acc, alliedFid) =>
afterFreeingAlliedPrisonersFromOneFaction(acc, alliedFid)
@@ -342,8 +327,8 @@ case class ProvinceConqueredAction(
): ActionResultT = {
val afterFreeingPrisoners =
conqueredProvince.unaffiliatedHeroes
.filter(_.unaffiliatedHeroType == Prisoner)
.filter(_.lastFactionId.contains(actionResult.actingFactionId.get))
.filter(_.`type` == UNAFFILIATED_HERO_PRISONER)
.filter(_.lastFaction.contains(actionResult.actingFactionId.get))
afterFreeingAllAlliedPrisoners(
actionResult
@@ -371,7 +356,7 @@ case class ProvinceConqueredAction(
loyaltyChange = StatAbsolute(FreedPrisonerLoyalty.doubleValue),
newEventsForHeroBackstory = Vector(
ReleasedByProvinceCaptureBackstoryEvent(
date = currentDate,
date = DateConverter.fromProto(startingState.currentDate),
capturingFactionId = actionResult.actingFactionId.get,
releasedInProvinceId = conqueredProvince.id
)
@@ -395,9 +380,8 @@ case class ProvinceConqueredAction(
val changedUHs =
remainingUHs
.map(uh =>
uh.copy(recruitmentInfo = RecruitmentInfo.NotDivined)
) // Clear recruitment info
.map(uh => LegacyUnaffiliatedHeroUtils.withQuestCleared(uh))
.map(UnaffiliatedHeroConverter.fromProto)
actionResult.copy(
changedProvinces = headCp.copy(
@@ -10,6 +10,7 @@ import net.eagle0.eagle.model.action_result.concrete.{
import net.eagle0.eagle.model.action_result.types.ProvinceHeldResultType
import net.eagle0.eagle.model.action_result.{ActionResultT, NotificationDetails}
import net.eagle0.eagle.model.state.CapturedHero
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.{BattalionId, FactionId, GameId, HeroId, RoundId}
@@ -1,73 +1,57 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
import net.eagle0.eagle.library.util.BattalionUtils
import net.eagle0.common.victory_condition.VictoryCondition.{
VICTORY_CONDITION_HOLDS_CRITICAL_TILES,
VICTORY_CONDITION_LAST_PLAYER_STANDING,
VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS
}
import net.eagle0.eagle.common.action_result_type.ActionResultType.{
SENT_SUPPLIES,
START_BATTLE
}
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.army.{Army, HostileArmyGroup, 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.shardok_battle.ShardokBattle.BattleType
import net.eagle0.eagle.internal.shardok_battle.{ShardokBattle, ShardokPlayer}
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.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.state.HostileArmyGroupStatus.Attacking
import net.eagle0.eagle.model.state.{
Army,
BattalionType,
BattalionTypeId,
HostileArmyGroup,
MovingArmy,
Supplies
}
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.shardok_interface.ResolvedEagleUnit
import net.eagle0.eagle.{
BattalionId,
FactionId,
GameId,
HeroId,
ProvinceId,
RoundId
}
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.{
SentSuppliesResultType,
StartBattleResultType
}
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.shardok_battle.{
BattleType,
ShardokBattle,
ShardokPlayer,
VictoryCondition
}
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.shardok_interface.{EagleUnit, ResolvedEagleUnit}
import net.eagle0.eagle.{GameId, ProvinceId, RoundId}
import scala.util.hashing.MurmurHash3
case class RequestBattlesAction(
gameId: GameId,
currentRoundId: RoundId,
currentDate: net.eagle0.eagle.model.state.date.Date,
battleCounter: Int,
heroes: Map[HeroId, HeroT],
battalions: Map[BattalionId, BattalionT],
provinces: Map[ProvinceId, ProvinceT],
factions: Map[FactionId, FactionT],
battalionTypes: Map[BattalionTypeId, BattalionType]
) extends ProtolessSequentialResultsAction {
override def results: Vector[ActionResultT] =
provincesWithAttackingArmies.zipWithIndex
.flatMap { case (pwaas, idx) => handleOneProvince(pwaas, idx) }
case class RequestBattlesAction(startingGameState: GameState)
extends DeterministicSequentialResultsAction(startingGameState) {
val ExpandUnit: CombatUnit => EagleUnit =
EagleUnit.ExpandUnit(_, startingGameState)
private def handleOneProvince(
override def results(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionResult] =
provincesWithAttackingArmies.zipWithIndex
.flatMap { case (pwaas, idx) =>
handleOneProvince(startingGameState, pwaas, idx)
}
def handleOneProvince(
gameState: GameState,
provinceWithAttackingArmies: ProvinceWithAttackingArmies,
index: Int
): Vector[ActionResultT] = {
val defendingArmy = provinces(
provinceWithAttackingArmies.provinceId
).defendingArmy
): Vector[ActionResult] = {
val defendingArmy = gameState
.provinces(provinceWithAttackingArmies.provinceId)
.defendingArmy
internalRequire(
provinceWithAttackingArmies.armies
.forall(_.status == Attacking),
.forall(_.status.asMessage.sealedValue.isAttacking),
"all armies must be attacking in the battle request phase"
)
@@ -76,14 +60,15 @@ case class RequestBattlesAction(
if participantCount == 1 then {
// it's a single-army attack with no defending army
uncontestedAttackResults(provinceWithAttackingArmies)
uncontestedAttackResults(provinceWithAttackingArmies, gameState)
} else {
Vector(
CommenceImminentBattle(
eagleGameId = gameId,
battleIndex = battleCounter + index + 1,
roundId = currentRoundId,
battleProvince = provinces(provinceWithAttackingArmies.provinceId),
eagleGameId = gameState.gameId,
battleIndex = gameState.battleCounter + index + 1,
roundId = gameState.currentRoundId,
battleProvince =
gameState.provinces(provinceWithAttackingArmies.provinceId),
attackingArmies = provinceWithAttackingArmies.armies,
defendingArmy = defendingArmy
)
@@ -95,48 +80,48 @@ case class RequestBattlesAction(
eagleGameId: GameId,
battleIndex: Int,
roundId: RoundId,
battleProvince: ProvinceT,
battleProvince: Province,
attackingArmies: Vector[HostileArmyGroup],
defendingArmy: Option[Army]
): ActionResultT =
ActionResultC(
actionResultType = StartBattleResultType,
): ActionResult =
ActionResult(
`type` = START_BATTLE,
changedProvinces = Vector(
ChangedProvinceC(
provinceId = battleProvince.id,
removedHostileArmyFactionIds = attackingArmies.map(_.factionId),
ChangedProvince(
id = battleProvince.id,
removedHostileArmyFactionIds =
attackingArmies.map(_.factionId).toVector,
foodDelta = Some(-defenderFood(battleProvince))
)
),
provinceId = Some(battleProvince.id),
province = Some(battleProvince.id),
newBattle = Some(
ShardokBattle(
shardokGameId =
s"${eagleGameId.toHexString}_${battleIndex.toHexString}_${battleHash(battleProvince.id, attackingArmies, defendingArmy, roundId).toHexString}",
battleIndex = battleIndex,
battleType = BattleType.AssaultProvince,
battleType = BattleType.BATTLE_TYPE_ASSAULT_PROVINCE,
players = shardokPlayers(
attackingArmies,
defendingArmy,
defenderFood(battleProvince),
battleProvince
defenderFood(battleProvince)
),
hexMapName = battleProvince.hexMapName,
eagleGameId = eagleGameId,
defenderProvince = battleProvince.id,
roundStarted = currentRoundId
roundStarted = startingGameState.currentRoundId
)
)
)
def defenderFood(province: ProvinceT): Int = {
def defenderFood(province: Province): Int = {
Math.min(
BattalionUtils.monthlyConsumedFood(
LegacyBattalionUtils.monthlyConsumedFood(
battalions = province.defendingArmy.toVector
.flatMap(_.units)
.flatMap(_.battalionId)
.map(battalions),
bts = battalionTypes.values.toVector
.map(startingGameState.battalions),
bts = startingGameState.battalionTypes.toVector
),
province.food
)
@@ -149,28 +134,18 @@ case class RequestBattlesAction(
roundId: RoundId
): Int =
MurmurHash3.arrayHash(
attackingArmies
.flatMap(group =>
group.armies.map(army =>
s"${group.factionId}:${army.id}:${army.originProvinceId}->${army.destinationProvinceId}:r${army.arrivalRound}"
)
)
.sorted
.toArray ++
attackingArmies.map(_.toProtoString).toArray ++
Array(
defendingArmy
.map(army => s"defender:${army.factionId}")
.getOrElse("no defender"),
s"province:$battleProvinceId",
s"round:$roundId"
defendingArmy.map(_.toProtoString).getOrElse("no defender"),
battleProvinceId.toString,
roundId.toString
)
)
private def shardokPlayers(
attackingArmies: Vector[HostileArmyGroup],
defendingArmy: Option[Army],
defenderFood: Int,
battleProvince: ProvinceT
defenderFood: Int
): Vector[ShardokPlayer] =
attackingArmies.map { aa =>
ShardokPlayer(
@@ -179,8 +154,8 @@ case class RequestBattlesAction(
armyGroup = Some(aa),
food = attackerFood(aa),
victoryConditions = Vector(
VictoryCondition.HoldsCriticalTiles,
VictoryCondition.LastPlayerStanding
VICTORY_CONDITION_HOLDS_CRITICAL_TILES,
VICTORY_CONDITION_LAST_PLAYER_STANDING
)
)
} ++ defendingArmy.map { da =>
@@ -191,109 +166,97 @@ case class RequestBattlesAction(
HostileArmyGroup(
factionId = da.factionId,
armies = Vector(
MovingArmy(
id = 0, // placeholder
army = da,
arrivalRound = currentRoundId,
destinationProvinceId = battleProvince.id,
originProvinceId = battleProvince.id,
supplies = Supplies(0, 0),
suppliesLoss = 0.0,
startingPositionIndex = None
)
),
status = Attacking
MovingArmy(army = Some(da))
)
)
),
food = defenderFood,
victoryConditions = Vector(
VictoryCondition.LastPlayerStanding,
VictoryCondition.WinAfterMaxRounds
VICTORY_CONDITION_LAST_PLAYER_STANDING,
VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS
)
)
}
def attackerFood(armyGroup: HostileArmyGroup): Int = {
Math.min(
BattalionUtils.monthlyConsumedFood(
LegacyBattalionUtils.monthlyConsumedFood(
battalions = armyGroup.armies
.flatMap(_.army.units)
.flatMap(_.getArmy.units)
.flatMap(_.battalionId)
.map(battalions),
bts = battalionTypes.values.toVector
.map(startingGameState.battalions),
bts = startingGameState.battalionTypes.toVector
),
armyGroup.armies.map(_.supplies.food).sum
armyGroup.armies.map(_.supplies.map(_.food).getOrElse(0)).sum
)
}
private def uncontestedAttackResults(
provinceWithAttackingArmies: ProvinceWithAttackingArmies
): Vector[ActionResultT] = {
provinceWithAttackingArmies: ProvinceWithAttackingArmies,
gameState: GameState
): Vector[ActionResult] = {
Vector(
ActionResultC(
actionResultType = SentSuppliesResultType,
ActionResult(
`type` = SENT_SUPPLIES,
changedProvinces = Vector(
ChangedProvinceC(
provinceId = provinceWithAttackingArmies.provinceId,
ChangedProvince(
id = provinceWithAttackingArmies.provinceId,
removedIncomingArmyIds = provinceWithAttackingArmies.armies
.flatMap(_.armies)
.map(_.id),
.map(_.id)
.toVector,
goldDelta = Some(
provinceWithAttackingArmies.armies
.flatMap(_.armies)
.map(_.supplies.gold)
.flatMap(_.supplies)
.map(_.gold)
.sum
).filterNot(_ == 0),
foodDelta = Some(
provinceWithAttackingArmies.armies
.flatMap(_.armies)
.map(_.supplies.food)
.flatMap(_.supplies)
.map(_.food)
.sum
).filterNot(_ == 0)
)
)
),
ProvinceConqueredAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
conqueredProvince = provinces(provinceWithAttackingArmies.provinceId),
notFledConquerors = provinceWithAttackingArmies.armies
.flatMap(_.armies)
.flatMap(_.army.units)
.map(u =>
ResolvedEagleUnit(
hero = heroes(u.heroId),
battalion = u.battalionId.map(battalions),
status = UnitStatus.Normal
)
),
notFledDefenders = Vector.empty,
destroyedBattalions =
provinces(provinceWithAttackingArmies.provinceId).battalionIds,
defendingFactionId =
provinces(provinceWithAttackingArmies.provinceId).rulingFactionId,
factions = factions,
heroes = heroes,
battalions = battalions,
provinces = provinces
).immediateExecute
ActionResultProtoConverter.toProto(
ProvinceConqueredAction(
startingState = gameState,
conqueredProvince =
gameState.provinces(provinceWithAttackingArmies.provinceId),
notFledConquerors = provinceWithAttackingArmies.armies
.flatMap(_.armies)
.flatMap(_.getArmy.units)
.map(u => ResolvedEagleUnit(u, gameState)),
notFledDefenders = Vector.empty,
destroyedBattalions = gameState
.provinces(provinceWithAttackingArmies.provinceId)
.battalionIds
.toVector,
defendingFactionId = gameState
.provinces(provinceWithAttackingArmies.provinceId)
.rulingFactionId
).immediateExecute
)
)
}
private def provincesWithAttackingArmies
: Vector[ProvinceWithAttackingArmies] =
provinces.values
startingGameState.provinces.values
.filter(_.hostileArmies.nonEmpty)
.map(p =>
ProvinceWithAttackingArmies(
provinceId = p.id,
armies = p.hostileArmies
armies = p.hostileArmies.toVector
)
)
.toVector
private case class ProvinceWithAttackingArmies(
case class ProvinceWithAttackingArmies(
provinceId: ProvinceId,
armies: Vector[HostileArmyGroup]
)
@@ -1,81 +1,372 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.common_unit.UnitStatus as CommonUnitStatus
import net.eagle0.common.common_unit.UnitStatus.{
CAPTURED_UNIT,
FLED_UNIT,
NEVER_ENTERED_UNIT,
NORMAL_UNIT,
OUTLAWED_UNIT,
RETREATED_UNIT,
UNKNOWN_UNIT,
Unrecognized
}
import net.eagle0.eagle.common.action_result_type.ActionResultType.BATTLE_ENDED
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.common.recruitment_info.RecruitmentInfo
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.RECRUITMENT_STATUS_OUTLAW
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.UNAFFILIATED_HERO_OUTLAW
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.army.{Army, MovingArmy}
import net.eagle0.eagle.internal.battalion.Battalion
import net.eagle0.eagle.internal.battle_revelation.BattleRevelation
import net.eagle0.eagle.internal.battle_revelation.BattleRevelation.RevelationType.DID_BATTLE
import net.eagle0.eagle.internal.changed_hero.ChangedHero
import net.eagle0.eagle.internal.changed_hero.ChangedHero.Vigor
import net.eagle0.eagle.internal.changed_province.ChangedProvince
import net.eagle0.eagle.internal.event_for_hero_backstory.{
EventForHeroBackstory,
FoughtInBattleBackstoryEvent
}
import net.eagle0.eagle.internal.event_for_hero_backstory.FoughtInBattleBackstoryEvent.UnitStatus
import net.eagle0.eagle.internal.event_for_hero_backstory.FoughtInBattleBackstoryEvent.UnitStatus.{
UNIT_STATUS_CAPTURED,
UNIT_STATUS_FLED,
UNIT_STATUS_NEVER_ENTERED,
UNIT_STATUS_NORMAL,
UNIT_STATUS_OUTLAWED,
UNIT_STATUS_RETREATED,
UNIT_STATUS_UNKNOWN
}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle.BattleType
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle.BattleType.BATTLE_TYPE_ASSAULT_PROVINCE
import net.eagle0.eagle.internal.shardok_battle.{ShardokBattle, ShardokPlayer}
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplier
import net.eagle0.eagle.library.actions.impl.common.{
Action,
ActionWithResultingState
}
import net.eagle0.eagle.library.actions.util.ShatteredArmyUtils
import net.eagle0.eagle.library.settings.{
BattleAgricultureDevastationDelta,
BattleEconomyDevastationDelta,
BattleInfrastructureDevastationDelta
}
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentUtils
import net.eagle0.eagle.library.util.view_filters.LegacyBattalionViewFilter
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.proto_converters.{
ActionResultProtoConverter,
ArmyConverter,
ChangedProvinceConverter
}
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.model.state.shardok_battle.{BattleType, ShardokBattle}
import net.eagle0.eagle.shardok_interface.BattleResolution
import net.eagle0.eagle.{
BattalionId,
FactionId,
GameId,
HeroId,
ProvinceId,
RoundId
}
import net.eagle0.eagle.library.actions.impl.action.{
ProvinceConqueredAction,
ProvinceHeldAction
import net.eagle0.eagle.model.state.quest.QuestT
import net.eagle0.eagle.model.state.quest.concrete.DefeatFactionQuest
import net.eagle0.eagle.shardok_interface.{
BattleResolution,
ResolvedEagleUnit,
ResolvedShardokPlayer
}
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
case class ResolveBattleAction(
gameId: GameId,
currentRoundId: RoundId,
currentDate: Date,
heroes: Map[HeroId, HeroT],
battalions: Map[BattalionId, BattalionT],
provinces: Map[ProvinceId, ProvinceT],
factions: Map[FactionId, FactionT],
outstandingBattles: Vector[ShardokBattle],
startingGameState: GameState,
resolvedBattles: Vector[BattleResolution]
) extends ProtolessSequentialResultsAction {
) extends Action {
def fleeProvince(hero: Hero, battle: ShardokBattle): Option[ProvinceId] =
battle.players
.find(_.eagleFid == hero.getFactionId)
.map(sp =>
sp.getArmyGroup.armies
.map(_.getArmy)
.find(_.units.exists(_.heroId == hero.id))
.get
)
.flatMap(_.fleeProvinceId)
override def results: Vector[ActionResultT] = {
override def execute(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionWithResultingState] =
for {
rb <- resolvedBattles
sb <- outstandingBattles
.find(_.shardokGameId == rb.battle.shardokGameId)
.toVector
res <- resolveOneBattleProtoless(sb, rb)
sb <- outstandingBattle(
startingGameState.outstandingBattles.toVector,
rb
).toVector
res <- resolveOneBattle(
startingState = startingGameState,
actionResultProtoApplier = actionResultProtoApplier,
battle = sb,
battleResolution = rb
)
} yield res
}
def fleeProvince(hero: HeroT, battle: ShardokBattle): Option[ProvinceId] =
battle.players
.find(_.eagleFid == hero.factionId.get)
.flatMap(_.armyGroup)
.flatMap(_.armies.find(_.army.units.exists(_.heroId == hero.id)))
.flatMap(_.army.fleeProvinceId)
private def newOutlaws(
battleResolution: BattleResolution
): Vector[UnaffiliatedHero] =
(for {
rsp <- battleResolution.resolvedPlayers
outlaw <- rsp.units.filter(_.status == OUTLAWED_UNIT)
} yield UnaffiliatedHero(
heroId = outlaw.hero.id,
`type` = UNAFFILIATED_HERO_OUTLAW,
roundsInType = 0,
recruitmentAttempted = false,
lastFaction = Some(rsp.eagleFid),
recruitmentInfo = Some(
RecruitmentInfo(
status = RECRUITMENT_STATUS_OUTLAW
)
)
)).toVector
private def resolveOneBattleProtoless(
private def defenderCP(
battle: ShardokBattle,
battleResolution: BattleResolution
): Vector[ActionResultT] = {
): ChangedProvince =
ChangedProvince(
id = battle.defenderProvince,
newUnaffiliatedHeroes = newOutlaws(battleResolution),
removedHostileArmyFactionIds =
battle.players.filterNot(_.isDefender).map(_.eagleFid),
removedRulingPlayerHeroIds = (for {
rsp <- battleResolution.resolvedPlayers
if startingGameState
.provinces(battle.defenderProvince)
.rulingFactionId
.contains(rsp.eagleFid)
rse <- rsp.units
in <- battle.players
.find(sp => rse.hero.factionId.contains(sp.eagleFid))
if unitReturned(
hid = rse.hero.id,
in = in,
out = rsp,
battleType = battle.battleType
)
} yield rse.hero.id).toVector,
removedBattalionIds = (for {
rsp <- battleResolution.resolvedPlayers
if startingGameState
.provinces(battle.defenderProvince)
.rulingFactionId
.contains(rsp.eagleFid)
rse <- rsp.units
in <- battle.players
.find(sp => rse.hero.factionId.contains(sp.eagleFid))
if unitReturned(
hid = rse.hero.id,
in = in,
out = rsp,
battleType = battle.battleType
)
} yield rse.battalion.id).toVector,
goldDelta = Some(
battle.players
.filterNot(_.isDefender)
.flatMap(_.getArmyGroup.armies)
.map(_.getSupplies.gold)
.sum
),
foodDelta = Some(
battle.players
.filterNot(_.isDefender)
.flatMap(_.getArmyGroup.armies)
.map(_.getSupplies.food)
.sum
),
economyDevastationDelta = Some(BattleEconomyDevastationDelta.doubleValue),
agricultureDevastationDelta =
Some(BattleAgricultureDevastationDelta.doubleValue),
infrastructureDevastationDelta =
Some(BattleInfrastructureDevastationDelta.doubleValue),
addedBattleRevelations =
battle.players.filterNot(_.isDefender).map { sp =>
BattleRevelation(
provinceId = battle.defenderProvince,
revealedToFactionId = sp.eagleFid,
revelationType = DID_BATTLE
)
}
)
private def defeatLoserQuestCompletionCheck(
losingFids: Vector[FactionId]
): (QuestT, ProvinceT) => Boolean =
(q, _: ProvinceT) =>
q match {
case DefeatFactionQuest(targetFactionId) =>
losingFids.contains(targetFactionId)
case _ => false
}
private def winnersFulfilledQuestActionResults(
startingState: GameState,
battleResolution: BattleResolution
): Vector[ActionResultT] =
battleResolution.resolvedPlayers.toVector.partition(
_.endGameCondition.condition.isVictory
) match {
case (winners, losers) =>
winners
.flatMap(rsp =>
LegacyFactionUtils.provinces(rsp.eagleFid, startingState)
)
.flatMap(p =>
QuestFulfillmentUtils.fulfilledQuestResults(
province = ProvinceConverter.fromProto(p),
isFulfilled =
defeatLoserQuestCompletionCheck(losers.map(_.eagleFid)),
gameId = startingState.gameId,
currentDate = DateConverter.fromProto(startingState.currentDate),
currentRoundId = startingState.currentRoundId,
previousBackstoryTextIdLookup = hid => {
startingState.heroes(hid).backstoryVersions.last.textId
}
)
)
}
private def verifyHidsOutMatchHidsIn(
battle: ShardokBattle,
battleResolution: BattleResolution
): Unit = {
battle.players
.sortBy(_.eagleFid)
.zip(battleResolution.resolvedPlayers.sortBy(_.eagleFid))
.foreach { case (setup, resolved) =>
internalRequire(
resolved.eagleFid == setup.eagleFid,
s"FactionIds do not match for all players in ${battle.players}, ${battleResolution.resolvedPlayers}"
)
val sentHids =
setup.getArmyGroup.armies
.flatMap(_.getArmy.units)
.map(_.heroId)
.toSet
val returnedHids = resolved.units.map(_.hero.id).toSet
internalRequire(
sentHids == returnedHids,
s"Sent $sentHids into battle but got $returnedHids back"
)
}
}
private def battleEndedARs(
startingState: GameState,
battle: ShardokBattle,
battleResolution: BattleResolution
): Vector[ActionResult] = {
val changedHeroes = battleResolution.resolvedPlayers
.flatMap(_.units)
.map { u =>
val isAttacker = !battle.players
.find(_.eagleFid == u.hero.factionId.get)
.get
.isDefender
changedHeroWithNewBackstoryRequest(
newHero = u.hero,
newBattalion = Option.when(
u.battalion != Battalion.defaultInstance && u.battalion.id != -1
) {
u.battalion
},
status = u.status match {
case UNKNOWN_UNIT => UNIT_STATUS_UNKNOWN
case NORMAL_UNIT => UNIT_STATUS_NORMAL
case CAPTURED_UNIT => UNIT_STATUS_CAPTURED
case FLED_UNIT => UNIT_STATUS_FLED
case RETREATED_UNIT => UNIT_STATUS_RETREATED
case NEVER_ENTERED_UNIT => UNIT_STATUS_NEVER_ENTERED
case OUTLAWED_UNIT => UNIT_STATUS_OUTLAWED
case CommonUnitStatus.Unrecognized(value) =>
UnitStatus.Unrecognized(value)
},
provinceId = battle.defenderProvince,
enemyFactionIds = battle.players
.filter(_.isDefender == isAttacker)
.map(_.eagleFid)
.toVector,
isVictorious = battleResolution.resolvedPlayers
.find(_.eagleFid == u.hero.factionId.get)
.get
.endGameCondition
.condition
.isVictory,
isAttacker = isAttacker,
startingState = startingState
)
}
.toVector
ActionResult(
`type` = BATTLE_ENDED,
changedHeroes = changedHeroes,
changedBattalions = battleResolution.resolvedPlayers
.flatMap(_.units)
.map(_.battalion)
.filterNot(_.id == 0)
.filterNot(_.size == 0)
.toVector,
changedProvinces = changedProvincesFromWithdrawnUnits(
batt = battle,
br = battleResolution,
destroyedBattalionIds = destroyedBattalionIds(battleResolution)
) :+ defenderCP(
battle = battle,
battleResolution = battleResolution
),
destroyedBattalionIds = destroyedBattalionIds(battleResolution),
resolvedBattle = Some(battle.shardokGameId)
) +: winnersFulfilledQuestActionResults(
startingState = startingState,
battleResolution = battleResolution
).map(ActionResultProtoConverter.toProto)
}
private def resolveOneBattle(
startingState: GameState,
actionResultProtoApplier: ActionResultProtoApplier,
battle: ShardokBattle,
battleResolution: BattleResolution
): Vector[ActionWithResultingState] = {
internalRequire(
battle == battleResolution.battle,
s"Battles do not match! Sent $battle but resolved ${battleResolution.battle}"
)
verifyHidsOutMatchHidsIn(battle, battleResolution)
val (winningResolvedPlayers, losingResolvedPlayers) =
battleResolution.resolvedPlayers.toVector.partition(
_.endGameCondition.condition.isVictory
)
val battleEndedResults = actionResultProtoApplier.applyActionResults(
startingState,
battleEndedARs(startingState, battle, battleResolution)
)
val winningShardokPlayers = winningResolvedPlayers
.map(_.eagleFid)
.flatMap(fid => battle.players.find(_.eagleFid == fid))
val finalResult = battle.battleType match {
case BattleType.Unknown =>
case BattleType.BATTLE_TYPE_UNKNOWN =>
throw new EagleInternalException("Unknown battle type")
case BattleType.AssaultProvince =>
case BattleType.BATTLE_TYPE_ASSAULT_PROVINCE =>
internalRequire(
winningShardokPlayers.size == 1,
s"Assault province battle type must have exactly one victor"
@@ -83,57 +374,121 @@ case class ResolveBattleAction(
val wsp = winningShardokPlayers.head
if wsp.isDefender then
val gs = battleEndedResults.last.gameState
val defendingProv = gs.provinces(battle.defenderProvince)
val capturedUnits = battleResolution.resolvedPlayers.toVector
.filterNot(_.eagleFid == wsp.eagleFid)
.flatMap { losingPlayer =>
losingPlayer.units
.filterNot(u =>
unitReturned(
hid = u.hero.id,
in = battle.players
.find(_.eagleFid == losingPlayer.eagleFid)
.get,
out = losingPlayer,
battleType = BATTLE_TYPE_ASSAULT_PROVINCE
)
)
.filterNot(_.status.isOutlawedUnit)
}
ProvinceHeldAction(
gameId = gameId,
currentRoundId = currentRoundId,
defendingProvince = provinces(battle.defenderProvince),
gameId = gs.gameId,
currentRoundId = gs.currentRoundId,
defendingProvince = ProvinceConverter.fromProto(defendingProv),
attackingFactionIds = losingResolvedPlayers.map(_.eagleFid),
capturedHeroIds =
Vector.empty, // TODO: implement captured units logic
destroyedBattalionIds = destroyedBattalionIds(battleResolution),
heroFactions = Map.empty // TODO: implement hero factions
).immediateExecute
capturedHeroIds = capturedUnits.map(_.hero.id),
destroyedBattalionIds = capturedUnits
.flatMap(_.optionalBattalionId)
.filter(gs.battalions.contains),
heroFactions = capturedUnits
.map(u => u.hero.id -> gs.heroes(u.hero.id).factionId.get)
.toMap
)
else
ProvinceConqueredAction(
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
conqueredProvince = provinces(battle.defenderProvince),
notFledConquerors = Vector.empty, // TODO: implement
notFledDefenders = Vector.empty, // TODO: implement
startingState = battleEndedResults.last.gameState,
conqueredProvince = battleEndedResults.last.gameState
.provinces(battle.defenderProvince),
notFledConquerors = winningResolvedPlayers.head.units
.filterNot(u =>
unitReturned(
hid = u.hero.id,
in = wsp,
out = winningResolvedPlayers.head,
battleType = BATTLE_TYPE_ASSAULT_PROVINCE
)
)
.filterNot(_.status == OUTLAWED_UNIT)
.toVector,
notFledDefenders = (for {
sp <- battle.players.filter(_.isDefender)
rsp = battleResolution.resolvedPlayers
.find(_.eagleFid == sp.eagleFid)
.get
u <- rsp.units.filterNot(u =>
unitReturned(
hid = u.hero.id,
in = sp,
out = rsp,
battleType = BATTLE_TYPE_ASSAULT_PROVINCE
)
)
} yield u).toVector,
destroyedBattalions = destroyedBattalionIds(battleResolution),
defendingFactionId =
battle.players.find(_.isDefender).map(_.eagleFid),
factions = factions,
heroes = heroes,
battalions = battalions,
provinces = provinces
).immediateExecute
battle.players.find(_.isDefender).map(_.eagleFid)
)
end if
case BattleType.FreeForAll =>
case BattleType.BATTLE_TYPE_FREE_FOR_ALL =>
Option
.when(winningShardokPlayers.nonEmpty) {
WonFreeForAllAction(
battalions = battalions,
battleProvince = provinces(battle.defenderProvince),
winningUnits = winningResolvedPlayers.flatMap(_.units),
winningArmyGroups =
winningShardokPlayers.flatMap(_.armyGroup).toVector
).immediateExecute
startingState = battleEndedResults.last.gameState,
battleProvince = battleEndedResults.last.gameState
.provinces(battle.defenderProvince),
winningUnits = for {
wrp <- winningResolvedPlayers
wsp = battle.players.find(_.eagleFid == wrp.eagleFid).get
reu <- wrp.units
if !unitReturned(
hid = reu.hero.id,
in = wsp,
out = wrp,
battleType = BattleType.BATTLE_TYPE_FREE_FOR_ALL
)
} yield reu,
winningPlayers = winningShardokPlayers
)
}
.getOrElse(
FreeForAllDrawAction(
defenderProvince = battle.defenderProvince,
armiesFromPlayers =
battle.players.flatMap(_.armyGroup).flatMap(_.armies).toVector,
remainingUnits = battleResolution.resolvedPlayers.flatMap(_.units)
).immediateExecute
startingState = battleEndedResults.last.gameState,
battle = battle,
remainingUnits = battleResolution.resolvedPlayers.flatMap { rsp =>
rsp.units.filterNot(reu =>
unitReturned(
hid = reu.hero.id,
in = battle.players.find(_.eagleFid == rsp.eagleFid).get,
out = rsp,
battleType = BattleType.BATTLE_TYPE_FREE_FOR_ALL
)
)
}
)
)
case BattleType.Sally =>
case BattleType.BATTLE_TYPE_SALLY =>
throw new EagleInternalException("Sally battle unimplemented")
case BattleType.Unrecognized(value) =>
throw new EagleInternalException(s"Unrecognized battle type $value")
}
Vector(finalResult)
battleEndedResults :+ actionResultProtoApplier
.applyActionResult(
battleEndedResults.last.gameState,
ActionResultProtoConverter.toProto(finalResult.immediateExecute)
)
}
def destroyedBattalionIds(
@@ -141,15 +496,226 @@ case class ResolveBattleAction(
): Vector[BattalionId] =
(battleResolution.resolvedPlayers
.flatMap(_.units)
.flatMap(_.battalion)
.filterNot(_.hasDefaultBattalion)
.map(_.battalion)
.filter(_.size == 0)
.map(_.id)
.filterNot(_ == 0) ++ battleResolution.resolvedPlayers
.flatMap(_.units)
.filter(unit =>
unit.status == UnitStatus.Captured || unit.status == UnitStatus.Outlawed
unit.status == CAPTURED_UNIT || unit.status == OUTLAWED_UNIT
)
.flatMap(_.optionalBattalionId)
.map(_.battalion.id)
.filterNot(_ == 0)).toVector
private def maybeDelta(newValue: Int, oldValue: Int): Option[Int] =
Option(newValue - oldValue).filterNot(_ == 0)
private def changedHeroWithNewBackstoryRequest(
newHero: Hero,
newBattalion: Option[Battalion],
status: UnitStatus,
provinceId: ProvinceId,
isVictorious: Boolean,
isAttacker: Boolean,
enemyFactionIds: Vector[FactionId],
startingState: GameState
): ChangedHero = (
startingState.heroes(newHero.id),
newBattalion.map(b => startingState.battalions(b.id))
) match {
case (originalHero, originalBattalion) =>
val backstoryEvent = Option.when(enemyFactionIds.nonEmpty) {
EventForHeroBackstory(
date = startingState.currentDate,
details = FoughtInBattleBackstoryEvent(
provinceId = provinceId,
battalion = originalBattalion.map(
LegacyBattalionViewFilter.filteredBattalionViewBelongingToPlayer
),
isVictorious = isVictorious,
isAttacker = isAttacker,
status = status,
enemyFactionIds = enemyFactionIds
)
)
}
ChangedHero(
id = newHero.id,
vigor = Vigor.VigorAbsolute(newHero.vigor),
strengthXpDelta =
maybeDelta(newHero.strengthXp, originalHero.strengthXp),
agilityXpDelta = maybeDelta(newHero.agilityXp, originalHero.agilityXp),
wisdomXpDelta = maybeDelta(newHero.wisdomXp, originalHero.wisdomXp),
charismaXpDelta =
maybeDelta(newHero.charismaXp, originalHero.charismaXp),
constitutionXpDelta =
maybeDelta(newHero.constitutionXp, originalHero.constitutionXp),
newBackstoryEvents = backstoryEvent.toVector
)
}
private def changedProvincesFromWithdrawnUnits(
batt: ShardokBattle,
br: BattleResolution,
destroyedBattalionIds: Vector[BattalionId]
): Vector[ChangedProvince] =
(for {
sp <- batt.players
rsp = br.resolvedPlayers.find(_.eagleFid == sp.eagleFid).get
ma <- sp.getArmyGroup.armies
army <- armyFromResolvedArmy(
ma = ma,
sp = sp,
rsp = rsp,
bt = batt.battleType,
destroyedBattalionIds = destroyedBattalionIds
)
} yield ma.getArmy.fleeProvinceId
.map(fleeProvince =>
changedProvinceFromFledUnits(
fledArmy = army,
arrivalRound = batt.roundStarted + 1,
destinationProvince = fleeProvince,
battleProvince = batt.defenderProvince
)
)
.getOrElse(
changedProvinceFromShatteredArmy(
shatteredArmy = ma,
battleProvinceId = batt.defenderProvince
)
)).toVector
private def armyFromResolvedArmy(
ma: MovingArmy,
sp: ShardokPlayer,
rsp: ResolvedShardokPlayer,
bt: BattleType,
destroyedBattalionIds: Vector[BattalionId]
): Option[Army] =
armyFromResolvedUnits(
units = ma.getArmy.units
.filter(u =>
unitReturned(hid = u.heroId, in = sp, out = rsp, battleType = bt)
)
.flatMap(cu => rsp.units.find(_.hero.id == cu.heroId))
.toVector,
removedBattalionIds = destroyedBattalionIds,
fid = ma.getArmy.factionId,
fleeProvinceId =
Option.when(rsp.endGameCondition.condition.isAllyVictory)(
ma.destinationProvince
)
)
private def armyFromResolvedUnits(
units: Vector[ResolvedEagleUnit],
removedBattalionIds: Vector[BattalionId],
fid: FactionId,
fleeProvinceId: Option[ProvinceId]
): Option[Army] =
if units.isEmpty then None
else
Some(
Army(
fleeProvinceId = fleeProvinceId,
factionId = fid,
units = units
.map(ru =>
CombatUnit(
factionId = fid,
heroId = ru.hero.id,
battalionId = ru.optionalBattalionId
.filterNot(
removedBattalionIds.contains(_)
)
.filterNot(_ == 0)
)
)
)
)
private def changedProvinceFromFledUnits(
fledArmy: Army,
arrivalRound: RoundId,
destinationProvince: ProvinceId,
battleProvince: ProvinceId
): ChangedProvince =
ChangedProvince(
id = destinationProvince,
addedIncomingArmies = Vector(
MovingArmy(
army = Some(fledArmy),
arrivalRound = arrivalRound,
destinationProvince = destinationProvince,
originProvince = battleProvince
)
)
)
private def changedProvinceFromShatteredArmy(
shatteredArmy: MovingArmy,
battleProvinceId: ProvinceId
): ChangedProvince =
ChangedProvinceConverter.toProto(
ShatteredArmyUtils.provinceWithOutlawsForIncomingArmy(
currentRoundId = startingGameState.currentRoundId,
province = ProvinceConverter.fromProto(
startingGameState.provinces(battleProvinceId)
),
incomingArmy = ArmyConverter.fromProto(shatteredArmy)
)
)
private def unitReturned(
hid: HeroId,
in: ShardokPlayer,
out: ResolvedShardokPlayer,
battleType: BattleType
): Boolean =
out.units.find(_.hero.id == hid).get.status match {
case FLED_UNIT => true
case NEVER_ENTERED_UNIT =>
!in.isDefender &&
in.getArmyGroup.armies
.find(_.getArmy.units.exists(_.heroId == hid))
.get
.getArmy
.fleeProvinceId
.isDefined &&
!out.endGameCondition.condition.isVictory
case CAPTURED_UNIT =>
out.endGameCondition.condition.isDraw || out.endGameCondition.condition.isAllyVictory
case NORMAL_UNIT =>
out.endGameCondition.condition.isDraw || out.endGameCondition.condition.isAllyVictory
case RETREATED_UNIT => false
case OUTLAWED_UNIT => false
case UNKNOWN_UNIT =>
throw new EagleInternalException("Unknown unit status")
case Unrecognized(value) =>
throw new EagleInternalException(s"Unrecognized unit status $value")
}
private def outstandingBattle(
outstandingBattles: Vector[ShardokBattle],
resolvedBattle: BattleResolution
): Option[ShardokBattle] = {
val obo = outstandingBattles
.find(_.shardokGameId == resolvedBattle.battle.shardokGameId)
.orElse {
printf(
s"Failed to locate outstanding battle ${resolvedBattle.battle.shardokGameId}; may already be resolved"
)
None
}
if obo.exists(ob => ob != resolvedBattle.battle) then {
throw new EagleInternalException(
s"Battles do not match! ${obo.get} vs ${resolvedBattle.battle}"
)
}
obo
}
}
@@ -1,45 +1,45 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.model.state.CombatUnit
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.internal.shardok_battle.ShardokPlayer
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
import net.eagle0.eagle.model.action_result.ActionResultT
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.WonFreeForAllResultType
import net.eagle0.eagle.model.proto_converters.ArmyConverter
import net.eagle0.eagle.model.state.HostileArmyGroup
import net.eagle0.eagle.model.state.HostileArmyGroupStatus.AwaitingDecision
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.shardok_interface.ResolvedEagleUnit
import net.eagle0.eagle.BattalionId
case class WonFreeForAllAction(
battalions: Map[BattalionId, BattalionT],
battleProvince: ProvinceT,
startingState: GameState,
battleProvince: Province,
winningUnits: Vector[ResolvedEagleUnit],
winningArmyGroups: Vector[HostileArmyGroup]
winningPlayers: Vector[ShardokPlayer]
) extends ProtolessSimpleAction {
override def immediateExecute: ActionResultT = {
val notFledHids = winningUnits.map(_.hero.id)
val incomingArmies = winningArmyGroups
.flatMap(_.armies)
.map { movingArmy =>
movingArmy.copy(
army = movingArmy.army.copy(
units = movingArmy.army.units
.filter(cu => notFledHids.contains(cu.heroId))
.map(cu =>
CombatUnit(
heroId = cu.heroId,
battalionId = cu.battalionId.filter(battalions.contains),
factionId = cu.factionId
)
val incomingArmies = winningPlayers
.flatMap(_.getArmyGroup.armies)
.map { army =>
army.update(
_.army.units.modify(
_.filter(cu => notFledHids.contains(cu.heroId)).map(cu =>
CombatUnit(
heroId = cu.heroId,
battalionId =
cu.battalionId.filter(startingState.battalions.contains),
factionId = cu.factionId
)
)
)
)
}
.filter(_.army.units.nonEmpty)
.filter(_.getArmy.units.nonEmpty)
.toVector
ActionResultC(
@@ -51,11 +51,11 @@ case class WonFreeForAllAction(
removedHostileArmyFactionIds =
battleProvince.hostileArmies.map(_.factionId).toVector,
newHostileArmies = incomingArmies
.groupBy(_.army.factionId)
.groupBy(_.getArmy.factionId)
.map { case (fid, armies) =>
HostileArmyGroup(
factionId = fid,
armies = armies,
armies = armies.map(ArmyConverter.fromProto),
status = AwaitingDecision
)
}
@@ -93,7 +93,6 @@ 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:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:combat_unit_converter",
@@ -101,6 +100,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/proto_converters:proto_conversion_exception",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:tribute_amount_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/battalion",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer/status",
@@ -53,7 +53,6 @@ import net.eagle0.eagle.model.proto_converters.province.{
ProvinceOrderTypeConverter
}
import net.eagle0.eagle.model.proto_converters.{
BattalionConverter,
BattalionTypeConverter,
ImprovementTypeConverter,
ProtoConversionException,
@@ -61,6 +60,7 @@ import net.eagle0.eagle.model.proto_converters.{
UnaffiliatedHeroConverter
}
import net.eagle0.eagle.model.proto_converters.BattalionTypeIdConverter
import net.eagle0.eagle.model.proto_converters.battalion.BattalionConverter
import net.eagle0.eagle.model.proto_converters.diplomacy_option.DiplomacyOptionConverter
import net.eagle0.eagle.model.state.diplomacy_offer.{
AllianceOffer,
@@ -439,9 +439,7 @@ class CommandFactory {
gold = sc.gold,
food = sc.food,
availableDestinationProvinceIds =
commandForOrigin.availableDestinationProvinces
.map(_.provinceId)
.toVector,
commandForOrigin.availableDestinationProvinces.toVector,
availableHeroIds = commandForOrigin.availableHeroIds.toVector,
availableBattalionIds =
commandForOrigin.availableBattalions.map(_.battalionId).toVector,
@@ -87,20 +87,6 @@ scala_library(
],
)
scala_library(
name = "battalion_utils",
srcs = ["BattalionUtils.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
],
)
scala_library(
name = "beast_utils",
srcs = ["BeastUtils.scala"],
@@ -127,8 +113,6 @@ scala_library(
],
visibility = [
":__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/util:__pkg__",
],
exports = [
@@ -233,7 +217,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util:battalion_power",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/battalion",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
],
@@ -1,24 +0,0 @@
package net.eagle0.eagle.library.util
import net.eagle0.eagle.model.state.BattalionType
import net.eagle0.eagle.model.state.battalion.BattalionT
object BattalionUtils {
def monthlyConsumedFood(
battalions: Iterable[BattalionT],
bts: Vector[BattalionType]
): Int =
battalions
.map(b => monthlyConsumedFood(b, bts))
.sum
.ceil
.toInt
def monthlyConsumedFood(
battalion: BattalionT,
bts: Vector[BattalionType]
): Double = {
val battalionType = bts.find(_.typeId == battalion.typeId).get
battalion.size.toDouble * battalionType.monthlyFoodCost
}
}
@@ -6,7 +6,7 @@ import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.util.BattalionPower
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.proto_converters.battalion.BattalionConverter
import net.eagle0.eagle.{FactionId, ProvinceId, RoundId}
object IncomingArmyUtils {
@@ -96,8 +96,7 @@ object AttackCommandChooser {
): Vector[PossibleAttackCommandInfo] =
(for {
opmc <- ac.oneProvinceCommands
availableDestination <- opmc.availableDestinationProvinces
destinationProvinceId = availableDestination.provinceId
destinationProvinceId <- opmc.availableDestinationProvinces
destinationProvince = gameState.provinces(destinationProvinceId)
if destinationFactionIsEligible(
actingFactionId = actingFactionId,
@@ -130,8 +129,20 @@ object AttackCommandChooser {
battalionTypes = gameState.battalionTypes.toVector
)
// Get river crossing requirement from the available destination
val requiresRiverCrossing = availableDestination.requiresRiverCrossing
val startingPositionIndex = destinationProvince.neighbors
.find(n => n.provinceId == originProvince.id)
.get
.startingPositionIndex
val isWinter = winterMonths.contains(gameState.currentDate.get.month)
val mapInfo = ShardokMapInfo.shardokMapInfos(destinationProvince.id)
val destinationCastleCount =
ShardokMapInfo.shardokMapInfos(destinationProvince.id).castleCount
val requiresRiverCrossing = !isWinter && (10 - mapInfo
.waterCrossingRequirementsByPosition(
startingPositionIndex
) > destinationCastleCount)
PossibleAttackCommandInfo(
selectedUnits = selectedUnits,
@@ -1218,7 +1218,7 @@ object CommandChoiceHelpers {
): Option[CommandSelection] = {
val originProvince = gameState.provinces(opmc.originProvinceId)
opmc.availableDestinationProvinces
.map(dest => gameState.provinces(dest.provinceId))
.map(gameState.provinces)
.filter(_.rulingFactionId.contains(actingFactionId))
.filter(p => p.rulingFactionHeroIds.length < p.heroCap)
.maxByOption(p => p.rulingFactionHeroIds.length - p.heroCap)
@@ -1288,7 +1288,7 @@ object CommandChoiceHelpers {
functionalRandom
.nextRandomElementOpt(
opmc.availableDestinationProvinces
.map(dest => gameState.provinces(dest.provinceId))
.map(gameState.provinces)
.filter(_.rulingFactionId.isEmpty)
) match {
case RandomState(Some(destProvince), fr) =>
@@ -1744,7 +1744,7 @@ object CommandChoiceHelpers {
else {
val interestingDestinations =
fromHere.get.availableDestinationProvinces
.map(dest => gs.provinces(dest.provinceId))
.map(gs.provinces)
.filter(_.rulingFactionId.contains(actingFactionId))
.filterNot(p =>
LegacyProvinceUtils.containsFactionLeader(p.id, gs)
@@ -51,9 +51,7 @@ object ExpandCommandSelector {
expansionCandidateProvinces(
gameState,
oneProvinceCommand.originProvinceId,
oneProvinceCommand.availableDestinationProvinces
.map(_.provinceId)
.toVector
oneProvinceCommand.availableDestinationProvinces.toVector
).partition(p =>
LegacyProvinceUtils
.isAdjacentEnemy(p.id, actingFactionId, gameState)
@@ -392,7 +390,7 @@ object ExpandCommandSelector {
spreadHeroesMarchCandidates(
gameState,
opmc.originProvinceId,
opmc.availableDestinationProvinces.map(_.provinceId).toVector
opmc.availableDestinationProvinces.toVector
).map(
(opmc, _)
)
@@ -1,11 +1,8 @@
package net.eagle0.eagle.library.util.hero
import net.eagle0.eagle.HeroId
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.hero.Profession.NoProfession
import scala.annotation.tailrec
object HeroUtils {
def power(hero: HeroT): Double = (
hero.strength
@@ -20,47 +17,4 @@ object HeroUtils {
private def professionPower(hero: HeroT): Double =
if hero.profession == NoProfession then 0.0
else professionPowerValue
def sortKeys(
hero: HeroT,
factionLeaderIds: Vector[HeroId],
includePower: Boolean = true
): Vector[Int] = {
val factionHeadScore =
if !includePower then 0
else if factionLeaderIds.headOption.contains(hero.id) then 2
else if factionLeaderIds.contains(hero.id) then 1
else 0
val professionScore =
if hero.profession == NoProfession then 0
else professionPower(hero).toInt
val powerScore =
if includePower then power(hero).toInt // max ~700
else 0
Vector(
factionHeadScore,
professionScore,
powerScore,
hero.id // just to make the sort stable
)
}
def sortOrderer(
factionLeaderIds: Vector[HeroId]
): (HeroT, HeroT) => Boolean = { case (h1, h2) =>
@tailrec def go(keys: Vector[(Int, Int)]): Boolean = keys match {
case (key1, key2) +: t =>
if key1 == key2 then go(t)
else key1 > key2
case _ => false
}
go(sortKeys(h1, factionLeaderIds).zip(sortKeys(h2, factionLeaderIds)))
}
def sortOrdering(factionLeaderIds: Vector[HeroId]): Ordering[HeroT] =
Ordering.fromLessThan(sortOrderer(factionLeaderIds))
}
@@ -69,9 +69,6 @@ object LegacyHeroUtils {
go(sortKeys(gameState, hid1).zip(sortKeys(gameState, hid2)))
}
def sortOrdering(gameState: GameState): Ordering[HeroId] =
Ordering.fromLessThan(sortOrderer(gameState))
// Returns a compatibility level from 0 to 100
def discordance(a: Hero, b: Hero): Double = {
def oneTraitFactor(aTrait: Double, bTrait: Double): Double =
@@ -9,7 +9,6 @@ import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.shardok_battle.ShardokBattle
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
object ActionResultT {
@@ -28,7 +27,7 @@ trait ActionResultT {
// def lastCommandTypeForActingProvince: Option[CommandType]
def newBattle: Option[ShardokBattle]
// def newBattle: Option[ShardokBattleT]
// def resolvedBattle: Option[String]
// def resolvedImminentBattle: Option[Int]
@@ -73,7 +72,7 @@ trait ActionResultT {
newRoundId: Option[RoundId] = newRoundId,
newDate: Option[Date] = newDate,
// lastCommandTypeForActingProvince: Option[CommandType] = None,
newBattle: Option[ShardokBattle] = newBattle,
// newBattle: Option[ShardokBattleT] = None,
// resolvedBattle: Option[String] = None,
// resolvedImminentBattle: Option[Int] = None,
// newChronicleEntry: Option[ChronicleEntry] = None,
@@ -41,7 +41,6 @@ scala_library(
"//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",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
],
)
@@ -17,7 +17,6 @@ import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.shardok_battle.ShardokBattle
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
object ActionResultC {
@@ -33,7 +32,6 @@ case class ActionResultC(
newRoundPhase: Option[RoundPhase] = None,
newRoundId: Option[RoundId] = None,
newDate: Option[Date] = None,
newBattle: Option[ShardokBattle] = None,
changedBattalions: Vector[ChangedBattalionT] = Vector.empty,
changedFactions: Vector[ChangedFactionT] = Vector.empty,
changedHeroes: Vector[ChangedHeroT] = Vector.empty,
@@ -67,7 +65,6 @@ case class ActionResultC(
newRoundPhase: Option[RoundPhase] = newRoundPhase,
newRoundId: Option[RoundId] = newRoundId,
newDate: Option[Date] = newDate,
newBattle: Option[ShardokBattle] = newBattle,
changedBattalions: Vector[ChangedBattalionT] = changedBattalions,
changedFactions: Vector[ChangedFactionT] = changedFactions,
changedHeroes: Vector[ChangedHeroT] = changedHeroes,
@@ -99,7 +96,6 @@ case class ActionResultC(
newRoundPhase = newRoundPhase,
newRoundId = newRoundId,
newDate = newDate,
newBattle = newBattle,
changedBattalions = changedBattalions,
changedFactions = changedFactions,
changedHeroes = changedHeroes,
@@ -33,7 +33,6 @@ scala_library(
"//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",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
],
)
@@ -29,6 +29,7 @@ import net.eagle0.eagle.model.action_result.{
ClientTextVisibilityExtensionT,
NotificationT
}
import net.eagle0.eagle.model.proto_converters.battalion.BattalionConverter
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.hero.{
@@ -36,14 +37,12 @@ import net.eagle0.eagle.model.proto_converters.hero.{
HeroConverter
}
import net.eagle0.eagle.model.proto_converters.generated_text_request.LlmRequestConverter
import net.eagle0.eagle.model.proto_converters.shardok_battle.ShardokBattleConverter
import net.eagle0.eagle.model.state.RoundPhase
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.shardok_battle.ShardokBattle
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
object ActionResultProtoConverter {
@@ -58,7 +57,6 @@ object ActionResultProtoConverter {
newRoundPhase: Option[RoundPhase],
newRoundId: Option[RoundId],
newDate: Option[Date],
newBattle: Option[ShardokBattle],
changedBattalions: Vector[ChangedBattalionT],
changedFactions: Vector[ChangedFactionT],
changedHeroes: Vector[ChangedHeroT],
@@ -92,7 +90,6 @@ object ActionResultProtoConverter {
),
newRoundId = newRoundId,
newDate = newDate.map(DateConverter.toProto),
newBattle = newBattle.map(ShardokBattleConverter.toProto),
changedBattalions = changedBattalions.map {
case ChangedBattalionC(to) => BattalionConverter.toProto(to)
},
@@ -12,7 +12,6 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
deps = [
":battalion_converter",
":changed_faction_converter",
":changed_hero_converter",
":changed_province_converter",
@@ -30,18 +29,17 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_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/types/base:action_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/battalion",
"//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/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/shardok_battle:shardok_battle_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/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
],
)
@@ -69,29 +67,6 @@ scala_library(
],
)
scala_library(
name = "battalion_converter",
srcs = ["BattalionConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
deps = [
":battalion_type_id_converter",
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:proto_conversion_exception",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
],
)
scala_library(
name = "battalion_type_id_converter",
srcs = ["BattalionTypeIdConverter.scala"],
@@ -99,9 +74,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/battalion:__pkg__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
@@ -115,10 +89,8 @@ scala_library(
name = "battalion_type_converter",
srcs = ["BattalionTypeConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
deps = [
@@ -189,7 +161,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:event_for_hero_backstory",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/hero:event_for_hero_backstory_trait",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
],
)
@@ -0,0 +1,22 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "battalion",
srcs = ["BattalionConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:proto_conversion_exception",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
],
)
@@ -1,9 +1,12 @@
package net.eagle0.eagle.model.proto_converters
package net.eagle0.eagle.model.proto_converters.battalion
import net.eagle0.eagle.BattalionId
import net.eagle0.eagle.common.battalion_type.BattalionTypeId as BattalionTypeIdProto
import net.eagle0.eagle.internal.battalion.Battalion as BattalionProto
import net.eagle0.eagle.model.proto_converters.ProtoConversionException
import net.eagle0.eagle.model.proto_converters.{
BattalionTypeIdConverter,
ProtoConversionException
}
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
@@ -0,0 +1,15 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "battalion_status",
srcs = ["BattalionStatusConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/common:common_unit_scala_proto",
"//src/main/scala/net/eagle0/eagle/model/state/battalion_status",
],
)
@@ -0,0 +1,27 @@
package net.eagle0.eagle.model.proto_converters.battalion_status
import net.eagle0.common.common_unit.UnitStatus
import net.eagle0.eagle.model.state.battalion_status.BattalionStatus
object BattalionStatusConverter {
def fromProto(proto: UnitStatus): BattalionStatus = proto match {
case UnitStatus.NORMAL_UNIT => BattalionStatus.Normal
case UnitStatus.CAPTURED_UNIT => BattalionStatus.Captured
case UnitStatus.FLED_UNIT => BattalionStatus.Fled
case UnitStatus.RETREATED_UNIT => BattalionStatus.Retreated
case UnitStatus.NEVER_ENTERED_UNIT => BattalionStatus.NeverEntered
case UnitStatus.OUTLAWED_UNIT => BattalionStatus.Outlawed
case UnitStatus.UNKNOWN_UNIT => BattalionStatus.Normal
case UnitStatus.Unrecognized(value) =>
throw new IllegalArgumentException(s"Unknown UnitStatus: $value")
}
def toProto(model: BattalionStatus): UnitStatus = model match {
case BattalionStatus.Normal => UnitStatus.NORMAL_UNIT
case BattalionStatus.Captured => UnitStatus.CAPTURED_UNIT
case BattalionStatus.Fled => UnitStatus.FLED_UNIT
case BattalionStatus.Retreated => UnitStatus.RETREATED_UNIT
case BattalionStatus.NeverEntered => UnitStatus.NEVER_ENTERED_UNIT
case BattalionStatus.Outlawed => UnitStatus.OUTLAWED_UNIT
}
}
@@ -7,7 +7,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state/faction",
@@ -14,11 +14,11 @@ 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/model/action_result:notification_trait",
"//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:notification_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:proto_conversion_exception",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:round_phase_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/battalion",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/chronicle_entry:chronicle_entry_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
@@ -13,7 +13,6 @@ import net.eagle0.eagle.internal.run_status.RunStatus as RunStatusProto
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle as ShardokBattleProto
import net.eagle0.eagle.common.battalion_type.BattalionType as BattalionTypeProto
import net.eagle0.eagle.model.proto_converters.{
BattalionConverter,
BattalionTypeConverter,
NotificationConverter,
ProtoConversionException,
@@ -27,6 +26,7 @@ import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.proto_converters.run_status.RunStatusConverter
import net.eagle0.eagle.model.proto_converters.shardok_battle.ShardokBattleConverter
import net.eagle0.eagle.model.action_result.NotificationT
import net.eagle0.eagle.model.proto_converters.battalion.BattalionConverter
import net.eagle0.eagle.model.state.BattalionType
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
@@ -23,7 +23,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state/hero:event_for_hero_backstory_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/unit_status",
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
],
)
@@ -56,8 +55,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
@@ -74,7 +71,6 @@ scala_library(
"//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/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
],
)
@@ -2,7 +2,6 @@ package net.eagle0.eagle.model.proto_converters.hero
import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus
import net.eagle0.eagle.common.unaffiliated_hero_quest.Quest as QuestProto
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.internal.event_for_hero_backstory.FoughtInBattleBackstoryEvent.UnitStatus.{
UNIT_STATUS_CAPTURED,
UNIT_STATUS_FLED,
@@ -190,7 +189,7 @@ object EventForHeroBackstoryConverter {
isVictorious: Boolean,
isAttacker: Boolean,
enemyFactionIds: Vector[FactionId],
status: UnitStatus
status: FoughtInBattleBackstoryEvent.BattalionStatus
) =>
FoughtInBattleBackstoryEventProto(
provinceId = provinceId,
@@ -349,14 +348,14 @@ object EventForHeroBackstoryConverter {
)
def toProto(
unitStatus: UnitStatus
): UnitStatusProto = unitStatus match {
case UnitStatus.Normal => UNIT_STATUS_NORMAL
case UnitStatus.Captured => UNIT_STATUS_CAPTURED
case UnitStatus.Fled => UNIT_STATUS_FLED
case UnitStatus.Retreated => UNIT_STATUS_RETREATED
case UnitStatus.NeverEntered => UNIT_STATUS_NEVER_ENTERED
case UnitStatus.Outlawed => UNIT_STATUS_OUTLAWED
battalionStatus: FoughtInBattleBackstoryEvent.BattalionStatus
): UnitStatusProto = battalionStatus match {
case FoughtInBattleBackstoryEvent.Normal => UNIT_STATUS_NORMAL
case FoughtInBattleBackstoryEvent.Captured => UNIT_STATUS_CAPTURED
case FoughtInBattleBackstoryEvent.Fled => UNIT_STATUS_FLED
case FoughtInBattleBackstoryEvent.Retreated => UNIT_STATUS_RETREATED
case FoughtInBattleBackstoryEvent.NeverEntered => UNIT_STATUS_NEVER_ENTERED
case FoughtInBattleBackstoryEvent.Outlawed => UNIT_STATUS_OUTLAWED
}
def fromProto(
@@ -669,15 +668,16 @@ object EventForHeroBackstoryConverter {
def fromProto(
statusProto: UnitStatusProto
): UnitStatus = {
): FoughtInBattleBackstoryEvent.BattalionStatus = {
statusProto match {
case UNIT_STATUS_NORMAL => UnitStatus.Normal
case UNIT_STATUS_CAPTURED => UnitStatus.Captured
case UNIT_STATUS_FLED => UnitStatus.Fled
case UNIT_STATUS_RETREATED => UnitStatus.Retreated
case UNIT_STATUS_NEVER_ENTERED => UnitStatus.NeverEntered
case UNIT_STATUS_OUTLAWED => UnitStatus.Outlawed
case UNIT_STATUS_UNKNOWN =>
case UNIT_STATUS_NORMAL => FoughtInBattleBackstoryEvent.Normal
case UNIT_STATUS_CAPTURED => FoughtInBattleBackstoryEvent.Captured
case UNIT_STATUS_FLED => FoughtInBattleBackstoryEvent.Fled
case UNIT_STATUS_RETREATED => FoughtInBattleBackstoryEvent.Retreated
case UNIT_STATUS_NEVER_ENTERED =>
FoughtInBattleBackstoryEvent.NeverEntered
case UNIT_STATUS_OUTLAWED => FoughtInBattleBackstoryEvent.Outlawed
case UNIT_STATUS_UNKNOWN =>
throw new ProtoConversionException(
s"Unknown UnitStatusProto"
)
@@ -12,7 +12,8 @@ object GenderConverter {
}
def fromProto(genderProto: GenderProto): Gender = genderProto match {
case GenderProto.GENDER_UNKNOWN => Gender.Other
case GenderProto.GENDER_UNKNOWN =>
throw new ProtoConversionException("unknown gender proto")
case GenderProto.GENDER_FEMALE => Gender.Female
case GenderProto.GENDER_MALE => Gender.Male
case GenderProto.GENDER_OTHER => Gender.Other
@@ -27,10 +27,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer/status",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:event_for_hero_backstory",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request/chronicle_event",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/state/hero/backstory_version",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
],
)
@@ -9,7 +9,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
exports = [
@@ -3,12 +3,7 @@ load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "shardok_battle_converter",
srcs = ["ShardokBattleConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl/action:__pkg__",
],
visibility = ["//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/common:victory_condition_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
@@ -1,15 +0,0 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "unit_status_converter",
srcs = ["UnitStatusConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl/action:__pkg__",
],
deps = [
"//src/main/protobuf/net/eagle0/common:common_unit_scala_proto",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
],
)
@@ -1,27 +0,0 @@
package net.eagle0.eagle.model.proto_converters.unit_status
import net.eagle0.common.common_unit.UnitStatus as UnitStatusProto
import net.eagle0.eagle.model.state.unit_status.UnitStatus
object UnitStatusConverter {
def fromProto(unitStatus: UnitStatusProto): UnitStatus = unitStatus match {
case UnitStatusProto.NORMAL_UNIT => UnitStatus.Normal
case UnitStatusProto.CAPTURED_UNIT => UnitStatus.Captured
case UnitStatusProto.FLED_UNIT => UnitStatus.Fled
case UnitStatusProto.RETREATED_UNIT => UnitStatus.Retreated
case UnitStatusProto.NEVER_ENTERED_UNIT => UnitStatus.NeverEntered
case UnitStatusProto.OUTLAWED_UNIT => UnitStatus.Outlawed
case UnitStatusProto.Unrecognized(value) =>
throw new IllegalArgumentException(s"Unknown UnitStatus: $value")
}
def toProto(unitStatus: UnitStatus): UnitStatusProto =
unitStatus match {
case UnitStatus.Normal => UnitStatusProto.NORMAL_UNIT
case UnitStatus.Captured => UnitStatusProto.CAPTURED_UNIT
case UnitStatus.Fled => UnitStatusProto.FLED_UNIT
case UnitStatus.Retreated => UnitStatusProto.RETREATED_UNIT
case UnitStatus.NeverEntered => UnitStatusProto.NEVER_ENTERED_UNIT
case UnitStatus.Outlawed => UnitStatusProto.OUTLAWED_UNIT
}
}
@@ -2,22 +2,15 @@ load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "battalion",
srcs = [
"BattalionT.scala",
],
srcs = ["BattalionT.scala"],
visibility = [
":__subpackages__",
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/action_result:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/battalion:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/state/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/hero:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/resolved_eagle_unit:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
],
@@ -5,8 +5,7 @@ scala_library(
srcs = ["BattalionC.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/battalion:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
],
@@ -0,0 +1,11 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "battalion_status",
srcs = ["BattalionStatus.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/state/battalion:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/resolved_eagle_unit:__pkg__",
],
)
@@ -1,6 +1,6 @@
package net.eagle0.eagle.model.state.unit_status
package net.eagle0.eagle.model.state.battalion_status
enum UnitStatus {
enum BattalionStatus {
case Normal
case Captured
case Fled
@@ -18,7 +18,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer/status",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
],
)
@@ -33,8 +32,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/resolved_eagle_unit:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/main/scala/net/eagle0/util/hero_generation:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
@@ -48,7 +47,6 @@ scala_library(
visibility = [
":__subpackages__",
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
],
@@ -75,7 +73,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/main/scala/net/eagle0/util/hero_generation:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
@@ -1,11 +1,9 @@
package net.eagle0.eagle.model.state.hero
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.diplomacy_offer.status.Status
import net.eagle0.eagle.model.state.quest.QuestT
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.model.view.battalion.BattalionView
sealed trait EventForHeroBackstoryT { def date: Date }
@@ -73,6 +71,16 @@ case class ExiledBackstoryEvent(
exiledFromProvinceId: ProvinceId
) extends EventForHeroBackstoryT
object FoughtInBattleBackstoryEvent {
sealed trait BattalionStatus
case object Normal extends BattalionStatus
case object Captured extends BattalionStatus
case object Fled extends BattalionStatus
case object Retreated extends BattalionStatus
case object NeverEntered extends BattalionStatus
case object Outlawed extends BattalionStatus
}
case class FoughtInBattleBackstoryEvent(
date: Date,
provinceId: ProvinceId,
@@ -80,7 +88,7 @@ case class FoughtInBattleBackstoryEvent(
isVictorious: Boolean,
isAttacker: Boolean,
enemyFactionIds: Vector[FactionId],
status: UnitStatus
status: FoughtInBattleBackstoryEvent.BattalionStatus
) extends EventForHeroBackstoryT
case class HeroDepartedBackstoryEvent(
@@ -8,7 +8,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
@@ -0,0 +1,21 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "resolved_eagle_unit",
srcs = ["ResolvedEagleUnit.scala"],
visibility = [
":__subpackages__",
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/action_result:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion_status",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
],
)
@@ -0,0 +1,15 @@
package net.eagle0.eagle.model.state.resolved_eagle_unit
import net.eagle0.eagle.BattalionId
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.battalion_status.BattalionStatus
import net.eagle0.eagle.model.state.hero.HeroT
case class ResolvedEagleUnit(
hero: HeroT,
battalion: BattalionT,
status: BattalionStatus
) {
def optionalBattalionId: Option[BattalionId] =
if battalion.id == 0 then None else Some(battalion.id)
}
@@ -4,16 +4,9 @@ scala_library(
name = "shardok_battle",
srcs = ["ShardokBattle.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/action_result:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/shardok_battle:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl/action:__pkg__",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
@@ -1,12 +0,0 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "unit_status",
srcs = ["UnitStatus.scala"],
visibility = [
":__subpackages__",
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
deps = [],
)
@@ -10,8 +10,8 @@ scala_library(
],
deps = [
":resolved_shardok_player",
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
],
)
@@ -92,13 +92,11 @@ scala_library(
],
deps = [
":eagle_unit",
"//src/main/protobuf/net/eagle0/common:common_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_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/model/state/battalion",
"//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:profession",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
],
)
@@ -155,13 +153,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:vigor_to_constitution_xp_multiplier",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
"//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/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
],
)
@@ -1,6 +1,6 @@
package net.eagle0.eagle.shardok_interface
import net.eagle0.eagle.model.state.shardok_battle.ShardokBattle
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle
case class BattleResolution(
battle: ShardokBattle,
@@ -1,26 +1,39 @@
package net.eagle0.eagle.shardok_interface
import net.eagle0.common.common_unit.UnitStatus
import net.eagle0.eagle.BattalionId
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.internal.battalion.Battalion as EagleBattalion
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero as EagleHero
object ResolvedEagleUnit {
def apply(eagleUnit: CombatUnit, gs: GameState): ResolvedEagleUnit =
ResolvedEagleUnit(
hero = gs.heroes(eagleUnit.heroId),
battalion = eagleUnit.battalionId
.map(gs.battalions)
.getOrElse(EagleUnit.defaultBattalion),
status = UnitStatus.NORMAL_UNIT
)
}
case class ResolvedEagleUnit(
hero: HeroT,
battalion: Option[BattalionT],
hero: EagleHero,
battalion: EagleBattalion,
status: UnitStatus
) {
def hasDefaultBattalion: Boolean =
battalion.id == EagleUnit.defaultBattalionId
def optionalBattalionId: Option[BattalionId] =
battalion.map(_.id)
if hasDefaultBattalion then None else Some(battalion.id)
def validate(gameState: GameState): Unit = {
battalion.foreach { b =>
require(
gameState.battalions.contains(b.id),
s"Unknown battalion ID in $b"
)
}
require(
gameState.battalions.contains(battalion.id) || hasDefaultBattalion,
s"Unknown battalion ID in $battalion"
)
require(
gameState.heroes.contains(hero.id),
@@ -19,9 +19,6 @@ import net.eagle0.eagle.internal.shardok_battle.{ShardokBattle, ShardokPlayer}
import net.eagle0.eagle.library.settings.VigorToConstitutionXpMultiplier
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.{FactionId, GameId, ShardokGameId}
import net.eagle0.shardok.api.placement_command.PlacementCommand
import net.eagle0.shardok.common.hex_map.HexMap
@@ -73,21 +70,13 @@ class ShardokInterfaceGrpcClient(
ResolvedShardokPlayer(
eagleFid = ui.eagleFactionId,
endGameCondition = ui.endGameCondition.get,
units = ui.units.map(ru => {
val commonUnit = ru.unit.get
val eagleUnit = fromCommon(commonUnit)
units = ui.units.map(ru =>
ResolvedEagleUnit(
HeroConverter.fromProto(eagleUnit.hero),
if commonUnit.battalion.get.eagleBattalionId == EagleUnit.defaultBattalionId
then None
else
Some(
BattalionConverter.fromProto(eagleUnit.battalion)
)
,
convertUnitStatus(ru.status)
ru.unit.map(fromCommon).get.hero,
ru.unit.map(fromCommon).get.battalion,
ru.status
)
})
)
)
)
)
@@ -392,23 +381,4 @@ class ShardokInterfaceGrpcClient(
armament = commonUnit.battalion.get.armament
)
)
private def convertUnitStatus(
protoStatus: net.eagle0.common.common_unit.UnitStatus
): UnitStatus =
protoStatus match {
case net.eagle0.common.common_unit.UnitStatus.NORMAL_UNIT =>
UnitStatus.Normal
case net.eagle0.common.common_unit.UnitStatus.CAPTURED_UNIT =>
UnitStatus.Captured
case net.eagle0.common.common_unit.UnitStatus.FLED_UNIT => UnitStatus.Fled
case net.eagle0.common.common_unit.UnitStatus.RETREATED_UNIT =>
UnitStatus.Retreated
case net.eagle0.common.common_unit.UnitStatus.NEVER_ENTERED_UNIT =>
UnitStatus.NeverEntered
case net.eagle0.common.common_unit.UnitStatus.OUTLAWED_UNIT =>
UnitStatus.Outlawed
case other =>
throw new IllegalArgumentException(s"Unrecognized UnitStatus: $other")
}
}
@@ -2,7 +2,6 @@ package net.eagle0.eagle.ai
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.api.available_command.{
AvailableDestinationProvince,
MarchAvailableCommand,
MarchCommandFromOneProvince
}
@@ -80,12 +79,7 @@ class FixLeaderAloneCommandSelectorTest
MarchCommandFromOneProvince(
originProvinceId = 15,
availableHeroIds = Vector(151),
availableDestinationProvinces = Vector(14, 16).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
)
availableDestinationProvinces = Vector(14, 16)
)
)
),
@@ -95,12 +89,7 @@ class FixLeaderAloneCommandSelectorTest
MarchCommandFromOneProvince(
originProvinceId = 16,
availableHeroIds = Vector(161),
availableDestinationProvinces = Vector(15, 17).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
)
availableDestinationProvinces = Vector(15, 17)
)
)
)
@@ -139,12 +128,7 @@ class FixLeaderAloneCommandSelectorTest
MarchCommandFromOneProvince(
originProvinceId = 16,
availableHeroIds = Vector(161),
availableDestinationProvinces = Vector(15, 17).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
)
availableDestinationProvinces = Vector(15, 17)
)
)
)
@@ -2,7 +2,6 @@ package net.eagle0.eagle.ai
import net.eagle0.eagle.api.available_command.{
AvailableCommand,
AvailableDestinationProvince,
ImproveAvailableCommand,
MarchAvailableCommand,
MarchCommandFromOneProvince,
@@ -130,22 +129,12 @@ class MidGameAIClientTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = 2,
availableDestinationProvinces = Vector(1, 3, 4).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(1, 3, 4),
availableHeroIds = Vector(87)
),
MarchCommandFromOneProvince(
originProvinceId = 3,
availableDestinationProvinces = Vector(1, 2).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(1, 2),
availableHeroIds = Vector(11, 12, 13, 14),
goldAvailable = 6000,
foodAvailable = 7000
@@ -204,22 +193,12 @@ class MidGameAIClientTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = 2,
availableDestinationProvinces = Vector(1, 3, 4).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(1, 3, 4),
availableHeroIds = Vector(87)
),
MarchCommandFromOneProvince(
originProvinceId = 3,
availableDestinationProvinces = Vector(1, 2).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(1, 2),
availableHeroIds = Vector(11, 12, 13, 14),
goldAvailable = 6000,
foodAvailable = 7000
@@ -262,32 +241,17 @@ class MidGameAIClientTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = 1,
availableDestinationProvinces = Vector(2, 3, 4).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(2, 3, 4),
availableHeroIds = Vector(123, 124, 125, 126, 127, 128)
),
MarchCommandFromOneProvince(
originProvinceId = 2,
availableDestinationProvinces = Vector(1, 3, 4).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(1, 3, 4),
availableHeroIds = Vector(87)
),
MarchCommandFromOneProvince(
originProvinceId = 3,
availableDestinationProvinces = Vector(1, 2).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(1, 2),
availableHeroIds = Vector(11, 12, 13, 14),
goldAvailable = 6000,
foodAvailable = 7000
@@ -904,12 +868,7 @@ class MidGameAIClientTest
oneProvinceCommands = Vector[MarchCommandFromOneProvince](
MarchCommandFromOneProvince(
originProvinceId = 7,
availableDestinationProvinces = Vector(12, 15).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(12, 15),
availableHeroIds = Vector(10, 20, 30, 40, 50)
)
),
@@ -1,7 +1,6 @@
package net.eagle0.eagle.ai
import net.eagle0.eagle.api.available_command.{
AvailableDestinationProvince,
MarchAvailableCommand,
MarchCommandFromOneProvince
}
@@ -92,22 +91,12 @@ class MoveLeaderToBetterProvinceCommandChooserTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = 2,
availableDestinationProvinces = Vector(1, 3, 4).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(1, 3, 4),
availableHeroIds = Vector(87)
),
MarchCommandFromOneProvince(
originProvinceId = 1,
availableDestinationProvinces = Vector(2, 3).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(2, 3),
availableHeroIds = Vector(leaderId),
goldAvailable = 6000,
foodAvailable = 7000
@@ -164,24 +153,14 @@ class MoveLeaderToBetterProvinceCommandChooserTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = 1,
availableDestinationProvinces = Vector(2, 3).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(2, 3),
availableHeroIds = Vector(leaderId),
goldAvailable = 6000,
foodAvailable = 7000
),
MarchCommandFromOneProvince(
originProvinceId = 2,
availableDestinationProvinces = Vector(1, 3, 4).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(1, 3, 4),
availableHeroIds = Vector(123, 58, 59, 70),
goldAvailable = 6000,
foodAvailable = 7000
@@ -236,22 +215,12 @@ class MoveLeaderToBetterProvinceCommandChooserTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = 2,
availableDestinationProvinces = Vector(1, 3, 4).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(1, 3, 4),
availableHeroIds = Vector(87)
),
MarchCommandFromOneProvince(
originProvinceId = 1,
availableDestinationProvinces = Vector(2, 3).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(2, 3),
availableHeroIds = Vector(leaderId),
goldAvailable = 6000,
foodAvailable = 7000
@@ -3,7 +3,6 @@ package net.eagle0.eagle.ai
import net.eagle0.eagle.api.available_command.SwearBrotherhoodAvailableCommand.HeroAndBackstory
import net.eagle0.eagle.api.available_command.{
AvailableCommand,
AvailableDestinationProvince,
EligibleGift,
FeastAvailableCommand,
HeroGiftAvailableCommand,
@@ -300,12 +299,7 @@ class SeekMoreLeadersCommandChooserTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = 20,
availableDestinationProvinces = Vector(17, 909).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(17, 909),
availableHeroIds = Vector(12, 15, 16)
)
),
@@ -16,7 +16,6 @@ import net.eagle0.eagle.library.settings.{
MaxGoldTribute,
MinVigorForMarch
}
import net.eagle0.eagle.library.util.ShardokMapInfo
import org.scalatest.BeforeAndAfterEach
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
@@ -42,11 +41,7 @@ class AvailableMarchCommandFactoryTest
private val actingProvince = Province(
id = 7,
neighbors = Vector(
Neighbor(provinceId = 4, startingPositionIndex = 0),
Neighbor(provinceId = 8, startingPositionIndex = 1),
Neighbor(provinceId = 12, startingPositionIndex = 2)
),
neighbors = Vector(4, 8, 12).map(pid => Neighbor(provinceId = pid)),
rulingFactionId = Some(actingPlayerId),
rulingFactionHeroIds = sufficientVigorHeroes
.map(_.id) ++ insufficientVigorHeroes.map(_.id),
@@ -71,24 +66,9 @@ class AvailableMarchCommandFactoryTest
val gameState = GameState(
provinces = Map(
7 -> actingProvince,
4 -> Province(
id = 4,
rulingFactionId = Some(5),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
),
8 -> Province(
id = 8,
rulingFactionId = Some(6),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
4 -> Province(id = 4, rulingFactionId = Some(5)),
8 -> Province(id = 8, rulingFactionId = Some(6)),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(sufficientVigorHeroes ++ insufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -101,8 +81,7 @@ class AvailableMarchCommandFactoryTest
7
)
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces
.map(_.provinceId) should contain allElementsOf Vector(
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces should contain allElementsOf Vector(
8,
12
)
@@ -112,24 +91,9 @@ class AvailableMarchCommandFactoryTest
val gameState = GameState(
provinces = Map(
7 -> actingProvince,
4 -> Province(
id = 4,
rulingFactionId = Some(5),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
),
8 -> Province(
id = 8,
rulingFactionId = Some(6),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
4 -> Province(id = 4, rulingFactionId = Some(5)),
8 -> Province(id = 8, rulingFactionId = Some(6)),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(sufficientVigorHeroes ++ insufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -142,8 +106,7 @@ class AvailableMarchCommandFactoryTest
7
)
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces
.map(_.provinceId) should contain(
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces should contain(
4
)
}
@@ -152,24 +115,9 @@ class AvailableMarchCommandFactoryTest
val gameState = GameState(
provinces = Map(
7 -> actingProvince,
4 -> Province(
id = 4,
rulingFactionId = Some(5),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
),
8 -> Province(
id = 8,
rulingFactionId = None,
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
4 -> Province(id = 4, rulingFactionId = Some(5)),
8 -> Province(id = 8, rulingFactionId = None),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(sufficientVigorHeroes ++ insufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -182,8 +130,7 @@ class AvailableMarchCommandFactoryTest
7
)
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces
.map(_.provinceId) should contain(
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces should contain(
8
)
}
@@ -195,22 +142,10 @@ class AvailableMarchCommandFactoryTest
4 -> Province(
id = 4,
rulingFactionId = Some(5),
activeEvents = Vector(BlizzardEvent()),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
activeEvents = Vector(BlizzardEvent())
),
8 -> Province(
id = 8,
rulingFactionId = None,
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
8 -> Province(id = 8, rulingFactionId = None),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(sufficientVigorHeroes ++ insufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -223,32 +158,16 @@ class AvailableMarchCommandFactoryTest
7
)
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces
.map(_.provinceId) should not contain 4
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces should not contain 4
}
it should "include heroes with sufficient vigor" in {
val gameState = GameState(
provinces = Map(
7 -> actingProvince,
4 -> Province(
id = 4,
rulingFactionId = Some(5),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
),
8 -> Province(
id = 8,
rulingFactionId = None,
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
4 -> Province(id = 4, rulingFactionId = Some(5)),
8 -> Province(id = 8, rulingFactionId = None),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(sufficientVigorHeroes ++ insufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -269,24 +188,9 @@ class AvailableMarchCommandFactoryTest
val gameState = GameState(
provinces = Map(
7 -> actingProvince,
4 -> Province(
id = 4,
rulingFactionId = Some(5),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
),
8 -> Province(
id = 8,
rulingFactionId = None,
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
4 -> Province(id = 4, rulingFactionId = Some(5)),
8 -> Province(id = 8, rulingFactionId = None),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(sufficientVigorHeroes ++ insufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -307,24 +211,9 @@ class AvailableMarchCommandFactoryTest
val gameState = GameState(
provinces = Map(
7 -> actingProvince,
4 -> Province(
id = 4,
rulingFactionId = Some(5),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
),
8 -> Province(
id = 8,
rulingFactionId = None,
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
4 -> Province(id = 4, rulingFactionId = Some(5)),
8 -> Province(id = 8, rulingFactionId = None),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(sufficientVigorHeroes ++ insufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -349,24 +238,9 @@ class AvailableMarchCommandFactoryTest
rulingFactionHeroIds = actingProvince.rulingFactionHeroIds
.filterNot(id => sufficientVigorHeroes.map(_.id).contains(id))
),
4 -> Province(
id = 4,
rulingFactionId = Some(5),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
),
8 -> Province(
id = 8,
rulingFactionId = None,
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
4 -> Province(id = 4, rulingFactionId = Some(5)),
8 -> Province(id = 8, rulingFactionId = None),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(insufficientVigorHeroes ++ sufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -387,24 +261,9 @@ class AvailableMarchCommandFactoryTest
7 -> actingProvince.withActiveEvents(
Vector(BlizzardEvent())
),
4 -> Province(
id = 4,
rulingFactionId = Some(5),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
),
8 -> Province(
id = 8,
rulingFactionId = None,
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
4 -> Province(id = 4, rulingFactionId = Some(5)),
8 -> Province(id = 8, rulingFactionId = None),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(sufficientVigorHeroes ++ insufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -424,24 +283,9 @@ class AvailableMarchCommandFactoryTest
val gameState = GameState(
provinces = Map(
7 -> actingProvince,
4 -> Province(
id = 4,
rulingFactionId = Some(5),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 0))
),
8 -> Province(
id = 8,
rulingFactionId = Some(6),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 1))
),
12 -> Province(
id = 12,
rulingFactionId = Some(9),
neighbors =
Vector(Neighbor(provinceId = 7, startingPositionIndex = 2))
)
4 -> Province(id = 4, rulingFactionId = Some(5)),
8 -> Province(id = 8, rulingFactionId = Some(6)),
12 -> Province(id = 12, rulingFactionId = Some(9))
),
heroes = HeroMap(sufficientVigorHeroes ++ insufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
@@ -456,186 +300,4 @@ class AvailableMarchCommandFactoryTest
availableCommand.get.oneProvinceCommands.head.goldAvailable shouldBe actingProvince.gold
}
// Helper method to create a destination province with proper neighbor back-reference
private def createDestinationProvince(
provinceId: Int,
backReferencePositionIndex: Int,
rulingFactionId: Option[Int] = None
): Province = {
Province(
id = provinceId,
neighbors = Vector(
Neighbor(
provinceId = 7,
startingPositionIndex = backReferencePositionIndex
)
),
rulingFactionId = rulingFactionId
)
}
"availableCommand" should "set requiresRiverCrossing correctly based on map info" in {
// We need to use real province IDs that exist in the ShardokMapInfo
// Let's use province 1 as the acting province and 37 as destination
val actingProvinceId = 1
val destinationProvinceId = 37
val testActingProvince = Province(
id = actingProvinceId,
neighbors = Vector(
Neighbor(provinceId = destinationProvinceId, startingPositionIndex = 0)
),
rulingFactionId = Some(actingPlayerId),
rulingFactionHeroIds = sufficientVigorHeroes.map(_.id),
battalionIds = validBattalions.map(_.id),
gold = 3232,
food = 6263
)
val testDestinationProvince = Province(
id = destinationProvinceId,
neighbors = Vector(
Neighbor(provinceId = actingProvinceId, startingPositionIndex = 0)
),
rulingFactionId = Some(6)
)
val gameState = GameState(
provinces = Map(
actingProvinceId -> testActingProvince,
destinationProvinceId -> testDestinationProvince
),
heroes = HeroMap(sufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
battalionTypes = battalionTypes
)
val availableCommand = AvailableMarchCommandFactory.availableCommand(
gameState,
actingPlayerId,
actingProvinceId
)
val destinationProvince =
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces
.find(_.provinceId == destinationProvinceId)
destinationProvince should be(defined)
// Calculate expected river crossing requirement based on map info
val mapInfo = ShardokMapInfo.shardokMapInfos(destinationProvinceId)
val expectedRequiresRiverCrossing =
mapInfo.waterCrossingRequirementsByPosition(0) >= 10
destinationProvince.get.requiresRiverCrossing shouldBe expectedRequiresRiverCrossing
}
it should "set requiresRiverCrossing based on map info regardless of specific values" in {
// Use the same province combinations as the first river crossing test for consistency
val actingProvinceId = 1
val destinationProvinceId = 37
val testActingProvince = Province(
id = actingProvinceId,
neighbors = Vector(
Neighbor(provinceId = destinationProvinceId, startingPositionIndex = 0)
),
rulingFactionId = Some(actingPlayerId),
rulingFactionHeroIds = sufficientVigorHeroes.map(_.id),
battalionIds = validBattalions.map(_.id),
gold = 3232,
food = 6263
)
val testDestinationProvince = Province(
id = destinationProvinceId,
neighbors = Vector(
Neighbor(provinceId = actingProvinceId, startingPositionIndex = 0)
),
rulingFactionId = Some(6)
)
val gameState = GameState(
provinces = Map(
actingProvinceId -> testActingProvince,
destinationProvinceId -> testDestinationProvince
),
heroes = HeroMap(sufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
battalionTypes = battalionTypes
)
val availableCommand = AvailableMarchCommandFactory.availableCommand(
gameState,
actingPlayerId,
actingProvinceId
)
val destinationProvince =
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces
.find(_.provinceId == destinationProvinceId)
destinationProvince should be(defined)
// Calculate expected river crossing requirement
val mapInfo = ShardokMapInfo.shardokMapInfos(destinationProvinceId)
val expectedRequiresRiverCrossing =
mapInfo.waterCrossingRequirementsByPosition(0) >= 10
destinationProvince.get.requiresRiverCrossing shouldBe expectedRequiresRiverCrossing
}
it should "correctly handle multiple destination provinces" in {
// Use a simpler test that just verifies the feature works with one province
val actingProvinceId = 1
val destinationProvinceId = 37
val testActingProvince = Province(
id = actingProvinceId,
neighbors = Vector(
Neighbor(provinceId = destinationProvinceId, startingPositionIndex = 0)
),
rulingFactionId = Some(actingPlayerId),
rulingFactionHeroIds = sufficientVigorHeroes.map(_.id),
battalionIds = validBattalions.map(_.id),
gold = 3232,
food = 6263
)
val testDestinationProvince = Province(
id = destinationProvinceId,
neighbors = Vector(
Neighbor(provinceId = actingProvinceId, startingPositionIndex = 0)
),
rulingFactionId = Some(6)
)
val gameState = GameState(
provinces = Map(
actingProvinceId -> testActingProvince,
destinationProvinceId -> testDestinationProvince
),
heroes = HeroMap(sufficientVigorHeroes),
battalions = BattalionMap(validBattalions),
battalionTypes = battalionTypes
)
val availableCommand = AvailableMarchCommandFactory.availableCommand(
gameState,
actingPlayerId,
actingProvinceId
)
val availableDestinations =
availableCommand.get.oneProvinceCommands.head.availableDestinationProvinces
availableDestinations should have size 1
// Verify the destination has a river crossing requirement (boolean value)
val destination = availableDestinations.head
destination.provinceId shouldBe destinationProvinceId
// The requiresRiverCrossing should be a boolean value (we don't need to test the exact logic here)
destination.requiresRiverCrossing shouldBe a[Boolean]
}
}
@@ -375,7 +375,6 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/library/settings:max_food_tribute",
"//src/main/scala/net/eagle0/eagle/library/settings:max_gold_tribute",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_march",
"//src/main/scala/net/eagle0/eagle/library/util:shardok_map_info",
],
)
@@ -57,16 +57,6 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/library/settings:portion_of_capacity_for_upgrade_battalion_quest",
"//src/main/scala/net/eagle0/eagle/library/settings:quest_recruitment_bonus",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/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/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:battle_ended_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:province_conquered_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:province_held_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/date",
@@ -609,6 +599,7 @@ scala_test(
name = "perform_uncontested_conquest_action_test",
srcs = ["PerformUncontestedConquestActionTest.scala"],
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:eagle_validation_exception",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:perform_uncontested_conquest_action",
@@ -623,18 +614,11 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/model/action_result/types:province_conquered_result_type",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:sent_supplies_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:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
"//src/main/scala/net/eagle0/eagle/model/state:supplies",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
],
)
@@ -713,6 +697,7 @@ scala_test(
name = "province_conquered_action_test",
srcs = ["ProvinceConqueredActionTest.scala"],
deps = [
"//src/main/protobuf/net/eagle0/common:common_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
@@ -748,25 +733,13 @@ scala_test(
"//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:province_conquered_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_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/diplomacy_offer",
"//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:captured_hero",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//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/unit_status",
"//src/main/scala/net/eagle0/eagle/shardok_interface:eagle_unit",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
@@ -777,10 +750,9 @@ scala_test(
name = "province_held_action_test",
srcs = ["ProvinceHeldActionTest.scala"],
deps = [
"//src/main/protobuf/net/eagle0/common:common_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:gender_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:army_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
@@ -800,13 +772,8 @@ scala_test(
"//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:province_held_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//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:captured_hero",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
],
@@ -816,24 +783,25 @@ scala_test(
name = "request_battles_action_test",
srcs = ["RequestBattlesActionTest.scala"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:army_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:supplies_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//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/impl/action:request_battles_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/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/state:battalion_type",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
"//src/main/scala/net/eagle0/eagle/shardok_interface:eagle_unit",
"//src/test/scala/net/eagle0/common:proto_matchers",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
"//src/test/scala/net/eagle0/eagle/library/util/validations:testing_noop_validator",
"@maven//:org_scalamock_scalamock_3",
],
)
@@ -841,36 +809,30 @@ scala_test(
name = "resolve_battle_action_test",
srcs = ["ResolveBattleActionTest.scala"],
deps = [
"//src/main/protobuf/net/eagle0/common:victory_condition_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
"//src/main/protobuf/net/eagle0/common:common_unit_scala_proto",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_scala_grpc",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:army_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:supplies_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:resolve_battle_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: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/base:action_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/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero/backstory_version",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/shardok_battle:shardok_battle_converter",
"//src/main/scala/net/eagle0/eagle/library/settings:battle_agriculture_devastation_delta",
"//src/main/scala/net/eagle0/eagle/library/settings:battle_economy_devastation_delta",
"//src/main/scala/net/eagle0/eagle/library/settings:battle_infrastructure_devastation_delta",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_shardok_player",
"//src/test/scala/net/eagle0/common:proto_matchers",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
"@maven//:org_scalamock_scalamock_3",
],
)
@@ -993,8 +955,23 @@ scala_test(
name = "won_free_for_all_action_test",
srcs = ["WonFreeForAllActionTest.scala"],
deps = [
"//src/main/protobuf/net/eagle0/common:common_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:army_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:supplies_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:won_free_for_all_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//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",
@@ -1002,11 +979,11 @@ scala_test(
"//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:won_free_for_all_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/battalion/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/unit_status",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
"//src/test/scala/net/eagle0/common:proto_matchers",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
"//src/test/scala/net/eagle0/eagle/library/util/validations:testing_noop_validator",
],
)
@@ -43,36 +43,4 @@ class EndDiplomacyResolutionPhaseActionTest
result.newRoundPhase shouldBe Some(RoundPhase.ReconResolution)
}
}
it should "handle game state updates through the action sequence" in {
// Regression test for bug where invalidated offers used stale game state
// This would previously throw an exception when later actions in the sequence
// tried to process offers that had been invalidated but were still marked
// as Unresolved in the stale initial game state
// The fix ensures that each action in the sequence sees the updated game state
// from previous actions via the RandomStateTSequencer
// Create a game state with diplomacy offers
val testGameState = gameState.copy(
gameId = 12345L,
currentRoundId = 5
)
// This should not throw an exception even if offers get invalidated
// The RandomStateTSequencer ensures later actions see updated state
val results = EndDiplomacyResolutionPhaseAction(testGameState, applier)
.randomResults(functionalRandom)
.newValue
// Should always end with the end phase result
inside(results.last) { result =>
result.actionResultType shouldBe EndDiplomacyResolutionPhaseResultType
result.newRoundPhase shouldBe Some(RoundPhase.ReconResolution)
}
// The key improvement is that this no longer throws exceptions
// when invalidation occurs, because later actions now receive
// the updated game state via RandomStateTSequencer
}
}
@@ -1,7 +1,28 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.common.battalion_type.BattalionTypeId.{
HEAVY_CAVALRY,
LIGHT_INFANTRY
}
import net.eagle0.eagle.common.combat_unit.CombatUnit as CombatUnitProto
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType
import net.eagle0.eagle.common.round_phase.RoundPhase.UNCONTESTED_CONQUEST
import net.eagle0.eagle.internal.army.{
Attacking,
HostileArmyGroup,
Army as ArmyProto,
MovingArmy as MovingArmyProto
}
import net.eagle0.eagle.internal.battalion.Battalion
import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.internal.supplies.Supplies as SuppliesProto
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.library.actions.impl.{BattalionMap, HeroMap}
import net.eagle0.eagle.library.util.IDable.mapifyProvinces
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.types.{
EndUncontestedConquestPhaseResultType,
@@ -9,24 +30,9 @@ import net.eagle0.eagle.model.action_result.types.{
SentSuppliesResultType,
WithdrawalForTruceResultType
}
import net.eagle0.eagle.model.state.{
Army,
BattalionTypeId,
CombatUnit,
HostileArmyGroup,
HostileArmyGroupStatus,
MovingArmy,
Supplies
}
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.faction.FactionRelationship
import net.eagle0.eagle.model.state.faction.FactionRelationship.RelationshipLevel
import net.eagle0.eagle.model.state.hero.{Gender, Profession}
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.province.ProvinceOrderType
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.state.province.ProvinceOrderType.Entrust
import net.eagle0.eagle.model.state.{Army, CombatUnit, MovingArmy, Supplies}
import org.scalatest.Inside.inside
import org.scalatest.Inspectors.forExactly
import org.scalatest.LoneElement.convertToCollectionLoneElementWrapper
@@ -36,34 +42,18 @@ import org.scalatest.matchers.should.Matchers
class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
private val attackerId = 1
private val heroes: Map[Int, HeroC] = Map(
92 -> HeroC(
id = 92,
factionId = Some(attackerId),
nameTextId = "hn_92",
profession = Profession.Mage,
pronounGender = Gender.Male
),
102 -> HeroC(
id = 102,
factionId = Some(attackerId),
nameTextId = "hn_102",
profession = Profession.Ranger,
pronounGender = Gender.Female
)
private val heroes: Seq[Hero] = Vector(
Hero(id = 92, factionId = Some(attackerId), nameTextId = "hn_92"),
Hero(id = 102, factionId = Some(attackerId), nameTextId = "hn_102")
)
private val battalions: Map[Int, BattalionC] = Map(
34 -> BattalionC(
id = 34,
typeId = BattalionTypeId.LightInfantry,
size = 230
),
99 -> BattalionC(id = 99, typeId = BattalionTypeId.HeavyCavalry, size = 600)
private val battalions = Vector(
Battalion(id = 34, `type` = LIGHT_INFANTRY, size = 230),
Battalion(id = 99, `type` = HEAVY_CAVALRY, size = 600)
)
private val attackerCombatUnits = Vector(
CombatUnit(
CombatUnitProto(
factionId = attackerId,
heroId = 92,
battalionId = Some(34)
@@ -71,7 +61,7 @@ class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
)
private val moreAttackerCombatUnits = Vector(
CombatUnit(
CombatUnitProto(
factionId = attackerId,
heroId = 102,
battalionId = Some(99)
@@ -79,494 +69,382 @@ class PerformUncontestedConquestActionTest extends AnyFlatSpec with Matchers {
)
"Execute with no uncontested provinces" should "return only the END action" in {
val provinces = Map(
5 -> ProvinceC(
id = 5,
rulingFactionId = Some(15),
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UnknownOrderType,
rulingFactionHeroIds = Vector.empty,
incomingArmies = Vector(
MovingArmy(
id = 1,
army = Army(factionId = 1, units = attackerCombatUnits),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(0, 0),
suppliesLoss = 0.0,
startingPositionIndex = None
val startingState = GameState(
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentPhase = UNCONTESTED_CONQUEST,
provinces = Map(
5 -> Province(
id = 5,
rulingFactionId = Some(15),
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UNKNOWN_ORDERS,
rulingFactionHeroIds = Vector.empty,
incomingArmies = Vector(
MovingArmyProto(
army =
Some(ArmyProto(factionId = 1, units = attackerCombatUnits)),
arrivalRound = 9,
originProvince = 17,
supplies = None
)
)
)
)
)
val factions = Map(
1 -> FactionC(
id = 1,
factionHeadId = 92,
name = "Faction 1",
leaderIds = Vector(92)
),
2 -> FactionC(
id = 2,
factionHeadId = 102,
name = "Faction 2",
leaderIds = Vector(102)
factions = Map(
1 -> Faction(id = 1),
2 -> Faction(id = 2)
)
)
val results = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
).results
val results = PerformUncontestedConquestAction(startingState).results
results.loneElement.actionResultType.shouldBe(
EndUncontestedConquestPhaseResultType
)
results.loneElement.actionResultType shouldBe EndUncontestedConquestPhaseResultType
}
"Execute against an undefended province" should "return an uncontested conquest" in {
val provinces = Map(
5 -> ProvinceC(
id = 5,
rulingFactionId = None,
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UnknownOrderType,
rulingFactionHeroIds = Vector.empty,
hostileArmies = Vector(
HostileArmyGroup(
factionId = 1,
armies = Vector(
MovingArmy(
id = 1,
army = Army(factionId = 1, units = attackerCombatUnits),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(0, 0),
suppliesLoss = 0.0,
startingPositionIndex = None
val startingState = GameState(
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentPhase = UNCONTESTED_CONQUEST,
provinces = Map(
5 -> Province(
id = 5,
rulingFactionId = None,
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UNKNOWN_ORDERS,
rulingFactionHeroIds = Vector.empty,
hostileArmies = Vector(
HostileArmyGroup(
factionId = 1,
status = Attacking(),
armies = Vector(
MovingArmyProto(
army =
Some(ArmyProto(factionId = 1, units = attackerCombatUnits)),
arrivalRound = 9,
originProvince = 17,
supplies = None
)
)
),
status = HostileArmyGroupStatus.Attacking
)
)
)
)
)
val factions = Map(
1 -> FactionC(
id = 1,
factionHeadId = 92,
name = "Faction 1",
leaderIds = Vector(92)
),
2 -> FactionC(
id = 2,
factionHeadId = 102,
name = "Faction 2",
leaderIds = Vector(102)
factions = Map(
1 -> Faction(id = 1),
2 -> Faction(id = 2)
)
)
val results = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
).results
val results = PerformUncontestedConquestAction(startingState).results
results.size.shouldBe(3)
results.head.actionResultType.shouldBe(SentSuppliesResultType)
results(1).actionResultType.shouldBe(ProvinceConqueredResultType)
results(2).actionResultType.shouldBe(EndUncontestedConquestPhaseResultType)
results.foreach { ar =>
ar.newBattle.isEmpty.shouldBe(true)
}
results.size shouldBe 3
results.head.actionResultType shouldBe SentSuppliesResultType
results(1).actionResultType shouldBe ProvinceConqueredResultType
results(2).actionResultType shouldBe EndUncontestedConquestPhaseResultType
results.forall(art =>
ActionResultProtoConverter.toProto(art).newBattle.isEmpty
) shouldBe true
}
it should "add the attacking army's food and gold to the province" in {
val attackingArmy = HostileArmyGroup(
factionId = 1,
status = Attacking(),
armies = Vector(
MovingArmy(
id = 1,
army = Army(
factionId = 1,
units = attackerCombatUnits
MovingArmyProto(
army = Some(
ArmyProto(
factionId = 1,
units = attackerCombatUnits
)
),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(200, 300),
suppliesLoss = 0.0,
startingPositionIndex = None
originProvince = 17,
supplies = Some(SuppliesProto(gold = 200, food = 300))
)
)
)
val startingState = GameState(
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentPhase = UNCONTESTED_CONQUEST,
provinces = Map(
5 -> Province(
id = 5,
hostileArmies = Vector(attackingArmy),
gold = 125,
food = 256
)
),
status = HostileArmyGroupStatus.Attacking
)
val provinces = Map(
5 -> ProvinceC(
id = 5,
hostileArmies = Vector(attackingArmy),
gold = 125,
food = 256
factions = Map(
1 -> Faction(id = 1),
2 -> Faction(id = 2)
)
)
val factions = Map(
1 -> FactionC(
id = 1,
factionHeadId = 92,
name = "Faction 1",
leaderIds = Vector(92)
),
2 -> FactionC(
id = 2,
factionHeadId = 102,
name = "Faction 2",
leaderIds = Vector(102)
)
)
val results = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
).results
val results = PerformUncontestedConquestAction(startingState).results
inside(results.head.changedProvinces.head) { case cp: ChangedProvinceC =>
cp.provinceId.shouldBe(5)
cp.goldDelta.shouldBe(Some(200))
cp.foodDelta.shouldBe(Some(300))
cp.provinceId shouldBe 5
cp.goldDelta shouldBe Some(200)
cp.foodDelta shouldBe Some(300)
}
}
"attacking with two armies from the same faction" should "add both armies to the province" in {
val provinces = Map(
5 -> ProvinceC(
id = 5,
rulingFactionId = None,
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UnknownOrderType,
rulingFactionHeroIds = Vector.empty,
hostileArmies = Vector(
HostileArmyGroup(
factionId = 1,
armies = Vector(
MovingArmy(
id = 17,
army = Army(factionId = 1, units = attackerCombatUnits),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(300, 200),
suppliesLoss = 0.0,
startingPositionIndex = None
),
MovingArmy(
id = 717,
army = Army(factionId = 1, units = moreAttackerCombatUnits),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 27,
supplies = Supplies(500, 400),
suppliesLoss = 0.0,
startingPositionIndex = None
val startingState = GameState(
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentPhase = UNCONTESTED_CONQUEST,
provinces = Map(
5 -> Province(
id = 5,
rulingFactionId = None,
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UNKNOWN_ORDERS,
rulingFactionHeroIds = Vector.empty,
hostileArmies = Vector(
HostileArmyGroup(
factionId = 1,
status = Attacking(),
armies = Vector(
MovingArmyProto(
id = 17,
army =
Some(ArmyProto(factionId = 1, units = attackerCombatUnits)),
arrivalRound = 9,
originProvince = 17,
supplies = Some(SuppliesProto(food = 200, gold = 300))
),
MovingArmyProto(
id = 717,
army = Some(
ArmyProto(factionId = 1, units = moreAttackerCombatUnits)
),
arrivalRound = 9,
originProvince = 27,
supplies = Some(SuppliesProto(food = 400, gold = 500))
)
)
),
status = HostileArmyGroupStatus.Attacking
)
)
)
)
)
val factions = Map(
1 -> FactionC(
id = 1,
factionHeadId = 92,
name = "Faction 1",
leaderIds = Vector(92)
),
2 -> FactionC(
id = 2,
factionHeadId = 102,
name = "Faction 2",
leaderIds = Vector(102)
factions = Map(
1 -> Faction(id = 1),
2 -> Faction(id = 2)
)
)
val results = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
).results
val results =
PerformUncontestedConquestAction(startingState).results
results.size.shouldBe(3)
results.head.actionResultType.shouldBe(SentSuppliesResultType)
results.head.changedProvinces.should(have).size(1)
results.head.changedProvinces.head.shouldBe(
ChangedProvinceC(
provinceId = 5,
foodDelta = Some(600),
goldDelta = Some(800),
removedHostileArmyFactionIds = Vector(1)
)
results.size shouldBe 3
results.head.actionResultType shouldBe SentSuppliesResultType
results.head.changedProvinces should have size 1
results.head.changedProvinces.head shouldBe ChangedProvinceC(
provinceId = 5,
foodDelta = Some(600),
goldDelta = Some(800),
removedHostileArmyFactionIds = Vector(1)
)
results(1).actionResultType.shouldBe(ProvinceConqueredResultType)
results(1).changedProvinces.should(have).size(1)
results(1).changedProvinces.head.shouldBe(
ChangedProvinceC(
provinceId = 5,
newRulingFactionHeroIds = Vector(92, 102),
newBattalionIds = Vector(34, 99),
newRulingFactionId = Some(attackerId),
removedHostileArmyFactionIds = Vector(attackerId),
supportDelta = Some(-0.0),
newProvinceOrders = Some(Entrust),
clearDefendingArmy = true
)
results(1).actionResultType shouldBe ProvinceConqueredResultType
results(1).changedProvinces should have size 1
results(1).changedProvinces.head shouldBe ChangedProvinceC(
provinceId = 5,
newRulingFactionHeroIds = Vector(92, 102),
newBattalionIds = Vector(34, 99),
newRulingFactionId = Some(attackerId),
removedHostileArmyFactionIds = Vector(attackerId),
supportDelta = Some(-0.0),
newProvinceOrders = Some(Entrust),
clearDefendingArmy = true
)
results(2).actionResultType.shouldBe(EndUncontestedConquestPhaseResultType)
results.foreach { ar =>
ar.newBattle.isEmpty.shouldBe(true)
}
results(2).actionResultType shouldBe EndUncontestedConquestPhaseResultType
results.forall(art =>
ActionResultProtoConverter.toProto(art).newBattle.isEmpty
) shouldBe true
}
"attacking with two armies from different factions" should "fail" in {
val provinces = Map(
5 -> ProvinceC(
id = 5,
rulingFactionId = None,
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UnknownOrderType,
rulingFactionHeroIds = Vector.empty,
hostileArmies = Vector(
HostileArmyGroup(
factionId = 1,
armies = Vector(
MovingArmy(
id = 17,
army = Army(factionId = 1, units = attackerCombatUnits),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(300, 200),
suppliesLoss = 0.0,
startingPositionIndex = None
val startingState = GameState(
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentPhase = UNCONTESTED_CONQUEST,
provinces = Map(
5 -> Province(
id = 5,
rulingFactionId = None,
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UNKNOWN_ORDERS,
rulingFactionHeroIds = Vector.empty,
hostileArmies = Vector(
HostileArmyGroup(
factionId = 1,
status = Attacking(),
armies = Vector(
MovingArmyProto(
id = 17,
army =
Some(ArmyProto(factionId = 1, units = attackerCombatUnits)),
arrivalRound = 9,
originProvince = 17,
supplies = Some(SuppliesProto(food = 200, gold = 300))
)
)
),
status = HostileArmyGroupStatus.Attacking
),
HostileArmyGroup(
factionId = 2,
armies = Vector(
MovingArmy(
id = 717,
army = Army(factionId = 2, units = moreAttackerCombatUnits),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 27,
supplies = Supplies(500, 400),
suppliesLoss = 0.0,
startingPositionIndex = None
HostileArmyGroup(
factionId = 2,
status = Attacking(),
armies = Vector(
MovingArmyProto(
id = 717,
army = Some(
ArmyProto(factionId = 2, units = moreAttackerCombatUnits)
),
arrivalRound = 9,
originProvince = 27,
supplies = Some(SuppliesProto(food = 400, gold = 500))
)
)
),
status = HostileArmyGroupStatus.Attacking
)
)
)
)
)
val factions = Map(
1 -> FactionC(
id = 1,
factionHeadId = 92,
name = "Faction 1",
leaderIds = Vector(92)
),
2 -> FactionC(
id = 2,
factionHeadId = 102,
name = "Faction 2",
leaderIds = Vector(102)
factions = Map(
1 -> Faction(id = 1),
2 -> Faction(id = 2)
)
)
val ex = the[EagleInternalException] thrownBy {
PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
).results
PerformUncontestedConquestAction(startingState).results
}
ex.getMessage.shouldBe(
"requirement failed: Attacking armies Vector(1, 2) are not mutually allied"
)
ex.getMessage shouldBe "requirement failed: Attacking armies Vector(1, 2) are not mutually allied"
}
"attacking with two armies from truce-ing factions" should "bounce the armies" in {
val faction2CombatUnits = Vector(
CombatUnit(
factionId = 2,
heroId = 102,
battalionId = Some(99)
)
)
val provinces = Map(
17 -> ProvinceC(id = 17, rulingFactionId = Some(1)),
27 -> ProvinceC(id = 27, rulingFactionId = Some(2)),
5 -> ProvinceC(
id = 5,
rulingFactionId = None,
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UnknownOrderType,
rulingFactionHeroIds = Vector.empty,
hostileArmies = Vector(
HostileArmyGroup(
factionId = 1,
armies = Vector(
MovingArmy(
id = 17,
army = Army(factionId = 1, units = attackerCombatUnits),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(300, 200),
suppliesLoss = 0.0,
startingPositionIndex = None
val startingState = GameState(
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentPhase = UNCONTESTED_CONQUEST,
provinces = mapifyProvinces(
Province(id = 17, rulingFactionId = Some(1)),
Province(id = 27, rulingFactionId = Some(2)),
Province(
id = 5,
rulingFactionId = None,
rulingHeroId = None,
provinceOrders = ProvinceOrderType.UNKNOWN_ORDERS,
rulingFactionHeroIds = Vector.empty,
hostileArmies = Vector(
HostileArmyGroup(
factionId = 1,
status = Attacking(),
armies = Vector(
MovingArmyProto(
id = 17,
army =
Some(ArmyProto(factionId = 1, units = attackerCombatUnits)),
arrivalRound = 9,
originProvince = 17,
supplies = Some(SuppliesProto(food = 200, gold = 300))
)
)
),
status = HostileArmyGroupStatus.Attacking
),
HostileArmyGroup(
factionId = 2,
armies = Vector(
MovingArmy(
id = 717,
army = Army(factionId = 2, units = faction2CombatUnits),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 27,
supplies = Supplies(500, 400),
suppliesLoss = 0.0,
startingPositionIndex = None
HostileArmyGroup(
factionId = 2,
status = Attacking(),
armies = Vector(
MovingArmyProto(
id = 717,
army = Some(
ArmyProto(factionId = 2, units = moreAttackerCombatUnits)
),
arrivalRound = 9,
originProvince = 27,
supplies = Some(SuppliesProto(food = 400, gold = 500))
)
)
),
status = HostileArmyGroupStatus.Attacking
)
)
)
)
val factions = Map(
1 -> FactionC(
id = 1,
factionHeadId = 92,
name = "Faction 1",
leaderIds = Vector(92),
factionRelationships = Vector(
FactionRelationship(
targetFactionId = 2,
relationshipLevel = RelationshipLevel.Truce
)
)
)
),
2 -> FactionC(
id = 2,
factionHeadId = 102,
name = "Faction 2",
leaderIds = Vector(102),
factionRelationships = Vector(
FactionRelationship(
targetFactionId = 1,
relationshipLevel = RelationshipLevel.Truce
factions = Map(
1 -> Faction(
id = 1,
factionRelationships = Vector(
FactionRelationship(
targetFactionId = 2,
relationshipLevel = FactionRelationship.RelationshipLevel.TRUCE
)
)
),
2 -> Faction(
id = 2,
factionRelationships = Vector(
FactionRelationship(
targetFactionId = 1,
relationshipLevel = FactionRelationship.RelationshipLevel.TRUCE
)
)
)
)
)
val result = PerformUncontestedConquestAction(
gameId = 12345L,
currentRoundId = 9,
currentDate = Date(year = 1, month = Date.Month.January),
provinces = provinces,
factions = factions,
heroes = heroes,
battalions = battalions
).results.head
val result = PerformUncontestedConquestAction(startingState).results.head
result.actionResultType.shouldBe(WithdrawalForTruceResultType)
result.changedProvinces.should(
contain(
ChangedProvinceC(
provinceId = 5,
removedHostileArmyFactionIds = Vector(1, 2)
)
result.actionResultType shouldBe WithdrawalForTruceResultType
result.changedProvinces should contain(
ChangedProvinceC(
provinceId = 5,
removedHostileArmyFactionIds = Vector(1, 2)
)
)
forExactly(1, result.changedProvinces) { case cp: ChangedProvinceC =>
cp.provinceId.shouldBe(17)
cp.provinceId shouldBe 17
inside(cp.newIncomingArmies.loneElement) { case ma: MovingArmy =>
ma.id.shouldBe(17)
ma.originProvinceId.shouldBe(5)
ma.arrivalRound.shouldBe(10)
ma.supplies.shouldBe(Supplies(300, 200))
ma.startingPositionIndex.shouldBe(None)
ma.id shouldBe 17
ma.originProvinceId shouldBe 5
ma.arrivalRound shouldBe 10
ma.supplies shouldBe Supplies(food = 200, gold = 300)
ma.startingPositionIndex shouldBe None
inside(ma.army) { case army: Army =>
army.factionId.shouldBe(1)
army.units.loneElement.shouldBe(
CombatUnit(
heroId = 92,
battalionId = Some(34),
factionId = 1
)
army.factionId shouldBe 1
army.units.loneElement shouldBe CombatUnit(
heroId = 92,
battalionId = Some(34),
factionId = attackerId
)
}
}
}
forExactly(1, result.changedProvinces) { case cp: ChangedProvinceC =>
cp.provinceId.shouldBe(27)
cp.provinceId shouldBe 27
inside(cp.newIncomingArmies.loneElement) { case ma: MovingArmy =>
ma.id.shouldBe(717)
ma.originProvinceId.shouldBe(5)
ma.arrivalRound.shouldBe(10)
ma.supplies.shouldBe(Supplies(500, 400))
ma.startingPositionIndex.shouldBe(None)
ma.id shouldBe 717
ma.originProvinceId shouldBe 5
ma.arrivalRound shouldBe 10
ma.supplies shouldBe Supplies(food = 400, gold = 500)
ma.startingPositionIndex shouldBe None
inside(ma.army) { case army: Army =>
army.factionId.shouldBe(2)
army.units.loneElement.shouldBe(
CombatUnit(
heroId = 102,
battalionId = Some(99),
factionId = 2
)
army.factionId shouldBe 2
army.units.loneElement shouldBe CombatUnit(
heroId = 102,
battalionId = Some(99),
factionId = attackerId
)
}
}
@@ -272,14 +272,8 @@ class PerformVassalCommandsPhaseActionTest
BattalionWithFoodCost(battalionId = 2, monthlyFood = 50),
BattalionWithFoodCost(battalionId = 3, monthlyFood = 50)
),
availableDestinationProvinces = actingProvince.neighbors
.map(_.provinceId)
.map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
)
availableDestinationProvinces =
actingProvince.neighbors.map(_.provinceId)
)
)
)
@@ -1,10 +1,9 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.common_unit.UnitStatus
import net.eagle0.eagle.common.date.Date as DateProto
import net.eagle0.eagle.common.hero_backstory_version.BackstoryVersion
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType
import net.eagle0.eagle.common.profession.Profession
import net.eagle0.eagle.common.gender.Gender
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.{
RECRUITMENT_STATUS_HAS_QUEST,
RECRUITMENT_STATUS_PRISONER
@@ -19,16 +18,13 @@ import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
UNAFFILIATED_HERO_PRISONER,
UNAFFILIATED_HERO_RESIDENT
}
import net.eagle0.eagle.internal.battalion.Battalion
import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.{Neighbor, Province}
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero as UnaffiliatedHeroProto
import net.eagle0.eagle.internal.battalion.Battalion
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.library.actions.impl.{BattalionMap, HeroMap}
import net.eagle0.eagle.library.settings.{
FreedPrisonerLoyalty,
@@ -56,16 +52,13 @@ import net.eagle0.eagle.model.action_result.concrete.{
import net.eagle0.eagle.model.action_result.types.ProvinceConqueredResultType
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
import net.eagle0.eagle.model.proto_converters.diplomacy_offer.DiplomacyOfferConverter
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
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.BattalionConverter
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.diplomacy_offer.RansomOfferDetails.PrisonerToBeRansomed
import net.eagle0.eagle.model.state.diplomacy_offer.RansomOffer
import net.eagle0.eagle.model.state.diplomacy_offer.status.{
Unresolved,
Accepted,
Rejected,
Invalidated
}
import net.eagle0.eagle.model.state.hero.ReleasedByProvinceCaptureBackstoryEvent
@@ -78,118 +71,36 @@ import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
private def createProvinceConqueredAction(
gameState: GameState = startingState,
province: Province = conqueredProvince,
conquerors: Vector[ResolvedEagleUnit] = notFledAttackers,
defenders: Vector[ResolvedEagleUnit] = Vector.empty
): ProvinceConqueredAction = {
ProvinceConqueredAction(
gameId = gameState.gameId,
currentRoundId = gameState.currentRoundId,
currentDate = DateConverter.fromProto(gameState.currentDate),
conqueredProvince = ProvinceConverter.fromProto(province),
notFledConquerors = conquerors,
notFledDefenders = defenders,
destroyedBattalions = Vector.empty,
defendingFactionId = province.rulingFactionId,
factions =
gameState.factions.view.mapValues(FactionConverter.fromProto).toMap,
heroes = gameState.heroes.view.mapValues(HeroConverter.fromProto).toMap,
battalions =
gameState.battalions.view.mapValues(BattalionConverter.fromProto).toMap,
provinces =
gameState.provinces.view.mapValues(ProvinceConverter.fromProto).toMap
)
}
val attackingFactionId = 8
private val attackingHero1 =
Hero(
id = 23,
factionId = Some(attackingFactionId),
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_MALE
)
Hero(id = 23, factionId = Some(attackingFactionId))
private val attackingHero2 =
Hero(
id = 24,
factionId = Some(attackingFactionId),
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_FEMALE
)
Hero(id = 24, factionId = Some(attackingFactionId))
private val attackingBattalion1 = Battalion(id = 18)
private val attackingBattalion2 = Battalion(id = 17)
private val notFledAttackers = Vector(
ResolvedEagleUnit(
hero = HeroC(
id = 23,
factionId = Some(attackingFactionId),
profession = net.eagle0.eagle.model.state.hero.Profession.Champion,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Male
),
battalion = Some(
BattalionC(
id = 18,
typeId = net.eagle0.eagle.model.state.BattalionTypeId.LightInfantry,
size = 100,
training = 50.0,
armament = 50.0,
name = "Test Battalion 1"
)
),
status = UnitStatus.Normal
hero = attackingHero1,
battalion = attackingBattalion1,
status = UnitStatus.NORMAL_UNIT
),
ResolvedEagleUnit(
hero = HeroC(
id = 24,
factionId = Some(attackingFactionId),
profession = net.eagle0.eagle.model.state.hero.Profession.Champion,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Female
),
battalion = Some(
BattalionC(
id = 17,
typeId = net.eagle0.eagle.model.state.BattalionTypeId.LightInfantry,
size = 100,
training = 50.0,
armament = 50.0,
name = "Test Battalion 2"
)
),
status = UnitStatus.Normal
hero = attackingHero2,
battalion = attackingBattalion2,
status = UnitStatus.NORMAL_UNIT
)
)
private val defendingFactionId = 3
private val defendingHero =
Hero(
id = 55,
factionId = Some(defendingFactionId),
profession = Profession.PALADIN,
pronounGender = Gender.GENDER_MALE
)
Hero(id = 55, factionId = Some(defendingFactionId))
private val notFledDefenders = Vector(
ResolvedEagleUnit(
hero = HeroC(
id = 55,
factionId = Some(defendingFactionId),
profession = net.eagle0.eagle.model.state.hero.Profession.Paladin,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Male
),
battalion = Some(
BattalionC(
id = EagleUnit.defaultBattalionId,
typeId = net.eagle0.eagle.model.state.BattalionTypeId.LightInfantry,
size = 0,
training = 0.0,
armament = 0.0,
name = "Default Battalion"
)
),
status = UnitStatus.Captured
hero = defendingHero,
battalion = EagleUnit.defaultBattalion,
status = UnitStatus.CAPTURED_UNIT
)
)
@@ -227,7 +138,14 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
)
"execute" should "set the basics in the result" in {
val result = createProvinceConqueredAction().immediateExecute
val result = ProvinceConqueredAction(
startingState = startingState,
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = Vector.empty,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
result.actionResultType shouldBe ProvinceConqueredResultType
result.provinceId shouldBe Some(conqueredProvince.id)
@@ -235,8 +153,13 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
it should "change the ruling player to the attacking army's player" in {
val result = createProvinceConqueredAction(
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingState,
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
inside(result.changedProvinces.head) {
@@ -246,8 +169,13 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
it should "change the ruling player heroes to the attacking army's heroes" in {
val result = createProvinceConqueredAction(
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingState,
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
inside(result.changedProvinces.head) {
@@ -261,8 +189,13 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
it should "change the battalions to the attacking army's battalions" in {
val result = createProvinceConqueredAction(
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingState,
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
inside(result.changedProvinces.head) {
@@ -276,8 +209,13 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
it should "set the defending player heroes as unaffiliated prisoners" in {
val result = createProvinceConqueredAction(
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingState,
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
inside(result.changedProvinces.head) {
@@ -347,9 +285,13 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
).map(DiplomacyOfferConverter.toProto)
)
val result = createProvinceConqueredAction(
gameState = gsWithRansomOffers,
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = gsWithRansomOffers,
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
forExactly(1, result.changedFactions) {
@@ -397,24 +339,17 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
_.provinces(
conqueredProvince.id
) := conqueredProvinceWithImprisonedHeroesFromAttacker,
_.heroes(97) := Hero(
id = 97,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_MALE
),
_.heroes(94) := Hero(
id = 94,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_FEMALE
)
_.heroes(97) := Hero(id = 97, factionId = None),
_.heroes(94) := Hero(id = 94, factionId = None)
)
val result = createProvinceConqueredAction(
gameState = startingStateWithImprisonedHeroes,
province = conqueredProvinceWithImprisonedHeroesFromAttacker,
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingStateWithImprisonedHeroes,
conqueredProvince = conqueredProvinceWithImprisonedHeroesFromAttacker,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
inside(result.changedProvinces.head) {
@@ -486,24 +421,17 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
val startingStateWithImprisonedHeroes = startingState.update(
_.provinces(conqueredProvince.id) := conqueredProvinceWithQuests,
_.heroes(97) := Hero(
id = 97,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_MALE
),
_.heroes(94) := Hero(
id = 94,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_FEMALE
)
_.heroes(97) := Hero(id = 97, factionId = None),
_.heroes(94) := Hero(id = 94, factionId = None)
)
val result = createProvinceConqueredAction(
gameState = startingStateWithImprisonedHeroes,
province = conqueredProvinceWithQuests,
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingStateWithImprisonedHeroes,
conqueredProvince = conqueredProvinceWithQuests,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
inside(result.changedProvinces.head) {
@@ -525,8 +453,13 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
it should "remove the conquering fid from the attacking factions list" in {
val result = createProvinceConqueredAction(
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingState,
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
inside(result.changedProvinces.head) {
@@ -538,8 +471,13 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
it should "remove any scouting info from factions" in {
val result = createProvinceConqueredAction(
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingState,
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
result.changedFactions shouldBe Vector(
@@ -559,12 +497,15 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
it should "remove the focus province if it matches the conquered province" in {
val result = createProvinceConqueredAction(
gameState = startingState.update(
val result = ProvinceConqueredAction(
startingState = startingState.update(
_.factions(defendingFactionId).focusProvinceId := conqueredProvince.id
),
province = conqueredProvince,
defenders = notFledDefenders
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
result.changedFactions should contain(
@@ -576,16 +517,19 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
it should "not remove the focus province if it's another province" in {
val result = createProvinceConqueredAction(
gameState = startingState.update(
val result = ProvinceConqueredAction(
startingState = startingState.update(
_.factions(defendingFactionId).focusProvinceId := 12,
_.provinces(12) := Province(
id = 12,
rulingFactionId = Some(defendingFactionId)
)
),
province = conqueredProvince,
defenders = notFledDefenders
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
result.changedFactions should not contain (
@@ -597,8 +541,14 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
"execute" should "include a ProvinceExpansion notification if the province is not already occupied" in {
val result = createProvinceConqueredAction(
province = conqueredProvince.update(_.optionalRulingFactionId := None)
val result = ProvinceConqueredAction(
startingState = startingState,
conqueredProvince =
conqueredProvince.update(_.optionalRulingFactionId := None),
notFledConquerors = notFledAttackers,
notFledDefenders = Vector.empty,
destroyedBattalions = Vector.empty,
defendingFactionId = None
).immediateExecute
forExactly(1, result.newNotifications) { case notification: NotificationC =>
@@ -611,7 +561,14 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
}
it should "include a ProvinceConquered notification if the province was occupied" in {
val result = createProvinceConqueredAction().immediateExecute
val result = ProvinceConqueredAction(
startingState = startingState,
conqueredProvince = conqueredProvince,
notFledConquerors = notFledAttackers,
notFledDefenders = Vector.empty,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
forExactly(1, result.newNotifications) { case notification: NotificationC =>
notification.deferred shouldBe true
@@ -651,33 +608,22 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
_.heroes(97) := Hero(
id = 97,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_MALE,
backstoryVersions = Vector(
BackstoryVersion(
textId = "backstory_97",
date = Some(DateProto(year = 2020, month = 1))
)
)
backstoryVersions = Vector(BackstoryVersion(textId = "backstory_97"))
),
_.heroes(94) := Hero(
id = 94,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_FEMALE,
backstoryVersions = Vector(
BackstoryVersion(
textId = "backstory_94",
date = Some(DateProto(year = 2020, month = 1))
)
)
backstoryVersions = Vector(BackstoryVersion(textId = "backstory_94"))
)
)
val result = createProvinceConqueredAction(
gameState = startingStateWithPrisoners,
province = conqueredProvinceWithPrisoners,
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingStateWithPrisoners,
conqueredProvince = conqueredProvinceWithPrisoners,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
inside(result.changedProvinces.head) {
@@ -714,33 +660,22 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
_.heroes(97) := Hero(
id = 97,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_MALE,
backstoryVersions = Vector(
BackstoryVersion(
textId = "backstory_97",
date = Some(DateProto(year = 2020, month = 1))
)
)
backstoryVersions = Vector(BackstoryVersion(textId = "backstory_97"))
),
_.heroes(94) := Hero(
id = 94,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_FEMALE,
backstoryVersions = Vector(
BackstoryVersion(
textId = "backstory_94",
date = Some(DateProto(year = 2020, month = 1))
)
)
backstoryVersions = Vector(BackstoryVersion(textId = "backstory_94"))
)
)
val result = createProvinceConqueredAction(
gameState = startingStateWithPrisoners,
province = conqueredProvinceWithPrisoners,
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingStateWithPrisoners,
conqueredProvince = conqueredProvinceWithPrisoners,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
forExactly(1, result.changedHeroes) { case changedHero: ChangedHeroC =>
@@ -820,26 +755,12 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
_.heroes(97) := Hero(
id = 97,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_MALE,
backstoryVersions = Vector(
BackstoryVersion(
textId = "backstory_97",
date = Some(DateProto(year = 2020, month = 1))
)
)
backstoryVersions = Vector(BackstoryVersion(textId = "backstory_97"))
),
_.heroes(94) := Hero(
id = 94,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_FEMALE,
backstoryVersions = Vector(
BackstoryVersion(
textId = "backstory_94",
date = Some(DateProto(year = 2020, month = 1))
)
)
backstoryVersions = Vector(BackstoryVersion(textId = "backstory_94"))
),
_.provinces(alliedPid) := Province(
id = alliedPid,
@@ -851,10 +772,13 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
)
)
val result = createProvinceConqueredAction(
gameState = startingStateWithPrisoners,
province = conqueredProvinceWithPrisoners,
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingStateWithPrisoners,
conqueredProvince = conqueredProvinceWithPrisoners,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
forExactly(1, result.changedProvinces) {
@@ -915,33 +839,22 @@ class ProvinceConqueredActionTest extends AnyFlatSpec with Matchers {
_.heroes(97) := Hero(
id = 97,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_MALE,
backstoryVersions = Vector(
BackstoryVersion(
textId = "backstory_97",
date = Some(DateProto(year = 2020, month = 1))
)
)
backstoryVersions = Vector(BackstoryVersion(textId = "backstory_97"))
),
_.heroes(94) := Hero(
id = 94,
factionId = None,
profession = Profession.CHAMPION,
pronounGender = Gender.GENDER_FEMALE,
backstoryVersions = Vector(
BackstoryVersion(
textId = "backstory_94",
date = Some(DateProto(year = 2020, month = 1))
)
)
backstoryVersions = Vector(BackstoryVersion(textId = "backstory_94"))
)
)
val result = createProvinceConqueredAction(
gameState = startingStateWithPrisoners,
province = conqueredProvinceWithPrisoners,
defenders = notFledDefenders
val result = ProvinceConqueredAction(
startingState = startingStateWithPrisoners,
conqueredProvince = conqueredProvinceWithPrisoners,
notFledConquerors = notFledAttackers,
notFledDefenders = notFledDefenders,
destroyedBattalions = Vector.empty,
defendingFactionId = Some(defendingFactionId)
).immediateExecute
result.changedHeroes should contain allOf (
@@ -1,6 +1,6 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.common.common_unit.UnitStatus
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.internal.army.{Army, MovingArmy}
import net.eagle0.eagle.internal.battalion.Battalion
@@ -15,9 +15,7 @@ import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedPro
import net.eagle0.eagle.model.action_result.concrete.NotificationC
import net.eagle0.eagle.model.action_result.types.ProvinceHeldResultType
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.state.CapturedHero
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.shardok_interface.ResolvedEagleUnit
import org.scalatest.Inspectors.forExactly
import org.scalatest.flatspec.AnyFlatSpec
@@ -50,22 +48,14 @@ class ProvinceHeldActionTest extends AnyFlatSpec with Matchers {
private val notFledAttackers = Vector(
ResolvedEagleUnit(
hero = HeroC(
id = 5,
profession = net.eagle0.eagle.model.state.hero.Profession.Mage,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Female
),
battalion = None,
status = UnitStatus.Normal
hero = Hero(id = 5),
battalion = Battalion(id = 99),
status = UnitStatus.NORMAL_UNIT
),
ResolvedEagleUnit(
hero = HeroC(
id = 10,
profession = net.eagle0.eagle.model.state.hero.Profession.Ranger,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Male
),
battalion = None,
status = UnitStatus.Normal
hero = Hero(id = 10),
battalion = Battalion(id = 7),
status = UnitStatus.NORMAL_UNIT
)
)
@@ -1,305 +1,315 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.*
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.state.shardok_battle.{
BattleType,
VictoryCondition
import net.eagle0.eagle.library.actions.impl.WaitingAction
import net.eagle0.common.victory_condition.VictoryCondition.{
VICTORY_CONDITION_HOLDS_CRITICAL_TILES,
VICTORY_CONDITION_LAST_PLAYER_STANDING,
VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS
}
import net.eagle0.eagle.model.state.*
import net.eagle0.eagle.model.state.HostileArmyGroupStatus.{
import net.eagle0.eagle.common.action_result_type.ActionResultType.{
PROVINCE_CONQUERED,
SENT_SUPPLIES,
START_BATTLE
}
import net.eagle0.eagle.common.battalion_type.BattalionType
import net.eagle0.eagle.common.battalion_type.BattalionTypeId.LIGHT_INFANTRY
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.common.date.Date
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType
import net.eagle0.eagle.common.round_phase.RoundPhase.FOOD_CONSUMPTION
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.army.{
Army,
Attacking,
AwaitingDecision
AwaitingDecision,
HostileArmyGroup,
MovingArmy
}
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.hero.Gender.{Female, Male}
import net.eagle0.eagle.model.state.hero.Profession.NoProfession
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.province.{Neighbor, ProvinceOrderType}
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.internal.battalion.Battalion
import net.eagle0.eagle.internal.changed_province.ChangedProvince
import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.{Neighbor, Province}
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle.BattleType
import net.eagle0.eagle.internal.shardok_battle.{ShardokBattle, ShardokPlayer}
import net.eagle0.eagle.internal.supplies.Supplies
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplierImpl
import net.eagle0.eagle.library.actions.impl.{BattalionMap, HeroMap}
import net.eagle0.eagle.library.util.validations.TestingNoopValidator
import net.eagle0.eagle.shardok_interface.EagleUnit
import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.Inside.inside
class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
class RequestBattlesActionTest
extends AnyFlatSpec
with MockFactory
with Matchers {
import net.eagle0.common.ProtoMatchers.*
private val actionResultProtoApplier = new ActionResultProtoApplierImpl(
validator = TestingNoopValidator
)
private val gameId = 0xa878ab
private val attackerId = 1
private val defenderId = 2
private val heroes = Map[HeroId, HeroC](
92 -> HeroC(
id = 92,
factionId = Some(attackerId),
nameTextId = "hn_92",
profession = NoProfession,
pronounGender = Female
),
22 -> HeroC(
id = 22,
factionId = Some(defenderId),
nameTextId = "hn_22",
profession = NoProfession,
pronounGender = Male
)
private val heroes: Vector[Hero] = Vector(
Hero(id = 92, factionId = Some(attackerId), nameTextId = "hn_92"),
Hero(id = 22, factionId = Some(defenderId), nameTextId = "hn_22")
)
private val lightInfantryType = BattalionTypeId.LightInfantry
private val battalions = Map[BattalionId, BattalionC](
34 -> BattalionC(id = 34, typeId = lightInfantryType, size = 230),
33 -> BattalionC(id = 33, typeId = lightInfantryType, size = 210)
private val battalions = Vector(
Battalion(id = 34, `type` = LIGHT_INFANTRY, size = 230),
Battalion(id = 33, `type` = LIGHT_INFANTRY, size = 210)
)
private val attackerCombatUnits = Vector(
CombatUnit(factionId = attackerId, heroId = 92, battalionId = Some(34))
CombatUnit(
factionId = attackerId,
heroId = 92,
battalionId = Some(34)
)
)
private val defenderCombatUnits = Vector(
CombatUnit(factionId = defenderId, heroId = 22, battalionId = Some(33))
CombatUnit(
factionId = defenderId,
heroId = 22,
battalionId = Some(33)
)
)
private val battalionTypes = Map[BattalionTypeId, BattalionType](
lightInfantryType -> BattalionType(
typeId = lightInfantryType,
name = "Light Infantry",
capacity = 1000,
private val attackerExpandedUnits = Vector(
EagleUnit(
hero = Hero(id = 92, factionId = Some(attackerId), nameTextId = "hn_92"),
battalion = Battalion(id = 34, size = 230)
)
)
private val defenderExpandedUnits = Vector(
EagleUnit(
hero = Hero(id = 22, factionId = Some(defenderId), nameTextId = "hn_22"),
battalion = Battalion(id = 33, size = 210)
)
)
private val battalionTypes = Vector(
BattalionType(
typeId = LIGHT_INFANTRY,
allowsCasting = true,
allowsStealth = true,
alwaysArcheryCapable = false,
baseCost = 10.0,
monthlyFoodCost = 0.05,
baseArmamentCostPerTroop = 1.0,
trainingDifficulty = 1.0,
minimumAgriculture = 0,
minimumEconomy = 0
monthlyFoodCost = 0.05
)
)
// Helper to create minimal faction
private def createFaction(id: FactionId, headId: HeroId): FactionC =
FactionC(
id = id,
factionHeadId = headId,
name = s"Faction $id",
leaderIds = Vector(headId),
prestigeModifiers = Vector.empty,
factionRelationships = Vector.empty,
incomingDiplomacyOffers = Vector.empty,
focusProvinceId = None,
reconnedProvinces = Vector.empty,
lastActedProvinceIdThisRound = 1,
lastOutgoingTruceOfferRounds = Vector.empty,
lastOutgoingAllianceOfferRounds = Vector.empty,
lastOutgoingInvitationRounds = Vector.empty,
lastOutgoingRansomOfferRounds = Vector.empty,
earliestRoundForInvitation = 1
"Execute against a defended province" should "return a started battle" in {
val defendingArmy = Army(
factionId = 3,
units = defenderCombatUnits
)
"Execute against a defended province" should "return a started battle" in {
val defendingArmy = Army(factionId = 3, units = defenderCombatUnits)
val attackingArmy = HostileArmyGroup(
status = Attacking,
status = Attacking(),
factionId = 1,
armies = Vector(
MovingArmy(
id = 1, // MovingArmyId is just an Int
army = Army(
factionId = 1,
units = attackerCombatUnits,
fleeProvinceId = Some(17)
army = Some(
Army(
factionId = 1,
units = attackerCombatUnits,
fleeProvinceId = Some(17)
)
),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(gold = 0, food = 1000),
suppliesLoss = 0.0,
startingPositionIndex = None
originProvince = 17,
supplies = Some(Supplies(food = 1000))
)
)
)
val provinces = Map[ProvinceId, ProvinceC](
5 -> ProvinceC(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
neighbors =
Vector(Neighbor(provinceId = 17, startingPositionIndex = 4)),
provinceOrders = ProvinceOrderType.Entrust,
rulingFactionHeroIds = Vector(22),
defendingArmy = Some(defendingArmy),
hostileArmies = Vector(attackingArmy),
hexMapName = "Lower_Lake",
food = 1000
)
)
val factions = Map[FactionId, FactionC](
1 -> createFaction(1, 92),
3 -> createFaction(3, 22)
)
val action = RequestBattlesAction(
val startingState = GameState(
currentDate = Some(Date(year = 1501, month = 6)),
randomSeed = 1L,
gameId = 0xcafe,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0x17,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentPhase = FOOD_CONSUMPTION,
provinces = Map(
5 -> Province(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
neighbors =
Vector(Neighbor(provinceId = 17, startingPositionIndex = 4)),
provinceOrders = ProvinceOrderType.ENTRUST,
rulingFactionHeroIds = Vector(22),
defendingArmy = Some(defendingArmy),
hostileArmies = Vector(attackingArmy),
hexMapName = "Lower_Lake",
food = 1000
)
),
factions = Map(
1 -> Faction(id = 1),
3 -> Faction(id = 3)
),
battalionTypes = battalionTypes
)
val results = action.results
val action = RequestBattlesAction(startingState)
val results = action.resultsOfExecute()
results.size shouldBe 1
val battleResult = results.head
battleResult.newBattle should not be empty
val battle = battleResult.newBattle.get
battle.shardokGameId shouldBe "cafe_18_a884c092"
battle.battleIndex shouldBe 24
battle.battleType shouldBe BattleType.AssaultProvince
battle.eagleGameId shouldBe 0xcafe
battle.hexMapName shouldBe "Lower_Lake"
battle.defenderProvince shouldBe 5
battle.roundStarted shouldBe 9
battle.players should have size 2
val attacker = battle.players.find(_.eagleFid == 1).get
attacker.isDefender shouldBe false
attacker.armyGroup shouldBe Some(attackingArmy)
attacker.food shouldBe 12
attacker.victoryConditions should contain(
VictoryCondition.HoldsCriticalTiles
)
attacker.victoryConditions should contain(
VictoryCondition.LastPlayerStanding
)
val defender = battle.players.find(_.eagleFid == 3).get
defender.eagleFid shouldBe 3
defender.isDefender shouldBe true
defender.food shouldBe 11
defender.victoryConditions should contain(
VictoryCondition.LastPlayerStanding
)
defender.victoryConditions should contain(
VictoryCondition.WinAfterMaxRounds
results.head.`type` shouldBe START_BATTLE
results.head.newBattle.get should equalProto(
ShardokBattle(
shardokGameId = "cafe_18_7c083557",
battleIndex = 24,
battleType = BattleType.BATTLE_TYPE_ASSAULT_PROVINCE,
eagleGameId = 0xcafe,
hexMapName = "Lower_Lake",
players = Vector(
ShardokPlayer(
eagleFid = 1,
isDefender = false,
armyGroup = Some(attackingArmy),
food = 12,
victoryConditions = Vector(
VICTORY_CONDITION_HOLDS_CRITICAL_TILES,
VICTORY_CONDITION_LAST_PLAYER_STANDING
)
),
ShardokPlayer(
eagleFid = 3,
isDefender = true,
armyGroup = Some(
HostileArmyGroup(
factionId = 3,
armies = Vector(MovingArmy(army = Some(defendingArmy)))
)
),
food = 11,
victoryConditions = Vector(
VICTORY_CONDITION_LAST_PLAYER_STANDING,
VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS
)
)
),
defenderProvince = 5,
roundStarted = 9
)
)
}
"Execute against an undefended province" should "return an uncontested conquest" in {
val attackingArmy = HostileArmyGroup(
factionId = 1,
status = Attacking,
status = Attacking(),
armies = Vector(
MovingArmy(
id = 1,
army = Army(
factionId = 1,
units = attackerCombatUnits,
fleeProvinceId = Some(17)
army = Some(
Army(
factionId = 1,
units = attackerCombatUnits,
fleeProvinceId = Some(17)
)
),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(gold = 0, food = 0),
suppliesLoss = 0.0,
startingPositionIndex = None
originProvince = 17
)
)
)
val provinces = Map[ProvinceId, ProvinceC](
5 -> ProvinceC(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
provinceOrders = ProvinceOrderType.Entrust,
rulingFactionHeroIds = Vector(22),
hostileArmies = Vector(attackingArmy)
)
)
val factions = Map[FactionId, FactionC](
1 -> createFaction(1, 92),
2 -> createFaction(2, 22)
)
val results = RequestBattlesAction(
gameId = 0x123,
val startingState = GameState(
currentDate = Some(Date(year = 1501, month = 6)),
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
currentPhase = FOOD_CONSUMPTION,
provinces = Map(
5 -> Province(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
provinceOrders = ProvinceOrderType.ENTRUST,
rulingFactionHeroIds = Vector(22),
hostileArmies = Vector(attackingArmy)
)
),
factions = Map(
1 -> Faction(id = 1),
2 -> Faction(id = 2)
),
battalionTypes = battalionTypes
).results
)
val results =
RequestBattlesAction(startingState)
.execute(actionResultProtoApplier)
.map(_.actionResult)
results.size shouldBe 2
// First result is sending supplies, second is province conquered
results.last.changedProvinces.exists(_.provinceId == 5) shouldBe true
results.head.`type` shouldBe SENT_SUPPLIES
results.last.`type` shouldBe PROVINCE_CONQUERED
results.last.province should contain(5)
results.forall(_.newBattle.isEmpty) shouldBe true
}
it should "throw if a hostile army is not Attacking" in {
val undecidedArmy = HostileArmyGroup(
factionId = 1,
status = AwaitingDecision,
status = AwaitingDecision(),
armies = Vector(
MovingArmy(
id = 1,
army = Army(
factionId = 1,
units = attackerCombatUnits,
fleeProvinceId = Some(17)
army = Some(
Army(
factionId = 1,
units = attackerCombatUnits,
fleeProvinceId = Some(17)
)
),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(gold = 0, food = 0),
suppliesLoss = 0.0,
startingPositionIndex = None
originProvince = 17
)
)
)
val provinces = Map[ProvinceId, ProvinceC](
5 -> ProvinceC(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
provinceOrders = ProvinceOrderType.Entrust,
rulingFactionHeroIds = Vector(22),
hostileArmies = Vector(undecidedArmy)
)
)
val factions = Map[FactionId, FactionC](
1 -> createFaction(1, 92),
2 -> createFaction(2, 22)
val startingState = GameState(
currentDate = Some(Date(year = 1501, month = 6)),
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentPhase = FOOD_CONSUMPTION,
provinces = Map(
5 -> Province(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
provinceOrders = ProvinceOrderType.ENTRUST,
rulingFactionHeroIds = Vector(22),
hostileArmies = Vector(undecidedArmy)
)
),
factions = Map(
1 -> Faction(id = 1),
2 -> Faction(id = 2)
),
battalionTypes = battalionTypes
)
val ex = the[EagleInternalException] thrownBy {
RequestBattlesAction(
gameId = 0x123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
battalionTypes = battalionTypes
).results
RequestBattlesAction(startingState)
.execute(actionResultProtoApplier)
.map(_.actionResult)
}
ex.getMessage shouldBe "requirement failed: all armies must be attacking in the battle request phase"
@@ -308,58 +318,60 @@ class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
it should "add the attacking army's food and gold to the province" in {
val attackingArmy = HostileArmyGroup(
factionId = 1,
status = Attacking,
status = Attacking(),
armies = Vector(
MovingArmy(
id = 1,
army = Army(factionId = 1, units = attackerCombatUnits),
army = Some(
Army(
factionId = 1,
units = attackerCombatUnits
)
),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(gold = 200, food = 300),
suppliesLoss = 0.0,
startingPositionIndex = None
originProvince = 17,
supplies = Some(Supplies(gold = 200, food = 300))
)
)
)
val provinces = Map[ProvinceId, ProvinceC](
5 -> ProvinceC(
id = 5,
hostileArmies = Vector(attackingArmy),
gold = 125,
food = 256
)
)
val factions = Map[FactionId, FactionC](
1 -> createFaction(1, 92),
2 -> createFaction(2, 22)
)
val results = RequestBattlesAction(
gameId = 0x123,
val startingState = GameState(
currentDate = Some(Date(year = 1501, month = 6)),
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
currentPhase = FOOD_CONSUMPTION,
provinces = Map(
5 -> Province(
id = 5,
hostileArmies = Vector(attackingArmy),
gold = 125,
food = 256
)
),
factions = Map(
1 -> Faction(id = 1),
2 -> Faction(id = 2)
),
battalionTypes = battalionTypes
).results
)
inside(results.head.changedProvinces.head) {
case changedProvinceC: ChangedProvinceC =>
changedProvinceC.provinceId shouldBe 5
changedProvinceC.goldDelta should contain(200)
changedProvinceC.foodDelta should contain(300)
}
val results =
RequestBattlesAction(startingState)
.execute(actionResultProtoApplier)
.map(_.actionResult)
results.head.changedProvinces.head.id shouldBe 5
results.head.changedProvinces.head.goldDelta should contain(200)
results.head.changedProvinces.head.foodDelta should contain(300)
}
"Execute with an empty battalion" should "send an empty battalion" in {
val attackerCombatUnitsWithNoBattalion = Vector(
CombatUnit(factionId = attackerId, heroId = 92, battalionId = None)
CombatUnit(
factionId = attackerId,
heroId = 92,
battalionId = None
)
)
val defendingArmy = Army(
@@ -370,130 +382,224 @@ class RequestBattlesActionTest extends AnyFlatSpec with Matchers {
val attackingArmy = HostileArmyGroup(
factionId = 1,
status = Attacking,
status = Attacking(),
armies = Vector(
MovingArmy(
id = 1,
army = Army(
factionId = 1,
units = attackerCombatUnitsWithNoBattalion,
fleeProvinceId = Some(17)
army = Some(
Army(
factionId = 1,
units = attackerCombatUnitsWithNoBattalion,
fleeProvinceId = Some(17)
)
),
arrivalRound = 9,
destinationProvinceId = 5,
originProvinceId = 17,
supplies = Supplies(gold = 0, food = 0),
suppliesLoss = 0.0,
startingPositionIndex = None
originProvince = 17
)
)
)
val provinces = Map[ProvinceId, ProvinceC](
5 -> ProvinceC(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
neighbors =
Vector(Neighbor(provinceId = 17, startingPositionIndex = 4)),
provinceOrders = ProvinceOrderType.Entrust,
rulingFactionHeroIds = Vector(22),
defendingArmy = Some(defendingArmy),
hostileArmies = Vector(attackingArmy),
hexMapName = "Lower_Lake",
food = 1000
)
)
val factions = Map[FactionId, FactionC](
1 -> createFaction(1, 92),
3 -> createFaction(3, 22)
)
val results = RequestBattlesAction(
val startingState = GameState(
currentDate = Some(Date(year = 1501, month = 6)),
gameId = 0xabc123,
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0xa5,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
battalionTypes = battalionTypes
).results
results should have size 1
val battleResult = results.head
battleResult.newBattle should not be empty
val battle = battleResult.newBattle.get
battle.eagleGameId shouldBe 0xabc123
battle.roundStarted shouldBe 9
battle.shardokGameId shouldBe "abc123_a6_a884c092"
battle.battleIndex shouldBe 0xa6
battle.battleType shouldBe BattleType.AssaultProvince
battle.hexMapName shouldBe "Lower_Lake"
battle.defenderProvince shouldBe 5
battle.players should have size 2
battle.players.find(_.eagleFid == 1).get.isDefender shouldBe false
battle.players.find(_.eagleFid == 1).get.armyGroup shouldBe Some(
attackingArmy
)
battle.players.find(_.eagleFid == 1).get.victoryConditions should contain(
VictoryCondition.HoldsCriticalTiles
)
battle.players.find(_.eagleFid == 1).get.victoryConditions should contain(
VictoryCondition.LastPlayerStanding
)
battle.players.find(_.eagleFid == 3).get.isDefender shouldBe true
battle.players.find(_.eagleFid == 3).get.food shouldBe 11
battle.players.find(_.eagleFid == 3).get.victoryConditions should contain(
VictoryCondition.LastPlayerStanding
)
battle.players.find(_.eagleFid == 3).get.victoryConditions should contain(
VictoryCondition.WinAfterMaxRounds
)
inside(battleResult.changedProvinces.head) {
case changedProvinceC: ChangedProvinceC =>
changedProvinceC.provinceId shouldBe 5
changedProvinceC.removedHostileArmyFactionIds should contain(
attackingArmy.factionId
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentPhase = FOOD_CONSUMPTION,
provinces = Map(
5 -> Province(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
neighbors =
Vector(Neighbor(provinceId = 17, startingPositionIndex = 4)),
provinceOrders = ProvinceOrderType.ENTRUST,
rulingFactionHeroIds = Vector(22),
defendingArmy = Some(defendingArmy),
hostileArmies = Vector(attackingArmy),
hexMapName = "Lower_Lake",
food = 1000
)
changedProvinceC.foodDelta should contain(-11)
}
),
factions = Map(
1 -> Faction(id = 1),
3 -> Faction(id = 3)
),
battalionTypes = battalionTypes
)
val results =
RequestBattlesAction(startingState).execute(actionResultProtoApplier)
results should have size 1
results.head.actionResult should equalProto(
ActionResult(
`type` = START_BATTLE,
province = Some(5),
changedProvinces = Vector(
ChangedProvince(
id = 5,
removedHostileArmyFactionIds = Vector(attackingArmy.factionId),
foodDelta = Some(-11)
)
),
newBattle = Some(
ShardokBattle(
eagleGameId = 0xabc123,
roundStarted = 9,
shardokGameId = "abc123_a6_b7de69e8",
battleIndex = 0xa6,
battleType = BattleType.BATTLE_TYPE_ASSAULT_PROVINCE,
hexMapName = "Lower_Lake",
players = Vector(
ShardokPlayer(
eagleFid = 1,
isDefender = false,
armyGroup = Some(attackingArmy),
victoryConditions = Vector(
VICTORY_CONDITION_HOLDS_CRITICAL_TILES,
VICTORY_CONDITION_LAST_PLAYER_STANDING
)
),
ShardokPlayer(
eagleFid = 3,
isDefender = true,
armyGroup = Some(
HostileArmyGroup(
factionId = 3,
armies = Vector(MovingArmy(army = Some(defendingArmy)))
)
),
food = 11,
victoryConditions = Vector(
VICTORY_CONDITION_LAST_PLAYER_STANDING,
VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS
)
)
),
defenderProvince = 5
)
)
)
)
}
"execute" should "return nothing if there is no incoming army" in {
val provinces = Map[ProvinceId, ProvinceC](
5 -> ProvinceC(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
rulingFactionHeroIds = Vector(22)
)
)
val factions = Map[FactionId, FactionC](
1 -> createFaction(1, 92),
3 -> createFaction(3, 22)
)
val action = RequestBattlesAction(
gameId = 0x123,
val startingState = GameState(
currentDate = Some(Date(year = 1501, month = 6)),
heroes = HeroMap(heroes),
battalions = BattalionMap(battalions),
currentRoundId = 9,
currentDate = Date(year = 1501, month = Date.Month.June),
battleCounter = 0,
heroes = heroes,
battalions = battalions,
provinces = provinces,
factions = factions,
currentPhase = FOOD_CONSUMPTION,
provinces = Map(
5 -> Province(
id = 5,
rulingFactionId = Some(defenderId),
rulingHeroId = Some(22),
rulingFactionHeroIds = Vector(22)
)
),
factions = Map(
1 -> Faction(id = 1),
3 -> Faction(id = 3)
),
battalionTypes = battalionTypes
)
val results = action.results
val action = RequestBattlesAction(startingState)
val results = action.execute(actionResultProtoApplier).map(_.actionResult)
results shouldBe empty
}
// "Execute with multiple incoming armies" should "return a started battle and the rest just return" in {
// val defendingArmy = Army(
// factionId = 3,
// units = defenderCombatUnits
// )
//
// val attackingArmy1 = HostileArmyGroup(
// factionId = 1,
//
// MovingArmy(
// army = Some(Army(factionId = 1, units = attackerCombatUnits)),
// arrivalRound = 9,
// originProvince = 17
// )
// val attackingArmy2 = MovingArmy(
// army = Some(
// Army(
// factionId = 1,
// units = Vector(CombatUnit(factionId = 1, heroId = 212))
// )
// ),
// arrivalRound = 9,
// originProvince = 23
// )
// val attackingArmy3 = MovingArmy(
// army = Some(
// Army(
// factionId = 7,
// units = Vector(CombatUnit(factionId = 7, heroId = 929))
// )
// ),
// arrivalRound = 9,
// originProvince = 33
// )
//
// val startingState = GameState(
// gameId = 0xCAFE,
// battleCounter = 0x17,
// heroes = HeroMap(heroes),
// battalions = BattalionMap(battalions),
// currentRoundId = 9,
// currentPhase = FOOD_CONSUMPTION,
// provinces = Map(
// 5 -> Province(
// id = 5,
// rulingFactionId = Some(defenderId),
// rulingHeroId = Some(22),
// neighbors = Vector(
// Neighbor(provinceId = 17, startingPositionIndex = 4),
// Neighbor(provinceId = 23, startingPositionIndex = 6),
// Neighbor(provinceId = 33, startingPositionIndex = 0)
// ),
// provinceOrders = ProvinceOrderType.ENTRUST,
// rulingFactionHeroIds = Vector(22),
// defendingArmy = Some(defendingArmy),
// incomingArmies = Vector(attackingArmy1, attackingArmy2, attackingArmy3)
// ),
// 17 -> Province(id = 17),
// 23 -> Province(id = 23),
// 33 -> Province(id = 33)
// ),
// factions = Map(
// 1 -> Faction(id = 1),
// 3 -> Faction(id = 3),
// 7 -> Faction(id = 7)
// )
// )
//
// val action = RequestBattlesAction(startingState)
//
// val results = action.resultsOfExecute()
//
// results.size shouldBe 3
// results.map(_.`type`) shouldBe Vector(START_BATTLE, BATTLE_ENDED, BATTLE_ENDED)
// results(1).changedProvinces should have size 2
// // remove the incoming army
// results(1).changedProvinces.head.id shouldBe 5
// results(1).changedProvinces.head.removedIncomingArmyIds should have size 1
// // add an incoming army to the origin
// Vector(17, 23, 33).contains(results(1).changedProvinces.last.id) shouldBe true
// results(1).changedProvinces.last.addedIncomingArmies should have size 1
//
// results(2).changedProvinces should have size 2
// // remove the incoming army
// results(2).changedProvinces.head.id shouldBe 5
// results(2).changedProvinces.head.removedIncomingArmyIds should have size 1
// // add an incoming army to the origin
// Vector(17, 23, 33).contains(results(2).changedProvinces.last.id) shouldBe true
// results(2).changedProvinces.last.addedIncomingArmies should have size 1
// }
}
@@ -1,117 +1,117 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.common.ProtoMatchers
import net.eagle0.common.common_unit.UnitStatus
import net.eagle0.eagle.common.combat_unit.CombatUnit as CombatUnitProto
import net.eagle0.eagle.internal.army.{
AwaitingFreeForAll,
HostileArmyGroup,
Army as ArmyProto,
MovingArmy as MovingArmyProto
}
import net.eagle0.eagle.internal.battalion.Battalion
import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship.RelationshipLevel.TRUCE
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.internal.shardok_battle.ShardokPlayer
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplierImpl
import net.eagle0.eagle.library.util.IDable.{
mapifyBattalions,
mapifyFactions,
mapifyProvinces
}
import net.eagle0.eagle.library.util.validations.TestingNoopValidator
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.types.WonFreeForAllResultType
import net.eagle0.eagle.model.state.unit_status.UnitStatus
import net.eagle0.eagle.model.state.{
Army,
BattalionTypeId,
CombatUnit,
HostileArmyGroup,
MovingArmy,
Supplies
}
import net.eagle0.eagle.model.state.HostileArmyGroupStatus.AwaitingDecision
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
import net.eagle0.eagle.model.state.hero.concrete.HeroC
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.proto_converters.ArmyConverter
import net.eagle0.eagle.model.state.{Army, CombatUnit, MovingArmy, Supplies}
import net.eagle0.eagle.shardok_interface.ResolvedEagleUnit
import net.eagle0.eagle.{FactionId, ProvinceId, RoundId}
import org.scalatest.Inside.inside
import org.scalatest.Inspectors.forExactly
import org.scalatest.LoneElement.convertToCollectionLoneElementWrapper
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class WonFreeForAllActionTest extends AnyFlatSpec with Matchers {
private val winningFid: FactionId = 37
private val roundId: RoundId = 88
private val provinceId: ProvinceId = 19
class WonFreeForAllActionTest
extends AnyFlatSpec
with Matchers
with ProtoMatchers {
private val winningFid = 37
private val roundId = 88
private val provinceId = 19
private val winningArmy = MovingArmy(
id = 0,
army = Army(
factionId = winningFid,
units = Vector(
CombatUnit(
heroId = 11,
battalionId = Some(33),
factionId = winningFid
),
CombatUnit(
heroId = 12,
battalionId = Some(34),
factionId = winningFid
val actionResultProtoApplier = new ActionResultProtoApplierImpl(
validator = TestingNoopValidator
)
private val winningArmy = MovingArmyProto(
army = Some(
ArmyProto(
factionId = winningFid,
units = Vector(
CombatUnitProto(
heroId = 11,
battalionId = Some(33),
factionId = winningFid
),
CombatUnitProto(
heroId = 12,
battalionId = Some(34),
factionId = winningFid
)
)
)
),
arrivalRound = roundId,
destinationProvinceId = provinceId,
originProvinceId = 1,
supplies = Supplies(0, 0),
suppliesLoss = 0.0,
startingPositionIndex = None
arrivalRound = roundId
)
private val winningArmyGroup = HostileArmyGroup(
status = AwaitingDecision,
status = AwaitingFreeForAll(),
factionId = winningFid,
armies = Vector(winningArmy)
)
private val province = ProvinceC(
id = provinceId,
name = "Test Province"
)
private val battalions = Map(
33 -> BattalionC(
id = 33,
size = 100,
typeId = BattalionTypeId.LightInfantry
),
34 -> BattalionC(
id = 34,
size = 150,
typeId = BattalionTypeId.LightInfantry
),
37 -> BattalionC(
id = 37,
size = 100,
typeId = BattalionTypeId.LightInfantry
private val province =
Province(id = provinceId)
private val gameState = GameState(
factions = mapifyFactions(Faction(id = winningFid)),
provinces = mapifyProvinces(province),
battalions = mapifyBattalions(
Battalion(id = 33, size = 100),
Battalion(id = 34, size = 150),
Battalion(id = 37, size = 100)
)
)
private val winningUnits = Vector(
ResolvedEagleUnit(
hero = HeroC(
id = 11,
factionId = Some(winningFid),
profession = net.eagle0.eagle.model.state.hero.Profession.Mage,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Male
),
battalion = None,
status = UnitStatus.Normal
hero = Hero(id = 11, factionId = Some(winningFid)),
battalion = Battalion(id = 33),
status = UnitStatus.NORMAL_UNIT
),
ResolvedEagleUnit(
hero = HeroC(
id = 12,
factionId = Some(winningFid),
profession = net.eagle0.eagle.model.state.hero.Profession.Ranger,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Female
),
battalion = None,
status = UnitStatus.Normal
hero = Hero(id = 12, factionId = Some(winningFid)),
battalion = Battalion(id = 34),
status = UnitStatus.NORMAL_UNIT
)
)
private val winningPlayers = Vector(
ShardokPlayer(
eagleFid = winningFid,
isDefender = false,
armyGroup = Some(winningArmyGroup)
)
)
private val winningArmyGroups = Vector(winningArmyGroup)
"immediateExecute" should "set the basics" in {
val result = WonFreeForAllAction(
battalions,
gameState,
province,
winningUnits,
winningArmyGroups
winningPlayers
).immediateExecute
result.actionResultType shouldBe WonFreeForAllResultType
@@ -123,10 +123,10 @@ class WonFreeForAllActionTest extends AnyFlatSpec with Matchers {
it should "have a single changed province with the defending province ID" in {
val result = WonFreeForAllAction(
battalions,
gameState,
province,
winningUnits,
winningArmyGroups
winningPlayers
).immediateExecute
result.changedProvinces.loneElement.provinceId shouldBe provinceId
@@ -134,43 +134,39 @@ class WonFreeForAllActionTest extends AnyFlatSpec with Matchers {
it should "add the victor's incoming army back to the province" in {
val result = WonFreeForAllAction(
battalions,
gameState,
province,
winningUnits,
winningArmyGroups
winningPlayers
).immediateExecute
forExactly(1, result.changedProvinces) { case cp: ChangedProvinceC =>
cp.newHostileArmies.loneElement.armies.loneElement shouldBe winningArmy
cp.newHostileArmies.loneElement.armies.loneElement shouldBe ArmyConverter
.fromProto(winningArmy)
}
}
it should "not include a unit that is not in the winningUnits arg" in {
val fewerWinningUnits = Vector(
ResolvedEagleUnit(
hero = HeroC(
id = 12,
factionId = Some(winningFid),
profession = net.eagle0.eagle.model.state.hero.Profession.Ranger,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Female
),
battalion = None,
status = UnitStatus.Normal
hero = Hero(id = 12, factionId = Some(winningFid)),
battalion = Battalion(id = 34),
status = UnitStatus.NORMAL_UNIT
)
)
val result = WonFreeForAllAction(
battalions,
gameState,
province,
fewerWinningUnits,
winningArmyGroups
winningPlayers
).immediateExecute
inside(result.changedProvinces.head) { case cp: ChangedProvinceC =>
cp.newHostileArmies.loneElement.armies.loneElement shouldBe MovingArmy(
id = 0,
destinationProvinceId = provinceId,
originProvinceId = 1,
destinationProvinceId = 0,
originProvinceId = 0,
army = Army(
factionId = winningFid,
units = Vector(
@@ -184,94 +180,103 @@ class WonFreeForAllActionTest extends AnyFlatSpec with Matchers {
),
arrivalRound = roundId,
supplies = Supplies(0, 0),
suppliesLoss = 0.0,
suppliesLoss = 0,
startingPositionIndex = None
)
}
}
it should "add multiple incoming armies if they are all winners" in {
val sameFidWinningArmy = MovingArmy(
id = 1,
val sameFidWinningArmy = MovingArmyProto(
arrivalRound = roundId,
army = Army(
factionId = winningFid,
units = Vector(
CombatUnit(heroId = 13, battalionId = None, factionId = winningFid)
army = Some(
ArmyProto(
factionId = winningFid,
units = Vector(CombatUnitProto(heroId = 13, battalionId = None))
)
),
destinationProvinceId = provinceId,
originProvinceId = 1,
supplies = Supplies(0, 0),
suppliesLoss = 0.0,
startingPositionIndex = None
)
val differentFidWinningArmy = MovingArmy(
id = 2,
arrivalRound = roundId,
army = Army(
factionId = 777,
units = Vector(
CombatUnit(
heroId = 14,
battalionId = Some(35),
factionId = 777
)
)
),
destinationProvinceId = provinceId,
originProvinceId = 2,
supplies = Supplies(0, 0),
suppliesLoss = 0.0,
startingPositionIndex = None
)
val testBattalions = battalions + (35 -> BattalionC(
typeId = BattalionTypeId.LightInfantry,
id = 35,
size = 100
))
val multipleWinningArmyGroups = Vector(
HostileArmyGroup(
status = AwaitingDecision,
factionId = winningFid,
armies = Vector(winningArmy, sameFidWinningArmy)
),
HostileArmyGroup(
status = AwaitingDecision,
factionId = 777,
armies = Vector(differentFidWinningArmy)
)
)
val extendedWinningUnits = winningUnits ++ Vector(
ResolvedEagleUnit(
hero = HeroC(
id = 13,
profession = net.eagle0.eagle.model.state.hero.Profession.Engineer,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Male
val differentFidWinningArmy = MovingArmyProto(
arrivalRound = roundId,
army = Some(
ArmyProto(
factionId = 777,
units = Vector(CombatUnitProto(heroId = 14, battalionId = Some(35)))
)
)
)
val p = Province(id = provinceId)
val gs = GameState(
provinces = mapifyProvinces(p),
battalions = gameState.battalions ++ mapifyBattalions(
Battalion(id = 35, size = 100)
),
factions = mapifyFactions(
Faction(
id = winningFid,
factionRelationships = Vector(
FactionRelationship(
targetFactionId = 777,
relationshipLevel = TRUCE
)
)
),
battalion = None,
status = UnitStatus.Normal
Faction(
id = 777,
factionRelationships = Vector(
FactionRelationship(
targetFactionId = winningFid,
relationshipLevel = TRUCE
)
)
)
)
)
val wp = Vector(
ShardokPlayer(
eagleFid = winningFid,
isDefender = false,
armyGroup = Some(
HostileArmyGroup(
factionId = winningFid,
armies = Vector(winningArmy, sameFidWinningArmy)
)
)
),
ShardokPlayer(
eagleFid = 777,
isDefender = false,
armyGroup = Some(
HostileArmyGroup(
factionId = 777,
armies = Vector(differentFidWinningArmy)
)
)
)
)
val wu = winningUnits ++ Vector(
ResolvedEagleUnit(
hero = Hero(id = 13),
battalion = Battalion(),
status = UnitStatus.NORMAL_UNIT
),
ResolvedEagleUnit(
hero = HeroC(
id = 14,
profession = net.eagle0.eagle.model.state.hero.Profession.Paladin,
pronounGender = net.eagle0.eagle.model.state.hero.Gender.Female
),
battalion = None,
status = UnitStatus.Normal
hero = Hero(id = 14),
battalion = Battalion(id = 35),
status = UnitStatus.NORMAL_UNIT
)
)
val result = WonFreeForAllAction(
battalions = testBattalions,
battleProvince = province,
winningUnits = extendedWinningUnits,
winningArmyGroups = multipleWinningArmyGroups
startingState = gs,
battleProvince = p,
winningUnits = wu,
winningPlayers = wp
).immediateExecute
inside(result.changedProvinces.head) { case cp: ChangedProvinceC =>
@@ -279,9 +284,9 @@ class WonFreeForAllActionTest extends AnyFlatSpec with Matchers {
cp.newHostileArmies.flatMap(
_.armies
) should contain theSameElementsAs Seq(
winningArmy,
sameFidWinningArmy,
differentFidWinningArmy
ArmyConverter.fromProto(winningArmy),
ArmyConverter.fromProto(sameFidWinningArmy),
ArmyConverter.fromProto(differentFidWinningArmy)
)
}
}
@@ -147,12 +147,7 @@ class AttackCommandChooserTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = originProvinceId,
availableDestinationProvinces = Vector(destinationProvinceId).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(destinationProvinceId),
availableHeroIds = Vector(1, 2, 3, 4),
availableBattalions = battalions.map { batt =>
BattalionWithFoodCost(
@@ -281,77 +276,4 @@ class AttackCommandChooserTest
)
)
}
"chosenAttackCommand" should "respect river crossing requirements from AvailableDestinationProvince" in {
// Create a march command with river crossing requirement
val marchCommandWithRiverCrossing = MarchAvailableCommand(
actingProvinceId = originProvinceId,
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = originProvinceId,
availableDestinationProvinces = Vector(
AvailableDestinationProvince(
provinceId = destinationProvinceId,
requiresRiverCrossing = true
)
),
availableHeroIds = Vector(1, 2, 3, 4),
availableBattalions = battalions.map { batt =>
BattalionWithFoodCost(
battalionId = batt.id,
monthlyFood = 15
)
}
)
)
)
// Create a game state in winter (should still respect river crossing from proto)
val winterGameState = gameState.copy(
currentDate = Some(Date(year = 1, month = 1)) // January = winter
)
val attackCommands = AttackCommandChooser.possibleAttackCommands(
actingFactionId = actingFactionId,
gameState = winterGameState,
ac = marchCommandWithRiverCrossing
)
// Verify that the river crossing requirement is taken from the proto
attackCommands should have length 1
attackCommands.head.requiresRiverCrossing shouldBe true
// Also test with requiresRiverCrossing = false
val marchCommandWithoutRiverCrossing = MarchAvailableCommand(
actingProvinceId = originProvinceId,
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = originProvinceId,
availableDestinationProvinces = Vector(
AvailableDestinationProvince(
provinceId = destinationProvinceId,
requiresRiverCrossing = false
)
),
availableHeroIds = Vector(1, 2, 3, 4),
availableBattalions = battalions.map { batt =>
BattalionWithFoodCost(
battalionId = batt.id,
monthlyFood = 15
)
}
)
)
)
val attackCommandsNoRiverCrossing =
AttackCommandChooser.possibleAttackCommands(
actingFactionId = actingFactionId,
gameState = winterGameState,
ac = marchCommandWithoutRiverCrossing
)
attackCommandsNoRiverCrossing should have length 1
attackCommandsNoRiverCrossing.head.requiresRiverCrossing shouldBe false
}
}
@@ -692,12 +692,7 @@ class CommandChoiceHelpersTest
goldCost = 0,
goldAvailable = 0,
foodAvailable = 0,
availableDestinationProvinces = Vector(3, 4, 6, 7).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(3, 4, 6, 7),
availableHeroIds = (1 to 30).toVector,
availableBattalions = Vector(),
recommendedBattleConfig = None
@@ -2,7 +2,6 @@ package net.eagle0.eagle.library.util.command_choice_helpers
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.api.available_command.{
AvailableDestinationProvince,
ImproveAvailableCommand,
MarchAvailableCommand,
MarchCommandFromOneProvince,
@@ -151,12 +150,7 @@ class ExpandCommandSelectorTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = 9,
availableDestinationProvinces = Vector(10).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(10),
availableHeroIds = Vector(1, 2, 3)
)
)
@@ -220,12 +214,7 @@ class ExpandCommandSelectorTest
oneProvinceCommands = Vector(
MarchCommandFromOneProvince(
originProvinceId = 9,
availableDestinationProvinces = Vector(10).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
),
availableDestinationProvinces = Vector(10),
availableHeroIds = Vector(1, 2, 3)
)
)
@@ -466,15 +455,8 @@ class ExpandCommandSelectorTest
BattalionWithFoodCost(battalionId = 2, monthlyFood = 50),
BattalionWithFoodCost(battalionId = 3, monthlyFood = 50)
),
availableDestinationProvinces = actingProvince.neighbors
.map(_.provinceId)
.map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
)
.toVector
availableDestinationProvinces =
actingProvince.neighbors.map(_.provinceId).toVector
)
)
)
@@ -531,9 +513,9 @@ class ExpandCommandSelectorTest
.update(
_.oneProvinceCommands.head.availableDestinationProvinces
.modify(
_.filterNot(adp =>
_.filterNot(pid =>
marchGameState
.provinces(adp.provinceId)
.provinces(pid)
.rulingFactionId
.contains(actingFactionId)
)
@@ -571,11 +553,6 @@ class ExpandCommandSelectorTest
),
_.oneProvinceCommands.head.availableDestinationProvinces := Vector(
ownedProvince.id
).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
)
)
),
@@ -616,11 +593,6 @@ class ExpandCommandSelectorTest
),
_.oneProvinceCommands.head.availableDestinationProvinces := Vector(
unownedProvince1.id
).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
)
)
@@ -671,11 +643,6 @@ class ExpandCommandSelectorTest
),
_.oneProvinceCommands.head.availableDestinationProvinces := Vector(
unownedProvince1.id
).map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
)
)
@@ -723,12 +690,6 @@ class ExpandCommandSelectorTest
_.oneProvinceCommands.head.availableHeroIds := (1 to 12).toVector,
_.oneProvinceCommands.head.availableDestinationProvinces := actingProvince.neighbors
.map(_.provinceId)
.map(pid =>
AvailableDestinationProvince(
provinceId = pid,
requiresRiverCrossing = false
)
)
)
val selection = ExpandCommandSelector