mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Fix tribute co-attacker phase advancement (#8713)
This commit is contained in:
+9
-9
@@ -255,9 +255,9 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
|
||||
/**
|
||||
* Sequential attack decision logic:
|
||||
* - Sort undecided army groups by troop count (largest first, tiebreak by factionId)
|
||||
* - If no army has advanced yet: only the largest undecided army gets the command
|
||||
* - If an army has advanced: only allied undecided armies get a choice to join; hostile undecided armies are
|
||||
* auto-bounced at end of phase
|
||||
* - If no army has committed to attack yet: only the largest undecided army gets the command
|
||||
* - If an army has advanced or demanded tribute: only allied undecided armies get a choice to join; hostile
|
||||
* undecided armies are auto-bounced at end of phase
|
||||
*/
|
||||
private def isMyTurnToDecide(
|
||||
pid: ProvinceId,
|
||||
@@ -267,16 +267,16 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
|
||||
val province = gs.provinces(pid)
|
||||
val hostileArmies = province.hostileArmies
|
||||
|
||||
val advancedArmies = hostileArmies.filter(_.status == HostileArmyGroupStatus.Attacking)
|
||||
val committedArmies = hostileArmies.filter(group => HostileArmyGroupStatus.isCommittedToAttack(group.status))
|
||||
val undecidedArmies = hostileArmies.filter(_.status == HostileArmyGroupStatus.AwaitingDecision)
|
||||
|
||||
undecidedArmies.exists(_.factionId == fid) && {
|
||||
if advancedArmies.nonEmpty then {
|
||||
// Someone has already advanced - only allies can join
|
||||
val advancedFids = advancedArmies.map(_.factionId)
|
||||
advancedFids.forall(advFid => areAllied(fid, advFid, gs))
|
||||
if committedArmies.nonEmpty then {
|
||||
// Someone has already committed to the attack - only allies can join
|
||||
val committedFids = committedArmies.map(_.factionId)
|
||||
committedFids.forall(committedFid => areAllied(fid, committedFid, gs))
|
||||
} else {
|
||||
// No one has advanced yet - only the largest undecided army gets the command
|
||||
// No one has committed yet - only the largest undecided army gets the command
|
||||
val sortedUndecided = undecidedArmies
|
||||
.sortBy(g => armyGroupSortKey(g, gs))
|
||||
|
||||
|
||||
+13
-15
@@ -42,26 +42,24 @@ case class EndAttackDecisionPhaseAction(
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-bounce: For occupied provinces where an army has advanced, set remaining hostile AwaitingDecision armies to
|
||||
* Withdrawing.
|
||||
* Auto-bounce: For occupied provinces where an army has advanced or demanded tribute, set remaining hostile
|
||||
* AwaitingDecision armies to Withdrawing.
|
||||
*/
|
||||
private def autoBounceResults: Vector[ActionResultT] =
|
||||
provinces.flatMap { p =>
|
||||
val hasAttacker = p.hostileArmies.exists(_.status == HostileArmyGroupStatus.Attacking)
|
||||
val isOccupied = p.rulingFactionId.nonEmpty
|
||||
val undecidedFids = p.hostileArmies
|
||||
val committedAttackerFids = p.hostileArmies
|
||||
.filter(group => HostileArmyGroupStatus.isCommittedToAttack(group.status))
|
||||
.map(_.factionId)
|
||||
val isOccupied = p.rulingFactionId.nonEmpty
|
||||
val undecidedFids = p.hostileArmies
|
||||
.filter(_.status == HostileArmyGroupStatus.AwaitingDecision)
|
||||
.map(_.factionId)
|
||||
|
||||
if hasAttacker && isOccupied && undecidedFids.nonEmpty then {
|
||||
// Check which undecided armies are hostile to the advancing army
|
||||
val attackingFids = p.hostileArmies
|
||||
.filter(_.status == HostileArmyGroupStatus.Attacking)
|
||||
.map(_.factionId)
|
||||
|
||||
if committedAttackerFids.nonEmpty && isOccupied && undecidedFids.nonEmpty then {
|
||||
// Check which undecided armies are hostile to the committed attackers
|
||||
val hostileUndecided = undecidedFids.filter { fid =>
|
||||
!attackingFids.exists(advFid =>
|
||||
fid == advFid || FactionUtils.hasAlliance(fid, advFid, factions.values.toVector)
|
||||
!committedAttackerFids.forall(committedFid =>
|
||||
fid == committedFid || FactionUtils.hasAlliance(fid, committedFid, factions.values.toVector)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,8 +81,8 @@ case class EndAttackDecisionPhaseAction(
|
||||
)
|
||||
}
|
||||
.toVector
|
||||
} else if !hasAttacker && undecidedFids.size > 1 && hasNonAlliedPair(undecidedFids) then {
|
||||
// No army advanced but non-allied factions remain — bounce all undecided
|
||||
} else if committedAttackerFids.isEmpty && undecidedFids.size > 1 && hasNonAlliedPair(undecidedFids) then {
|
||||
// No army committed but non-allied factions remain — bounce all undecided
|
||||
Vector(
|
||||
ActionResultC(
|
||||
actionResultType = EndAttackDecisionPhase,
|
||||
|
||||
@@ -34,4 +34,10 @@ object HostileArmyGroupStatus {
|
||||
case object Attacking extends HostileArmyGroupStatus
|
||||
case object Withdrawing extends HostileArmyGroupStatus
|
||||
case class SafePassageGranted(destinationProvinceId: ProvinceId) extends HostileArmyGroupStatus
|
||||
|
||||
def isCommittedToAttack(status: HostileArmyGroupStatus): Boolean =
|
||||
status match {
|
||||
case Attacking | _: TributeDemanded => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
+40
-1
@@ -6,7 +6,15 @@ import net.eagle0.eagle.library.settings.{
|
||||
MinimumAgilityForStartFire,
|
||||
MinimumWisdomForStartFire
|
||||
}
|
||||
import net.eagle0.eagle.model.state.{Army, CombatUnit, HostileArmyGroup, HostileArmyGroupStatus, MovingArmy, Supplies}
|
||||
import net.eagle0.eagle.model.state.{
|
||||
Army,
|
||||
CombatUnit,
|
||||
HostileArmyGroup,
|
||||
HostileArmyGroupStatus,
|
||||
MovingArmy,
|
||||
Supplies,
|
||||
TributeAmount
|
||||
}
|
||||
import net.eagle0.eagle.model.state.{GameType, RoundPhase}
|
||||
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{
|
||||
@@ -636,6 +644,37 @@ class AvailableAttackDecisionCommandFactoryTest extends AnyFlatSpec with Matcher
|
||||
result shouldBe empty
|
||||
}
|
||||
|
||||
it should "block hostile army when another army has demanded tribute" in {
|
||||
val hostileFid = attackerFid + defenderFid + 1
|
||||
val defenderProvince = inside(baseGameState.provinces(defenderProvinceId)) { case province: ProvinceC => province }
|
||||
|
||||
val gs = baseGameState.copy(
|
||||
provinces = baseGameState.provinces.updated(
|
||||
defenderProvinceId,
|
||||
defenderProvince.copy(
|
||||
hostileArmies = Vector(
|
||||
defenderProvince.hostileArmies.head.copy(
|
||||
status = HostileArmyGroupStatus.TributeDemanded(TributeAmount(gold = 267, food = 4744))
|
||||
),
|
||||
HostileArmyGroup(
|
||||
factionId = hostileFid,
|
||||
status = HostileArmyGroupStatus.AwaitingDecision,
|
||||
armies = Vector.empty
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
factions = baseGameState.factions + (hostileFid -> FactionC(
|
||||
id = hostileFid,
|
||||
factionHeadId = 9382,
|
||||
name = "",
|
||||
leaderIds = Vector.empty
|
||||
))
|
||||
)
|
||||
|
||||
AvailableAttackDecisionCommandFactory.availableCommand(gs, hostileFid, defenderProvinceId) shouldBe empty
|
||||
}
|
||||
|
||||
it should "allow allied army to join when ally has advanced" in {
|
||||
val alliedFid = attackerFid + defenderFid + 1
|
||||
val defenderProvince = inside(baseGameState.provinces(defenderProvinceId)) { case province: ProvinceC => province }
|
||||
|
||||
+114
-1
@@ -13,7 +13,8 @@ import net.eagle0.eagle.model.state.{
|
||||
HostileArmyGroupStatus,
|
||||
MovingArmy,
|
||||
RoundPhase,
|
||||
Supplies
|
||||
Supplies,
|
||||
TributeAmount
|
||||
}
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.faction.concrete.FactionC
|
||||
@@ -363,6 +364,47 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
|
||||
}
|
||||
}
|
||||
|
||||
it should "bounce a hostile undecided army after tribute is demanded" in {
|
||||
val provinces: Vector[ProvinceT] = Vector(
|
||||
ProvinceC(
|
||||
id = 4,
|
||||
rulingFactionId = Some(31),
|
||||
hostileArmies = Vector(
|
||||
HostileArmyGroup(
|
||||
factionId = 9,
|
||||
status = HostileArmyGroupStatus.TributeDemanded(TributeAmount(gold = 267, food = 4744)),
|
||||
armies = Vector.empty
|
||||
),
|
||||
HostileArmyGroup(
|
||||
factionId = 15,
|
||||
status = HostileArmyGroupStatus.AwaitingDecision,
|
||||
armies = Vector.empty
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val factions: Map[FactionId, FactionC] = Map(
|
||||
9 -> FactionC(id = 9, factionHeadId = 3, name = "", leaderIds = Vector.empty),
|
||||
15 -> FactionC(id = 15, factionHeadId = 7, name = "", leaderIds = Vector.empty)
|
||||
)
|
||||
|
||||
val statusChanges = EndAttackDecisionPhaseAction(
|
||||
gameId = gameId,
|
||||
currentRoundId = currentRoundId,
|
||||
currentDate = currentDate,
|
||||
provinces = provinces,
|
||||
factions = factions
|
||||
).results
|
||||
.flatMap(_.changedProvinces)
|
||||
.collect { case cp: ChangedProvinceC => cp }
|
||||
.flatMap(_.hostileArmyStatusChanges)
|
||||
|
||||
statusChanges should contain(
|
||||
HostileArmyStatusChange(factionId = 15, newStatus = HostileArmyGroupStatus.Withdrawing)
|
||||
)
|
||||
}
|
||||
|
||||
it should "not bounce allied undecided armies" in {
|
||||
val provinces: Vector[ProvinceT] = Vector(
|
||||
ProvinceC(
|
||||
@@ -454,6 +496,77 @@ class EndAttackDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
|
||||
bounceResults shouldBe empty
|
||||
}
|
||||
|
||||
it should "bounce an undecided army allied with only one committed attacker" in {
|
||||
val provinces: Vector[ProvinceT] = Vector(
|
||||
ProvinceC(
|
||||
id = 4,
|
||||
rulingFactionId = Some(31),
|
||||
hostileArmies = Vector(
|
||||
HostileArmyGroup(factionId = 9, status = Attacking, armies = Vector.empty),
|
||||
HostileArmyGroup(
|
||||
factionId = 15,
|
||||
status = HostileArmyGroupStatus.TributeDemanded(TributeAmount(gold = 50, food = 75)),
|
||||
armies = Vector.empty
|
||||
),
|
||||
HostileArmyGroup(
|
||||
factionId = 17,
|
||||
status = HostileArmyGroupStatus.AwaitingDecision,
|
||||
armies = Vector.empty
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val factions: Map[FactionId, FactionC] = Map(
|
||||
9 -> FactionC(
|
||||
id = 9,
|
||||
factionHeadId = 3,
|
||||
name = "",
|
||||
leaderIds = Vector.empty,
|
||||
factionRelationships = Vector(
|
||||
FactionRelationship(targetFactionId = 15, relationshipLevel = RelationshipLevel.Ally, resetDate = None),
|
||||
FactionRelationship(targetFactionId = 17, relationshipLevel = RelationshipLevel.Ally, resetDate = None)
|
||||
)
|
||||
),
|
||||
15 -> FactionC(
|
||||
id = 15,
|
||||
factionHeadId = 7,
|
||||
name = "",
|
||||
leaderIds = Vector.empty,
|
||||
factionRelationships = Vector(
|
||||
FactionRelationship(targetFactionId = 9, relationshipLevel = RelationshipLevel.Ally, resetDate = None)
|
||||
)
|
||||
),
|
||||
17 -> FactionC(
|
||||
id = 17,
|
||||
factionHeadId = 8,
|
||||
name = "",
|
||||
leaderIds = Vector.empty,
|
||||
factionRelationships = Vector(
|
||||
FactionRelationship(targetFactionId = 9, relationshipLevel = RelationshipLevel.Ally, resetDate = None)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val results =
|
||||
EndAttackDecisionPhaseAction(
|
||||
gameId = gameId,
|
||||
currentRoundId = currentRoundId,
|
||||
currentDate = currentDate,
|
||||
provinces = provinces,
|
||||
factions = factions
|
||||
).results
|
||||
|
||||
val statusChanges = results
|
||||
.flatMap(_.changedProvinces)
|
||||
.collect { case cp: ChangedProvinceC => cp }
|
||||
.flatMap(_.hostileArmyStatusChanges)
|
||||
|
||||
statusChanges should contain(
|
||||
HostileArmyStatusChange(factionId = 17, newStatus = HostileArmyGroupStatus.Withdrawing)
|
||||
)
|
||||
}
|
||||
|
||||
"auto-bounce with no attacker" should "bounce all non-allied undecided armies" in {
|
||||
val provinces: Vector[ProvinceT] = Vector(
|
||||
ProvinceC(
|
||||
|
||||
Reference in New Issue
Block a user