Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 96566f9322 Convert EndPleaseRecruitMePhaseAction to Scala GameState and fix test
- Convert EndPleaseRecruitMePhaseAction to take Scala GameState directly
- Rewrite EndDefenseDecisionPhaseActionTest to use pure Scala model objects
  (instead of creating proto GameState and converting)
- Fix test to expect correct phase transition (TruceTurnBack, not BattleRequest)
- Update BUILD.bazel deps for both action and test

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 15:45:20 -08:00
adminandClaude Opus 4.5 0b5146f5b1 Document Scala 3 compiler crash blocker for EndPleaseRecruitMePhaseAction
When attempting to convert EndPleaseRecruitMePhaseAction to take Scala GameState,
the Scala 3.7.2 compiler crashes during the lambdaLift phase.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 15:05:04 -08:00
adminandClaude Opus 4.5 8cd2548f07 Convert three more actions to take Scala GameState
- EndFreeForAllDecisionPhaseAction: now takes Scala GameState directly
- EndBattleRequestPhaseAction: renamed fromProtoState to apply, takes Scala GameState
- EndDefenseDecisionPhaseAction: renamed fromProtoState to apply, takes Scala GameState

Updated RoundPhaseAdvancer callers to use GameStateConverter.fromProto().

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 14:59:18 -08:00
9 changed files with 155 additions and 224 deletions
+4
View File
@@ -128,6 +128,10 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
| `VigorXPApplier.scala` | Wraps proto results | Convert to work with T |
| `ResolveBattleAction.scala` | 2 converter calls | Convert after dependencies |
| `PerformForcedTurnBackAction.scala` | 1 converter call | Convert after dependencies |
| `EndFreeForAllDecisionPhaseAction.scala` | ~~fromProtoState~~ | **Complete** - now takes Scala GameState |
| `EndBattleRequestPhaseAction.scala` | ~~fromProtoState~~ | **Complete** - now takes Scala GameState |
| `EndDefenseDecisionPhaseAction.scala` | ~~fromProtoState~~ | **Complete** - now takes Scala GameState |
| `EndPleaseRecruitMePhaseAction.scala` | `fromProtoState` | **BLOCKED** - Scala 3.7.2 compiler crash (lambdaLift phase) |
| `InMemoryHistory.scala` | Stores proto results | Vend Scala types, remove proto entirely |
| `PersistedHistory.scala` | Stores proto results | Vend Scala types, convert internally for disk |
| `GameController.scala` | Uses proto for client communication | Keep proto (gRPC boundary) |
@@ -127,7 +127,7 @@ object RoundPhaseAdvancer {
actionResultProtoApplier.applyActionResult(
currentState,
ActionResultProtoConverter.toProto(
EndPleaseRecruitMePhaseAction.fromProtoState(currentState).immediateExecute
EndPleaseRecruitMePhaseAction(GameStateConverter.fromProto(currentState)).immediateExecute
)
)
)
@@ -187,7 +187,7 @@ object RoundPhaseAdvancer {
// There may eventually be VassalAttackDecisions, but for now we're leaving that on the player
actionResultProtoApplier.applyActionResults(
currentState,
EndFreeForAllDecisionPhaseAction(currentState).results.map(
EndFreeForAllDecisionPhaseAction(GameStateConverter.fromProto(currentState)).results.map(
ActionResultProtoConverter.toProto(_)
)
)
@@ -279,7 +279,7 @@ object RoundPhaseAdvancer {
actionResultProtoApplier.applyActionResult(
currentState,
ActionResultProtoConverter.toProto(
EndDefenseDecisionPhaseAction.fromProtoState(currentState).immediateExecute
EndDefenseDecisionPhaseAction(GameStateConverter.fromProto(currentState)).immediateExecute
)
)
)
@@ -319,7 +319,7 @@ object RoundPhaseAdvancer {
requestResults :+ actionResultProtoApplier.applyActionResult(
latestState,
ActionResultProtoConverter.toProto(
EndBattleRequestPhaseAction.fromProtoState(latestState).immediateExecute
EndBattleRequestPhaseAction(GameStateConverter.fromProto(latestState)).immediateExecute
)
)
@@ -283,21 +283,15 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_battle_request_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -463,22 +457,15 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_defense_decision_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:army_converter",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -496,28 +483,13 @@ scala_library(
],
deps = [
":withdrawn_army_returns_home_action",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_free_for_all_decision_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -631,7 +603,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
@@ -644,8 +615,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:notification_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_please_recruit_me_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -1,12 +1,12 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.EndBattleRequestPhaseResultType
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.RoundPhase
import net.eagle0.eagle.ProvinceId
@@ -23,7 +23,7 @@ case class EndBattleRequestPhaseAction(
}
object EndBattleRequestPhaseAction {
def fromProtoState(gameState: GameState): EndBattleRequestPhaseAction = {
def apply(gameState: GameState): EndBattleRequestPhaseAction = {
internalRequire(
gameState.provinces.forall {
case (_, province) =>
@@ -1,14 +1,13 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.EndDefenseDecisionPhaseResultType
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.ArmyConverter
import net.eagle0.eagle.model.state.{MovingArmy, RoundPhase}
import net.eagle0.eagle.model.state.{HostileArmyGroupStatus, MovingArmy, RoundPhase}
import net.eagle0.eagle.model.state.game_state.GameState
/** Resolution for a single province that paid tribute to hostile armies */
case class PayingProvinceResolution(
@@ -52,19 +51,21 @@ case class EndDefenseDecisionPhaseAction(
}
object EndDefenseDecisionPhaseAction {
def fromProtoState(gameState: GameState): EndDefenseDecisionPhaseAction = {
def apply(gameState: GameState): EndDefenseDecisionPhaseAction = {
val resolutions = gameState.provinces.values.flatMap { province =>
val tributePaidArmyGroups = province.hostileArmies.filter(
_.status.asMessage.sealedValue.isTributePaid
)
val tributePaidArmyGroups = province.hostileArmies.filter { ag =>
ag.status match {
case _: HostileArmyGroupStatus.TributePaid => true
case _ => false
}
}
if tributePaidArmyGroups.isEmpty then None
else {
val returningArmies = for {
ag <- tributePaidArmyGroups
armyProto <- ag.armies
} yield {
val army = ArmyConverter.fromProto(armyProto)
ag <- tributePaidArmyGroups
army <- ag.armies
} yield
// Swap destination/origin, clear flee province, set arrival to next round
MovingArmy(
id = army.id,
@@ -76,7 +77,6 @@ object EndDefenseDecisionPhaseAction {
suppliesLoss = army.suppliesLoss,
startingPositionIndex = army.startingPositionIndex
)
}
Some(
PayingProvinceResolution(
@@ -1,11 +1,10 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.EndFreeForAllDecisionPhaseResultType
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.RoundPhase
case class EndFreeForAllDecisionPhaseAction(gameState: GameState) extends ProtolessSequentialResultsAction {
@@ -13,7 +12,7 @@ case class EndFreeForAllDecisionPhaseAction(gameState: GameState) extends Protol
override def results: Vector[ActionResultT] =
WithdrawnArmiesReturnHomeAction(
gameState.currentRoundId,
gameState.provinces.values.map(ProvinceConverter.fromProto).toVector
gameState.provinces.values.toVector
).results :+ ActionResultC(
actionResultType = EndFreeForAllDecisionPhaseResultType,
newRoundPhase = Some(RoundPhase.FreeForAllBattleRequest)
@@ -1,12 +1,11 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
import net.eagle0.eagle.model.action_result.types.EndPleaseRecruitMePhaseResultType
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.action_result.NotificationT
import net.eagle0.eagle.model.proto_converters.NotificationConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.RoundPhase
case class EndPleaseRecruitMePhaseAction(
@@ -24,10 +23,8 @@ case class EndPleaseRecruitMePhaseAction(
}
object EndPleaseRecruitMePhaseAction {
def fromProtoState(gameState: GameState): EndPleaseRecruitMePhaseAction =
def apply(gameState: GameState): EndPleaseRecruitMePhaseAction =
EndPleaseRecruitMePhaseAction(
deferredNotifications = gameState.deferredNotifications
.map(n => NotificationConverter.fromProto(n, deferred = true))
.toVector
)
}
@@ -166,31 +166,21 @@ scala_test(
name = "end_defense_decision_phase_action_test",
srcs = ["EndDefenseDecisionPhaseActionTest.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/actions/applier:action_result_proto_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:end_defense_decision_phase_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
"//src/test/scala/net/eagle0/eagle/library/util/validations:testing_noop_validator",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_defense_decision_phase_result_type",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state:supplies",
"//src/main/scala/net/eagle0/eagle/model/state:tribute_amount",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
],
)
@@ -1,83 +1,86 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.common.action_result_type.ActionResultType.END_DEFENSE_DECISION_PHASE
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.common.date.Date
import net.eagle0.eagle.internal.army.{Army, HostileArmyGroup, MovingArmy, TributePaid}
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.province.Province
import net.eagle0.eagle.internal.supplies.Supplies
import net.eagle0.eagle.library.actions.applier.ActionResultProtoApplierImpl
import net.eagle0.eagle.library.util.validations.TestingNoopValidator
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.types.EndDefenseDecisionPhaseResultType
import net.eagle0.eagle.model.state.{
Army,
CombatUnit,
HostileArmyGroup,
HostileArmyGroupStatus,
MovingArmy,
RoundPhase,
Supplies,
TributeAmount
}
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.state.run_status.RunStatus
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class EndDefenseDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
val actionResultProtoApplier = new ActionResultProtoApplierImpl(
validator = TestingNoopValidator
)
private val defendingProvinceId: ProvinceId = 1
private val defenderFid: FactionId = 3
private val attacker1Fid: FactionId = 7
private val attacker2Fid: FactionId = 8
private val tributeArmy1: HostileArmyGroup = HostileArmyGroup(
status = TributePaid(),
factionId = attacker1Fid,
armies = Vector(
MovingArmy(
originProvince = 7,
destinationProvince = defendingProvinceId,
army = Some(
Army(
factionId = attacker1Fid,
units = Vector(
CombatUnit(heroId = 7, battalionId = Some(17))
)
)
id = 1,
originProvinceId = 7,
destinationProvinceId = defendingProvinceId,
arrivalRound = 19,
army = Army(
factionId = attacker1Fid,
units = Vector(CombatUnit(factionId = attacker1Fid, heroId = 7, battalionId = Some(17)))
),
supplies = Some(Supplies(gold = 100, food = 200))
supplies = Supplies(gold = 100, food = 200),
suppliesLoss = 0,
startingPositionIndex = None
),
MovingArmy(
originProvince = 27,
destinationProvince = defendingProvinceId,
army = Some(
Army(
factionId = attacker1Fid,
units = Vector(CombatUnit(heroId = 27, battalionId = None))
)
id = 2,
originProvinceId = 27,
destinationProvinceId = defendingProvinceId,
arrivalRound = 19,
army = Army(
factionId = attacker1Fid,
units = Vector(CombatUnit(factionId = attacker1Fid, heroId = 27, battalionId = None))
),
supplies = Some(Supplies(gold = 0, food = 0))
supplies = Supplies(gold = 0, food = 0),
suppliesLoss = 0,
startingPositionIndex = None
)
)
),
status = HostileArmyGroupStatus.TributePaid(TributeAmount(gold = 50, food = 50))
)
private val tributeArmy2: HostileArmyGroup = HostileArmyGroup(
status = TributePaid(),
factionId = attacker2Fid,
armies = Vector(
MovingArmy(
originProvince = 8,
destinationProvince = defendingProvinceId,
army = Some(
Army(
factionId = attacker2Fid,
units = Vector(
CombatUnit(heroId = 8, battalionId = Some(18))
)
)
id = 3,
originProvinceId = 8,
destinationProvinceId = defendingProvinceId,
arrivalRound = 19,
army = Army(
factionId = attacker2Fid,
units = Vector(CombatUnit(factionId = attacker2Fid, heroId = 8, battalionId = Some(18)))
),
supplies = Some(Supplies(gold = 100, food = 200))
supplies = Supplies(gold = 100, food = 200),
suppliesLoss = 0,
startingPositionIndex = None
)
)
),
status = HostileArmyGroupStatus.TributePaid(TributeAmount(gold = 25, food = 25))
)
private val defendingProvince = Province(
private val defendingProvince: ProvinceC = ProvinceC(
id = defendingProvinceId,
rulingFactionId = Some(defenderFid),
rulingHeroId = Some(9),
@@ -85,115 +88,82 @@ class EndDefenseDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
)
private val gameState: GameState = GameState(
gameId = 1L,
currentPhase = RoundPhase.DefenseDecision,
currentRoundId = 19,
currentDate = Some(Date(year = 1501, month = 6)),
currentDate = Some(Date(year = 1501, month = Date.Month.June)),
actionResultCount = 0,
randomSeed = 1L,
provinces = Map(
defendingProvinceId -> defendingProvince,
7 -> Province(id = 7),
27 -> Province(id = 27),
8 -> Province(id = 8)
7 -> ProvinceC(id = 7),
27 -> ProvinceC(id = 27),
8 -> ProvinceC(id = 8)
),
heroes = Map.empty,
battalions = Map.empty,
destroyedBattalions = Map.empty,
factions = Map(
defenderFid -> Faction(id = defenderFid, leaders = Vector(19))
)
defenderFid -> FactionC(id = defenderFid, factionHeadId = 19, name = "Defenders", leaderIds = Vector(19))
),
factionCommandCounts = Map.empty,
killedHeroes = Map.empty,
destroyedFactions = Map.empty,
outstandingBattles = Vector.empty,
battleCounter = 0,
deferredNotifications = Vector.empty,
runStatus = RunStatus.Running,
victor = None,
battalionTypes = Vector.empty,
chronicleEntries = Vector.empty
)
private def executeAction() = {
val actionResult = EndDefenseDecisionPhaseAction.fromProtoState(gameState).immediateExecute
val protoResult = ActionResultProtoConverter.toProto(actionResult)
Vector(actionResultProtoApplier.applyActionResult(gameState, protoResult))
"execute" should "end the phase and proceed to TRUCE_TURN_BACK" in {
val actionResult = EndDefenseDecisionPhaseAction(gameState).immediateExecute
actionResult.actionResultType shouldBe EndDefenseDecisionPhaseResultType
actionResult.newRoundPhase should contain(RoundPhase.TruceTurnBack)
}
"execute" should "end the phase and proceed to BATTLE_REQUEST" in {
val results = executeAction()
val lastAr = results.last.actionResult
it should "remove tribute paid armies from the province" in {
val actionResult = EndDefenseDecisionPhaseAction(gameState).immediateExecute
lastAr.`type` shouldBe END_DEFENSE_DECISION_PHASE
val changedDefendingProvince = actionResult.changedProvinces
.find(_.provinceId == defendingProvinceId)
.map(_.asInstanceOf[ChangedProvinceC])
changedDefendingProvince should not be empty
changedDefendingProvince.get.removedHostileArmyFactionIds should contain allOf (attacker1Fid, attacker2Fid)
}
it should "clear the tribute paid armies" in {
val results = executeAction().map(_.actionResult)
it should "add returning armies to their origin provinces" in {
val actionResult = EndDefenseDecisionPhaseAction(gameState).immediateExecute
val allCps = results.flatMap(_.changedProvinces)
// Province 7 should have incoming army returning
val province7Changes = actionResult.changedProvinces
.find(_.provinceId == 7)
.map(_.asInstanceOf[ChangedProvinceC])
province7Changes should not be empty
province7Changes.get.newIncomingArmies should have size 1
province7Changes.get.newIncomingArmies.head.originProvinceId shouldBe defendingProvinceId
province7Changes.get.newIncomingArmies.head.destinationProvinceId shouldBe 7
province7Changes.get.newIncomingArmies.head.arrivalRound shouldBe 20
allCps.should(
contain(
ChangedProvince(
id = defendingProvinceId,
removedHostileArmyFactionIds = Vector(tributeArmy1.factionId, tributeArmy2.factionId)
)
)
)
}
// Province 27 should have incoming army returning
val province27Changes = actionResult.changedProvinces
.find(_.provinceId == 27)
.map(_.asInstanceOf[ChangedProvinceC])
province27Changes should not be empty
province27Changes.get.newIncomingArmies should have size 1
province27Changes.get.newIncomingArmies.head.originProvinceId shouldBe defendingProvinceId
province27Changes.get.newIncomingArmies.head.destinationProvinceId shouldBe 27
it should "move the tribute demanding armies back to their originating provinces" in {
val results = executeAction().map(_.actionResult)
val allCps = results.flatMap(_.changedProvinces)
allCps.should(
contain.allElementsOf(
Vector(
ChangedProvince(
id = 7,
addedIncomingArmies = Vector(
MovingArmy(
originProvince = defendingProvinceId,
destinationProvince = 7,
arrivalRound = 20,
army = Some(
Army(
factionId = attacker1Fid,
units = Vector(
CombatUnit(heroId = 7, battalionId = Some(17))
)
)
),
supplies = Some(Supplies(gold = 100, food = 200))
)
)
),
ChangedProvince(
id = 27,
addedIncomingArmies = Vector(
MovingArmy(
originProvince = defendingProvinceId,
destinationProvince = 27,
arrivalRound = 20,
army = Some(
Army(
factionId = attacker1Fid,
units = Vector(
CombatUnit(heroId = 27, battalionId = None)
)
)
),
supplies = None // (0, 0) supplies converts to None in proto
)
)
),
ChangedProvince(
id = 8,
addedIncomingArmies = Vector(
MovingArmy(
originProvince = defendingProvinceId,
destinationProvince = 8,
arrivalRound = 20,
army = Some(
Army(
factionId = attacker2Fid,
units = Vector(
CombatUnit(heroId = 8, battalionId = Some(18))
)
)
),
supplies = Some(Supplies(gold = 100, food = 200))
)
)
)
)
)
)
// Province 8 should have incoming army returning
val province8Changes = actionResult.changedProvinces
.find(_.provinceId == 8)
.map(_.asInstanceOf[ChangedProvinceC])
province8Changes should not be empty
province8Changes.get.newIncomingArmies should have size 1
province8Changes.get.newIncomingArmies.head.originProvinceId shouldBe defendingProvinceId
province8Changes.get.newIncomingArmies.head.destinationProvinceId shouldBe 8
}
}