mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Make AI attack marches more aggressive (#8738)
* Make AI attack marches more aggressive * Test aggressive AI march sequence
This commit is contained in:
+4
-5
@@ -2,6 +2,7 @@ package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
MaxCombatUnitCountPerSide,
|
||||
MinVigorForMarch,
|
||||
RequiredRatioToAttack,
|
||||
RequiredRatioToAttackWithoutTakingCastles
|
||||
@@ -115,12 +116,10 @@ object AttackCommandChooser {
|
||||
heroesWithSufficientVigorForAttack(opmc.originProvinceId, gameState)
|
||||
|
||||
val unitCount = Vector(
|
||||
Math.max(
|
||||
(heroes.length + 1) / 2,
|
||||
ShardokMapInfo.shardokMapInfos(destinationProvince.id).castleCount
|
||||
),
|
||||
heroes.length,
|
||||
originProvince.rulingFactionHeroIds.length - 1, // Don't abandon the origin province
|
||||
battalions.length
|
||||
battalions.length,
|
||||
MaxCombatUnitCountPerSide.intValue
|
||||
).min
|
||||
|
||||
val selectedUnits = CombatUnitSelector.selectedCombatBattalions(
|
||||
|
||||
@@ -72,6 +72,7 @@ scala_library(
|
||||
srcs = ["AttackCommandChooser.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
@@ -82,6 +83,7 @@ scala_library(
|
||||
":combat_unit_selector",
|
||||
":march_supplies_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_march",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:required_ratio_to_attack",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:required_ratio_to_attack_without_taking_castles",
|
||||
|
||||
+149
-2
@@ -1,12 +1,27 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.library.settings.{MaxCombatUnitCountPerSide, MaxFoodTribute, MaxGoldTribute, MinVigorForMarch}
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplierImpl
|
||||
import net.eagle0.eagle.library.actions.impl.command.MarchCommand
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
ActionVigorCost,
|
||||
MarchWisdomXp,
|
||||
MaxCombatUnitCountPerSide,
|
||||
MaxFoodTribute,
|
||||
MaxGoldTribute,
|
||||
MinVigorForMarch,
|
||||
RequiredRatioToAttack,
|
||||
RequiredRatioToAttackWithoutTakingCastles,
|
||||
TurnsToResolveConquest
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AttackCommandChooser
|
||||
import net.eagle0.eagle.library.util.ShardokMapInfo
|
||||
import net.eagle0.eagle.model.state.{GameType, RoundPhase}
|
||||
import net.eagle0.eagle.model.state.{CombatUnit as ModelCombatUnit, GameType, RoundPhase}
|
||||
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.MarchSelected
|
||||
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.hero.concrete.HeroC
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
@@ -141,6 +156,11 @@ class AvailableMarchCommandFactoryTest extends AnyFlatSpec with BeforeAndAfterEa
|
||||
MaxCombatUnitCountPerSide.setIntValue(10)
|
||||
MaxGoldTribute.setIntValue(5555)
|
||||
MaxFoodTribute.setIntValue(6666)
|
||||
ActionVigorCost.setIntValue(15)
|
||||
MarchWisdomXp.setIntValue(7)
|
||||
RequiredRatioToAttack.setDoubleValue(1.5)
|
||||
RequiredRatioToAttackWithoutTakingCastles.setDoubleValue(2.5)
|
||||
TurnsToResolveConquest.setIntValue(2)
|
||||
}
|
||||
|
||||
"availableCommand" should "include neighbor provinces ruled by other players as available destinations" in {
|
||||
@@ -683,4 +703,131 @@ class AvailableMarchCommandFactoryTest extends AnyFlatSpec with BeforeAndAfterEa
|
||||
// The requiresRiverCrossing should be a boolean value (we don't need to test the exact logic here)
|
||||
destination.requiresRiverCrossing shouldBe a[Boolean]
|
||||
}
|
||||
|
||||
it should "leave no units behind for a same-origin support attack" in {
|
||||
val originProvinceId = 2
|
||||
val destinationProvinceId = 5
|
||||
val factionHeadProvinceId = 14
|
||||
val factionHeadId = 9
|
||||
val localLeaderId = 1
|
||||
val enemyFactionId = 6
|
||||
val enemyLeaderId = 50
|
||||
val originHeroIds = (1 to 8).toVector
|
||||
val attackBattalions = (101 to 107).toVector.map { id =>
|
||||
BattalionC(id = id, size = 100, typeId = LightInfantry)
|
||||
}
|
||||
val attackHeroes = originHeroIds.map { id =>
|
||||
HeroC(id = id, vigor = 100, factionId = Some(actingPlayerId))
|
||||
}
|
||||
val gameState = makeGameState(
|
||||
provinces = Map(
|
||||
originProvinceId -> ProvinceC(
|
||||
id = originProvinceId,
|
||||
neighbors = Vector(Neighbor(destinationProvinceId, startingPositionIndex = 7)),
|
||||
rulingFactionId = Some(actingPlayerId),
|
||||
rulingHeroId = Some(localLeaderId),
|
||||
rulingFactionHeroIds = originHeroIds,
|
||||
battalionIds = attackBattalions.map(_.id),
|
||||
gold = 5000,
|
||||
food = 5000
|
||||
),
|
||||
destinationProvinceId -> ProvinceC(
|
||||
id = destinationProvinceId,
|
||||
neighbors = Vector(Neighbor(originProvinceId, startingPositionIndex = 3)),
|
||||
rulingFactionId = Some(enemyFactionId),
|
||||
rulingHeroId = Some(enemyLeaderId),
|
||||
rulingFactionHeroIds = Vector(enemyLeaderId)
|
||||
),
|
||||
factionHeadProvinceId -> ProvinceC(
|
||||
id = factionHeadProvinceId,
|
||||
rulingFactionId = Some(actingPlayerId),
|
||||
rulingHeroId = Some(factionHeadId),
|
||||
rulingFactionHeroIds = Vector(factionHeadId)
|
||||
)
|
||||
),
|
||||
heroes = heroMap(
|
||||
attackHeroes ++ Vector(
|
||||
HeroC(id = factionHeadId, vigor = 100, factionId = Some(actingPlayerId)),
|
||||
HeroC(id = enemyLeaderId, vigor = 100, factionId = Some(enemyFactionId))
|
||||
)
|
||||
),
|
||||
battalions = battalionMap(attackBattalions),
|
||||
battalionTypes = battalionTypes
|
||||
).copy(
|
||||
factions = Map(
|
||||
actingPlayerId -> FactionC(
|
||||
id = actingPlayerId,
|
||||
factionHeadId = factionHeadId,
|
||||
name = "Acting faction",
|
||||
leaderIds = Vector(factionHeadId, localLeaderId)
|
||||
),
|
||||
enemyFactionId -> FactionC(
|
||||
id = enemyFactionId,
|
||||
factionHeadId = enemyLeaderId,
|
||||
name = "Enemy faction",
|
||||
leaderIds = Vector(enemyLeaderId)
|
||||
)
|
||||
)
|
||||
)
|
||||
val initialAvailable = getMarchCommand(
|
||||
AvailableMarchCommandFactory.availableCommand(
|
||||
gameState,
|
||||
actingPlayerId,
|
||||
originProvinceId
|
||||
)
|
||||
)
|
||||
val initialSelection = AttackCommandChooser
|
||||
.chosenAttackCommand(
|
||||
actingFactionId = actingPlayerId,
|
||||
gameState = gameState,
|
||||
acs = Vector(initialAvailable),
|
||||
knownEnemyHeroCount = _ => 1,
|
||||
knownEnemyBattalions = _ => Vector.empty
|
||||
)
|
||||
.get
|
||||
val initialMarch = inside(initialSelection.selected) { case march: MarchSelected => march }
|
||||
|
||||
initialMarch.marchingUnits should have size 7
|
||||
initialMarch.marchingUnits.map(_.heroId) should contain(localLeaderId)
|
||||
initialMarch.marchingUnits.flatMap(_.battalionId).toSet shouldBe attackBattalions.map(_.id).toSet
|
||||
|
||||
val commandForOrigin = initialAvailable.oneProvinceCommands
|
||||
.find(_.originProvinceId == originProvinceId)
|
||||
.get
|
||||
val marchResult = MarchCommand
|
||||
.make(
|
||||
actingFactionId = actingPlayerId,
|
||||
actingProvinceId = initialAvailable.actingProvinceId,
|
||||
originProvinceId = initialMarch.originProvince,
|
||||
destinationProvinceId = initialMarch.destinationProvinceId,
|
||||
marchingUnits =
|
||||
initialMarch.marchingUnits.map(unit => ModelCombatUnit(actingPlayerId, unit.heroId, unit.battalionId)),
|
||||
gold = initialMarch.gold,
|
||||
food = initialMarch.food,
|
||||
availableDestinationProvinceIds = commandForOrigin.availableDestinationProvinces.map(_.provinceId),
|
||||
availableHeroIds = commandForOrigin.availableHeroIds,
|
||||
availableBattalionIds = commandForOrigin.availableBattalions.map(_.battalionId),
|
||||
goldAvailable = commandForOrigin.goldAvailable,
|
||||
foodAvailable = commandForOrigin.foodAvailable,
|
||||
currentRoundId = gameState.currentRoundId
|
||||
)
|
||||
.immediateExecute
|
||||
val stateAfterMarch = ActionResultApplierImpl(None)
|
||||
.applyActionResult(gameState, marchResult)
|
||||
.resultingState
|
||||
val supportAvailable = getMarchCommand(
|
||||
AvailableMarchCommandFactory.availableCommand(
|
||||
stateAfterMarch,
|
||||
actingPlayerId,
|
||||
factionHeadProvinceId
|
||||
)
|
||||
)
|
||||
|
||||
supportAvailable.oneProvinceCommands.map(_.originProvinceId) should contain(originProvinceId)
|
||||
AttackCommandChooser.chosenSupportAttackCommand(
|
||||
actingFactionId = actingPlayerId,
|
||||
gameState = stateAfterMarch,
|
||||
acs = Vector(supportAvailable)
|
||||
) shouldBe empty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,19 +375,33 @@ scala_test(
|
||||
srcs = ["AvailableMarchCommandFactoryTest.scala"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:available_march_command_factory",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:march_command",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:action_vigor_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:march_wisdom_xp",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
|
||||
"//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/settings:required_ratio_to_attack",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:required_ratio_to_attack_without_taking_castles",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:turns_to_resolve_conquest",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:shardok_map_info",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:attack_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//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:combat_unit",
|
||||
"//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/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//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/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
|
||||
|
||||
+24
-3
@@ -225,7 +225,7 @@ class AttackCommandChooserTest extends AnyFlatSpec with Matchers with BeforeAndA
|
||||
|
||||
behavior of "AttackCommandChooserTest"
|
||||
|
||||
"chosenAttackCommand" should "return an attack on a hostile neighbor if we have enough troops" in {
|
||||
"chosenAttackCommand" should "take the largest army that leaves a hero behind" in {
|
||||
val generatedCommand = AttackCommandChooser
|
||||
.chosenAttackCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
@@ -247,7 +247,8 @@ class AttackCommandChooserTest extends AnyFlatSpec with Matchers with BeforeAndA
|
||||
destinationProvinceId = destinationProvinceId,
|
||||
marchingUnits = Vector(
|
||||
CombatUnit(heroId = 1, battalionId = Some(15)),
|
||||
CombatUnit(heroId = 2, battalionId = Some(25))
|
||||
CombatUnit(heroId = 2, battalionId = Some(25)),
|
||||
CombatUnit(heroId = 3, battalionId = Some(35))
|
||||
)
|
||||
),
|
||||
reason = "Marching to attack"
|
||||
@@ -256,6 +257,25 @@ class AttackCommandChooserTest extends AnyFlatSpec with Matchers with BeforeAndA
|
||||
generatedCommand shouldBe expectedCommand
|
||||
}
|
||||
|
||||
it should "respect the maximum combat unit count" in {
|
||||
MaxCombatUnitCountPerSide.setIntValue(2)
|
||||
|
||||
val generatedCommand = AttackCommandChooser
|
||||
.chosenAttackCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
acs = Vector(marchAvailableCommand),
|
||||
knownEnemyHeroCount = _ => 1,
|
||||
knownEnemyBattalions = _ => Vector.empty
|
||||
)
|
||||
.get
|
||||
|
||||
inside(generatedCommand.selected) {
|
||||
case march: MarchSelected =>
|
||||
march.marchingUnits should have size 2
|
||||
}
|
||||
}
|
||||
|
||||
it should "return nothing if the destination province does not have a ruler" in {
|
||||
val destinationProvince = inside(gameState.provinces(destinationProvinceId)) {
|
||||
case province: ProvinceC =>
|
||||
@@ -378,7 +398,8 @@ class AttackCommandChooserTest extends AnyFlatSpec with Matchers with BeforeAndA
|
||||
destinationProvinceId = destinationProvinceId,
|
||||
marchingUnits = Vector(
|
||||
CombatUnit(heroId = 1, battalionId = Some(15)),
|
||||
CombatUnit(heroId = 2, battalionId = Some(25))
|
||||
CombatUnit(heroId = 2, battalionId = Some(25)),
|
||||
CombatUnit(heroId = 3, battalionId = Some(35))
|
||||
)
|
||||
),
|
||||
reason = "Marching to attack"
|
||||
|
||||
Reference in New Issue
Block a user