Compare commits

...
Author SHA1 Message Date
admin dbfa949d19 almost finished 2025-09-22 19:38:41 -07:00
admin 2ea25b8716 take the test protoless 2025-09-22 17:16:44 -07:00
adminandClaude a5f9979749 Update ResolveBattleAction to use protoless downstream actions
- WonFreeForAllAction call updated to use proper armyGroup accessor pattern
- FreeForAllDrawAction call updated to use proper armyGroup accessor pattern
- Maintains compatibility with recently merged protoless actions
- All 200 tests pass

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 13:38:57 -07:00
10 changed files with 769 additions and 2540 deletions
@@ -74,11 +74,13 @@ scala_library(
"//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",
],
@@ -34,6 +34,7 @@ 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}
@@ -231,9 +232,25 @@ case class EngineImpl(
results = actionResultProtoApplier.applyActionResults(
startingState = currentState,
results = ResolveBattleAction(
startingGameState = currentState,
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
),
resolvedBattles = Vector(battleResolution)
).execute(actionResultProtoApplier).map(_.actionResult)
).results.map(_.asInstanceOf[ActionResult])
)
)
@@ -1591,30 +1591,20 @@ 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/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/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: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",
@@ -1625,22 +1615,13 @@ 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:battalion_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/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: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/quest",
"//src/main/scala/net/eagle0/eagle/model/state/quest/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/shardok_interface:battle_resolution",
"//src/main/scala/net/eagle0/eagle/shardok_interface:resolved_eagle_unit",
@@ -1,377 +1,81 @@
package net.eagle0.eagle.library.actions.impl.action
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.model.state.unit_status.UnitStatus as ModelUnitStatus
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.proto_converters.ArmyConverter
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
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.{
UNIT_STATUS_CAPTURED,
UNIT_STATUS_FLED,
UNIT_STATUS_NEVER_ENTERED,
UNIT_STATUS_NORMAL,
UNIT_STATUS_OUTLAWED,
UNIT_STATUS_RETREATED
}
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.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.actions.impl.common.ProtolessSequentialResultsAction
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.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.proto_converters.{
ActionResultProtoConverter,
ArmyConverter,
ChangedProvinceConverter
}
import net.eagle0.eagle.model.state.province.ProvinceT
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.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.model.state.unit_status.UnitStatus as ModelUnitStatus
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
import net.eagle0.eagle.library.actions.impl.action.{
ProvinceConqueredAction,
ProvinceHeldAction
}
case class ResolveBattleAction(
startingGameState: GameState,
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],
resolvedBattles: Vector[BattleResolution]
) extends Action {
) extends ProtolessSequentialResultsAction {
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 execute(
actionResultProtoApplier: ActionResultProtoApplier
): Vector[ActionWithResultingState] =
override def results: Vector[ActionResultT] = {
for {
rb <- resolvedBattles
sb <- outstandingBattle(
startingGameState.outstandingBattles.toVector,
rb
).toVector
res <- resolveOneBattle(
startingState = startingGameState,
actionResultProtoApplier = actionResultProtoApplier,
battle = sb,
battleResolution = rb
)
sb <- outstandingBattles
.find(_.shardokGameId == rb.battle.shardokGameId)
.toVector
res <- resolveOneBattleProtoless(sb, rb)
} yield res
}
private def newOutlaws(
battleResolution: BattleResolution
): Vector[UnaffiliatedHero] =
(for {
rsp <- battleResolution.resolvedPlayers
outlaw <- rsp.units.filter(_.status == ModelUnitStatus.Outlawed)
} 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 defenderCP(
battle: ShardokBattle,
battleResolution: BattleResolution
): 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.optionalBattalionId).flatten.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 = {
def fleeProvince(hero: HeroT, battle: ShardokBattle): Option[ProvinceId] =
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}"
)
.find(_.eagleFid == hero.factionId.get)
.flatMap(_.armyGroup)
.flatMap(_.armies.find(_.army.units.exists(_.heroId == hero.id)))
.flatMap(_.army.fleeProvinceId)
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,
private def resolveOneBattleProtoless(
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 = startingState.heroes(u.hero.id),
newBattalion = u.optionalBattalionId.map(startingState.battalions),
status = u.status match {
case ModelUnitStatus.Normal => UNIT_STATUS_NORMAL
case ModelUnitStatus.Captured => UNIT_STATUS_CAPTURED
case ModelUnitStatus.Fled => UNIT_STATUS_FLED
case ModelUnitStatus.Retreated => UNIT_STATUS_RETREATED
case ModelUnitStatus.NeverEntered => UNIT_STATUS_NEVER_ENTERED
case ModelUnitStatus.Outlawed => UNIT_STATUS_OUTLAWED
},
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)
.flatMap(_.battalion)
.filterNot(_.id == 0)
.filterNot(_.size == 0)
.map(BattalionConverter.toProto)
.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] = {
): Vector[ActionResultT] = {
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.BATTLE_TYPE_UNKNOWN =>
case BattleType.Unknown =>
throw new EagleInternalException("Unknown battle type")
case BattleType.BATTLE_TYPE_ASSAULT_PROVINCE =>
case BattleType.AssaultProvince =>
internalRequire(
winningShardokPlayers.size == 1,
s"Assault province battle type must have exactly one victor"
@@ -379,160 +83,57 @@ 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 == ModelUnitStatus.Outlawed)
}
ProvinceHeldAction(
gameId = gs.gameId,
currentRoundId = gs.currentRoundId,
defendingProvince = ProvinceConverter.fromProto(defendingProv),
gameId = gameId,
currentRoundId = currentRoundId,
defendingProvince = provinces(battle.defenderProvince),
attackingFactionIds = losingResolvedPlayers.map(_.eagleFid),
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 {
val gs = battleEndedResults.last.gameState
val conqueredProv = gs.provinces(battle.defenderProvince)
capturedHeroIds =
Vector.empty, // TODO: implement captured units logic
destroyedBattalionIds = destroyedBattalionIds(battleResolution),
heroFactions = Map.empty // TODO: implement hero factions
).immediateExecute
else
ProvinceConqueredAction(
gameId = gs.gameId,
currentRoundId = gs.currentRoundId,
currentDate = DateConverter.fromProto(gs.currentDate),
conqueredProvince = ProvinceConverter.fromProto(conqueredProv),
notFledConquerors = winningResolvedPlayers.head.units
.filterNot(u =>
unitReturned(
hid = u.hero.id,
in = wsp,
out = winningResolvedPlayers.head,
battleType = BATTLE_TYPE_ASSAULT_PROVINCE
)
)
.filterNot(_.status == ModelUnitStatus.Outlawed)
.map(u =>
ResolvedEagleUnit(
hero =
HeroConverter.fromProto(startingState.heroes(u.hero.id)),
battalion = u.battalion.map(b =>
BattalionConverter.fromProto(startingState.battalions(b.id))
),
status = u.status
)
)
.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 ResolvedEagleUnit(
hero = HeroConverter.fromProto(startingState.heroes(u.hero.id)),
battalion = u.battalion.map(b =>
BattalionConverter.fromProto(startingState.battalions(b.id))
),
status = u.status
)).toVector,
gameId = gameId,
currentRoundId = currentRoundId,
currentDate = currentDate,
conqueredProvince = provinces(battle.defenderProvince),
notFledConquerors = Vector.empty, // TODO: implement
notFledDefenders = Vector.empty, // TODO: implement
destroyedBattalions = destroyedBattalionIds(battleResolution),
defendingFactionId =
battle.players.find(_.isDefender).map(_.eagleFid),
factions =
gs.factions.view.mapValues(FactionConverter.fromProto).toMap,
heroes = gs.heroes.view.mapValues(HeroConverter.fromProto).toMap,
battalions =
gs.battalions.view.mapValues(BattalionConverter.fromProto).toMap,
provinces =
gs.provinces.view.mapValues(ProvinceConverter.fromProto).toMap
)
}
factions = factions,
heroes = heroes,
battalions = battalions,
provinces = provinces
).immediateExecute
end if
case BattleType.BATTLE_TYPE_FREE_FOR_ALL =>
case BattleType.FreeForAll =>
Option
.when(winningShardokPlayers.nonEmpty) {
WonFreeForAllAction(
battalions = battleEndedResults.last.gameState.battalions.view
.mapValues(BattalionConverter.fromProto)
.toMap,
battleProvince = ProvinceConverter.fromProto(
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,
winningArmyGroups = winningShardokPlayers.flatMap(
_.armyGroup.map(ArmyConverter.fromProto)
)
)
battalions = battalions,
battleProvince = provinces(battle.defenderProvince),
winningUnits = winningResolvedPlayers.flatMap(_.units),
winningArmyGroups =
winningShardokPlayers.flatMap(_.armyGroup).toVector
).immediateExecute
}
.getOrElse(
FreeForAllDrawAction(
defenderProvince = battle.defenderProvince,
armiesFromPlayers = battle.players
.flatMap(
_.armyGroup
.map(_.armies.map(ArmyConverter.fromProto))
.getOrElse(Vector.empty)
)
.toVector,
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
)
)
}
)
armiesFromPlayers =
battle.players.flatMap(_.armyGroup).flatMap(_.armies).toVector,
remainingUnits = battleResolution.resolvedPlayers.flatMap(_.units)
).immediateExecute
)
case BattleType.BATTLE_TYPE_SALLY =>
case BattleType.Sally =>
throw new EagleInternalException("Sally battle unimplemented")
case BattleType.Unrecognized(value) =>
throw new EagleInternalException(s"Unrecognized battle type $value")
}
battleEndedResults :+ actionResultProtoApplier
.applyActionResult(
battleEndedResults.last.gameState,
ActionResultProtoConverter.toProto(finalResult.immediateExecute)
)
Vector(finalResult)
}
def destroyedBattalionIds(
@@ -546,215 +147,9 @@ case class ResolveBattleAction(
.filterNot(_ == 0) ++ battleResolution.resolvedPlayers
.flatMap(_.units)
.filter(unit =>
unit.status == ModelUnitStatus.Captured || unit.status == ModelUnitStatus.Outlawed
unit.status == UnitStatus.Captured || unit.status == UnitStatus.Outlawed
)
.flatMap(_.optionalBattalionId)
.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: net.eagle0.eagle.internal.event_for_hero_backstory.FoughtInBattleBackstoryEvent.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 ModelUnitStatus.Fled => true
case ModelUnitStatus.NeverEntered =>
!in.isDefender &&
in.getArmyGroup.armies
.find(_.getArmy.units.exists(_.heroId == hid))
.get
.getArmy
.fleeProvinceId
.isDefined &&
!out.endGameCondition.condition.isVictory
case ModelUnitStatus.Captured =>
out.endGameCondition.condition.isDraw || out.endGameCondition.condition.isAllyVictory
case ModelUnitStatus.Normal =>
out.endGameCondition.condition.isDraw || out.endGameCondition.condition.isAllyVictory
case ModelUnitStatus.Retreated => false
case ModelUnitStatus.Outlawed => false
}
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
}
}
@@ -4,8 +4,10 @@ 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__",
],
deps = [
"//src/main/protobuf/net/eagle0/common:victory_condition_scala_proto",
@@ -4,6 +4,7 @@ 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__",
@@ -11,6 +12,7 @@ scala_library(
"//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 = [
@@ -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",
],
)
@@ -1,6 +1,6 @@
package net.eagle0.eagle.shardok_interface
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle
import net.eagle0.eagle.model.state.shardok_battle.ShardokBattle
case class BattleResolution(
battle: ShardokBattle,
@@ -57,6 +57,16 @@ 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",
@@ -831,34 +841,36 @@ scala_test(
name = "resolve_battle_action_test",
srcs = ["ResolveBattleActionTest.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/protobuf/net/eagle0/common:victory_condition_scala_proto",
"//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/library/actions/impl/action:resolve_battle_action",
"//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/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
"//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/shardok_interface:battle_resolution",
"//src/main/scala/net/eagle0/eagle/shardok_interface:eagle_unit",
"//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",
],
)