Let the TheaterCommander manage front line stance.

This improves the AI behavior by choosing the stances non-randomly:

* Breakthrough will be used if the base is expected to be capturable and
  the coalition outnumbers the enemy by 20%.
* Elimination will be used if the coalition has at least as many units
  as the enemy.
* Defensive will be used if the coalition has at least half as many
  units as the enemy.
* Retreat will be used if the coalition is significantly outnumbers.

This also exposes the option to the player.
This commit is contained in:
Dan Albert 2021-07-12 20:31:38 -07:00
parent 575aca5886
commit 0a416ab758
35 changed files with 361 additions and 57 deletions

View File

@ -5,6 +5,7 @@ Saves from 3.x are not compatible with 5.0.
## Features/Improvements ## Features/Improvements
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions. * **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
## Fixes ## Fixes

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from collections import Iterator from collections import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from game.theater import ConflictTheater from game.theater import ConflictTheater, ControlPoint
from game.theater.theatergroundobject import VehicleGroupGroundObject from game.theater.theatergroundobject import VehicleGroupGroundObject
from game.utils import meters, nautical_miles from game.utils import meters, nautical_miles
@ -53,9 +53,7 @@ class Garrisons:
continue continue
for garrison in garrisons: for garrison in garrisons:
# Not sure what distance DCS uses, but assuming it's about 2NM since if meters(garrison.distance_to(cp)) < ControlPoint.CAPTURE_DISTANCE:
# that's roughly the distance of the circle on the map.
if meters(garrison.distance_to(cp)) < nautical_miles(2):
blocking.append(garrison) blocking.append(garrison)
else: else:
defending.append(garrison) defending.append(garrison)

View File

@ -0,0 +1,19 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.compound.destroyenemygroundunits import (
DestroyEnemyGroundUnits,
)
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
from game.theater import FrontLine
@dataclass(frozen=True)
class CaptureBase(CompoundTask[TheaterState]):
front_line: FrontLine
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [BreakthroughAttack(self.front_line, state.player)]
yield [DestroyEnemyGroundUnits(self.front_line)]

View File

@ -0,0 +1,13 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.compound.capturebase import CaptureBase
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@dataclass(frozen=True)
class CaptureBases(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
for front in state.active_front_lines:
yield [CaptureBase(front)]

View File

@ -0,0 +1,19 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.primitive.cas import PlanCas
from game.commander.tasks.primitive.defensivestance import DefensiveStance
from game.commander.tasks.primitive.retreatstance import RetreatStance
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
from game.theater import FrontLine
@dataclass(frozen=True)
class DefendBase(CompoundTask[TheaterState]):
front_line: FrontLine
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [DefensiveStance(self.front_line, state.player)]
yield [RetreatStance(self.front_line, state.player)]
yield [PlanCas(self.front_line)]

View File

@ -0,0 +1,13 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.compound.defendbase import DefendBase
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@dataclass(frozen=True)
class DefendBases(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
for front in state.active_front_lines:
yield [DefendBase(front)]

View File

@ -0,0 +1,17 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.primitive.cas import PlanCas
from game.commander.tasks.primitive.eliminationattack import EliminationAttack
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
from game.theater import FrontLine
@dataclass(frozen=True)
class DestroyEnemyGroundUnits(CompoundTask[TheaterState]):
front_line: FrontLine
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [EliminationAttack(self.front_line, state.player)]
yield [PlanCas(self.front_line)]

View File

@ -1,19 +1,19 @@
from collections import Iterator from collections import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from game.commander.tasks.compound.aewcsupport import PlanAewcSupport
from game.commander.tasks.compound.attackairinfrastructure import ( from game.commander.tasks.compound.attackairinfrastructure import (
AttackAirInfrastructure, AttackAirInfrastructure,
) )
from game.commander.tasks.compound.attackbuildings import AttackBuildings from game.commander.tasks.compound.attackbuildings import AttackBuildings
from game.commander.tasks.compound.attackgarrisons import AttackGarrisons from game.commander.tasks.compound.attackgarrisons import AttackGarrisons
from game.commander.tasks.compound.capturebases import CaptureBases
from game.commander.tasks.compound.defendbases import DefendBases
from game.commander.tasks.compound.degradeiads import DegradeIads from game.commander.tasks.compound.degradeiads import DegradeIads
from game.commander.tasks.compound.frontlinedefense import FrontLineDefense
from game.commander.tasks.compound.interdictreinforcements import ( from game.commander.tasks.compound.interdictreinforcements import (
InterdictReinforcements, InterdictReinforcements,
) )
from game.commander.tasks.compound.protectairspace import ProtectAirSpace from game.commander.tasks.compound.protectairspace import ProtectAirSpace
from game.commander.tasks.compound.refuelingsupport import PlanRefuelingSupport from game.commander.tasks.compound.theatersupport import TheaterSupport
from game.commander.theaterstate import TheaterState from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method from game.htn import CompoundTask, Method
@ -23,10 +23,10 @@ class PlanNextAction(CompoundTask[TheaterState]):
aircraft_cold_start: bool aircraft_cold_start: bool
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [PlanAewcSupport()] yield [TheaterSupport()]
yield [PlanRefuelingSupport()]
yield [ProtectAirSpace()] yield [ProtectAirSpace()]
yield [FrontLineDefense()] yield [CaptureBases()]
yield [DefendBases()]
yield [InterdictReinforcements()] yield [InterdictReinforcements()]
yield [AttackGarrisons()] yield [AttackGarrisons()]
yield [AttackAirInfrastructure(self.aircraft_cold_start)] yield [AttackAirInfrastructure(self.aircraft_cold_start)]

View File

@ -0,0 +1,14 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.compound.aewcsupport import PlanAewcSupport
from game.commander.tasks.compound.refuelingsupport import PlanRefuelingSupport
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@dataclass(frozen=True)
class TheaterSupport(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [PlanAewcSupport()]
yield [PlanRefuelingSupport()]

View File

@ -0,0 +1,75 @@
from __future__ import annotations
import math
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
from game.commander.theaterstate import TheaterState
from game.profiling import MultiEventTracer
from game.theater import FrontLine
from gen.ground_forces.combat_stance import CombatStance
if TYPE_CHECKING:
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
class FrontLineStanceTask(TheaterCommanderTask, ABC):
def __init__(self, front_line: FrontLine, player: bool) -> None:
self.front_line = front_line
self.friendly_cp = self.front_line.control_point_friendly_to(player)
self.enemy_cp = self.front_line.control_point_hostile_to(player)
@property
@abstractmethod
def stance(self) -> CombatStance:
...
@staticmethod
def management_allowed(state: TheaterState) -> bool:
return not state.player or state.stance_automation_enabled
def better_stance_already_set(self, state: TheaterState) -> bool:
current_stance = state.front_line_stances[self.front_line]
if current_stance is None:
return False
preference = (
CombatStance.RETREAT,
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH,
)
current_rating = preference.index(current_stance)
new_rating = preference.index(self.stance)
return current_rating >= new_rating
@property
@abstractmethod
def have_sufficient_front_line_advantage(self) -> bool:
...
@property
def ground_force_balance(self) -> float:
# TODO: Planned CAS missions should reduce the expected opposing force size.
friendly_forces = self.friendly_cp.deployable_front_line_units
enemy_forces = self.enemy_cp.deployable_front_line_units
if enemy_forces == 0:
return math.inf
return friendly_forces / enemy_forces
def preconditions_met(self, state: TheaterState) -> bool:
if not self.management_allowed(state):
return False
if self.better_stance_already_set(state):
return False
return self.have_sufficient_front_line_advantage
def apply_effects(self, state: TheaterState) -> None:
state.front_line_stances[self.front_line] = self.stance
def execute(
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
) -> None:
self.friendly_cp.stances[self.enemy_cp.id] = self.stance

View File

@ -40,6 +40,9 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.flights = [] self.flights = []
def preconditions_met(self, state: TheaterState) -> bool:
return not state.player or state.ato_automation_enabled
def execute( def execute(
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
) -> None: ) -> None:

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass @dataclass
class PlanAewc(PackagePlanningTask[MissionTarget]): class PlanAewc(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
return self.target in state.aewc_targets return self.target in state.aewc_targets
def apply_effects(self, state: TheaterState) -> None: def apply_effects(self, state: TheaterState) -> None:

View File

@ -13,6 +13,8 @@ from gen.flights.flight import FlightType
@dataclass @dataclass
class PlanAntiShip(PackagePlanningTask[NavalGroundObject]): class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.threatening_air_defenses: if self.target not in state.threatening_air_defenses:
return False return False
return self.target_area_preconditions_met(state, ignore_iads=True) return self.target_area_preconditions_met(state, ignore_iads=True)

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass @dataclass
class PlanAntiShipping(PackagePlanningTask[CargoShip]): class PlanAntiShipping(PackagePlanningTask[CargoShip]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.enemy_shipping: if self.target not in state.enemy_shipping:
return False return False
return self.target_area_preconditions_met(state) return self.target_area_preconditions_met(state)

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass @dataclass
class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]): class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.enemy_garrisons: if self.target not in state.enemy_garrisons:
return False return False
return self.target_area_preconditions_met(state) return self.target_area_preconditions_met(state)

View File

@ -19,6 +19,8 @@ class PlanBarcap(TheaterCommanderTask):
target: ControlPoint target: ControlPoint
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if state.player and not state.ato_automation_enabled:
return False
return self.target in state.vulnerable_control_points return self.target in state.vulnerable_control_points
def apply_effects(self, state: TheaterState) -> None: def apply_effects(self, state: TheaterState) -> None:

View File

@ -0,0 +1,37 @@
from __future__ import annotations
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
from game.commander.theaterstate import TheaterState
from game.theater import ControlPoint
from game.theater.theatergroundobject import VehicleGroupGroundObject
from game.utils import meters
from gen.ground_forces.combat_stance import CombatStance
class BreakthroughAttack(FrontLineStanceTask):
@property
def stance(self) -> CombatStance:
return CombatStance.BREAKTHROUGH
@property
def have_sufficient_front_line_advantage(self) -> bool:
return self.ground_force_balance >= 1.2
@property
def opposing_garrisons_eliminated(self) -> bool:
# TODO: Should operate on TheaterState to account for BAIs planned this turn.
for tgo in self.enemy_cp.ground_objects:
if not isinstance(tgo, VehicleGroupGroundObject):
continue
if meters(tgo.distance_to(self.enemy_cp)) < ControlPoint.CAPTURE_DISTANCE:
return False
return True
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
return self.opposing_garrisons_eliminated
def apply_effects(self, state: TheaterState) -> None:
super().apply_effects(state)
state.active_front_lines.remove(self.front_line)

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass @dataclass
class PlanCas(PackagePlanningTask[FrontLine]): class PlanCas(PackagePlanningTask[FrontLine]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
return self.target in state.vulnerable_front_lines return self.target in state.vulnerable_front_lines
def apply_effects(self, state: TheaterState) -> None: def apply_effects(self, state: TheaterState) -> None:

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass @dataclass
class PlanConvoyInterdiction(PackagePlanningTask[Convoy]): class PlanConvoyInterdiction(PackagePlanningTask[Convoy]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.enemy_convoys: if self.target not in state.enemy_convoys:
return False return False
return self.target_area_preconditions_met(state) return self.target_area_preconditions_met(state)

View File

@ -13,6 +13,8 @@ from gen.flights.flight import FlightType
@dataclass @dataclass
class PlanDead(PackagePlanningTask[IadsGroundObject]): class PlanDead(PackagePlanningTask[IadsGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if ( if (
self.target not in state.threatening_air_defenses self.target not in state.threatening_air_defenses
and self.target not in state.detecting_air_defenses and self.target not in state.detecting_air_defenses

View File

@ -0,0 +1,14 @@
from __future__ import annotations
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
from gen.ground_forces.combat_stance import CombatStance
class DefensiveStance(FrontLineStanceTask):
@property
def stance(self) -> CombatStance:
return CombatStance.DEFENSIVE
@property
def have_sufficient_front_line_advantage(self) -> bool:
return self.ground_force_balance >= 0.5

View File

@ -0,0 +1,14 @@
from __future__ import annotations
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
from gen.ground_forces.combat_stance import CombatStance
class EliminationAttack(FrontLineStanceTask):
@property
def stance(self) -> CombatStance:
return CombatStance.ELIMINATION
@property
def have_sufficient_front_line_advantage(self) -> bool:
return self.ground_force_balance >= 1.0

View File

@ -14,6 +14,8 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
aircraft_cold_start: bool aircraft_cold_start: bool
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.oca_targets: if self.target not in state.oca_targets:
return False return False
return self.target_area_preconditions_met(state) return self.target_area_preconditions_met(state)

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass @dataclass
class PlanRefueling(PackagePlanningTask[MissionTarget]): class PlanRefueling(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
return self.target in state.refueling_targets return self.target in state.refueling_targets
def apply_effects(self, state: TheaterState) -> None: def apply_effects(self, state: TheaterState) -> None:

View File

@ -0,0 +1,14 @@
from __future__ import annotations
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
from gen.ground_forces.combat_stance import CombatStance
class RetreatStance(FrontLineStanceTask):
@property
def stance(self) -> CombatStance:
return CombatStance.RETREAT
@property
def have_sufficient_front_line_advantage(self) -> bool:
return True

View File

@ -13,6 +13,8 @@ from gen.flights.flight import FlightType
@dataclass @dataclass
class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]): class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.strike_targets: if self.target not in state.strike_targets:
return False return False
return self.target_area_preconditions_met(state) return self.target_area_preconditions_met(state)

View File

@ -3,12 +3,13 @@ from __future__ import annotations
import dataclasses import dataclasses
import itertools import itertools
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Union from typing import TYPE_CHECKING, Any, Union, Optional
from game.commander.garrisons import Garrisons from game.commander.garrisons import Garrisons
from game.commander.objectivefinder import ObjectiveFinder from game.commander.objectivefinder import ObjectiveFinder
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.htn import WorldState from game.htn import WorldState
from game.settings import AutoAtoBehavior
from game.theater import ControlPoint, FrontLine, MissionTarget from game.theater import ControlPoint, FrontLine, MissionTarget
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
TheaterGroundObject, TheaterGroundObject,
@ -17,6 +18,7 @@ from game.theater.theatergroundobject import (
) )
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from game.transfers import Convoy, CargoShip from game.transfers import Convoy, CargoShip
from gen.ground_forces.combat_stance import CombatStance
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -24,7 +26,12 @@ if TYPE_CHECKING:
@dataclass @dataclass
class TheaterState(WorldState["TheaterState"]): class TheaterState(WorldState["TheaterState"]):
player: bool
stance_automation_enabled: bool
ato_automation_enabled: bool
vulnerable_control_points: list[ControlPoint] vulnerable_control_points: list[ControlPoint]
active_front_lines: list[FrontLine]
front_line_stances: dict[FrontLine, Optional[CombatStance]]
vulnerable_front_lines: list[FrontLine] vulnerable_front_lines: list[FrontLine]
aewc_targets: list[MissionTarget] aewc_targets: list[MissionTarget]
refueling_targets: list[MissionTarget] refueling_targets: list[MissionTarget]
@ -69,7 +76,12 @@ class TheaterState(WorldState["TheaterState"]):
# Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly # Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly
# expensive. # expensive.
return TheaterState( return TheaterState(
player=self.player,
stance_automation_enabled=self.stance_automation_enabled,
ato_automation_enabled=self.ato_automation_enabled,
vulnerable_control_points=list(self.vulnerable_control_points), vulnerable_control_points=list(self.vulnerable_control_points),
active_front_lines=list(self.active_front_lines),
front_line_stances=dict(self.front_line_stances),
vulnerable_front_lines=list(self.vulnerable_front_lines), vulnerable_front_lines=list(self.vulnerable_front_lines),
aewc_targets=list(self.aewc_targets), aewc_targets=list(self.aewc_targets),
refueling_targets=list(self.refueling_targets), refueling_targets=list(self.refueling_targets),
@ -96,8 +108,15 @@ class TheaterState(WorldState["TheaterState"]):
@classmethod @classmethod
def from_game(cls, game: Game, player: bool) -> TheaterState: def from_game(cls, game: Game, player: bool) -> TheaterState:
finder = ObjectiveFinder(game, player) finder = ObjectiveFinder(game, player)
auto_stance = game.settings.automate_front_line_stance
auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
return TheaterState( return TheaterState(
player=player,
stance_automation_enabled=auto_stance,
ato_automation_enabled=auto_ato,
vulnerable_control_points=list(finder.vulnerable_control_points()), vulnerable_control_points=list(finder.vulnerable_control_points()),
active_front_lines=list(finder.front_lines()),
front_line_stances={f: None for f in finder.front_lines()},
vulnerable_front_lines=list(finder.front_lines()), vulnerable_front_lines=list(finder.front_lines()),
aewc_targets=[finder.farthest_friendly_control_point()], aewc_targets=[finder.farthest_friendly_control_point()],
refueling_targets=[finder.closest_friendly_control_point()], refueling_targets=[finder.closest_friendly_control_point()],

View File

@ -24,6 +24,7 @@ from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner import GroundPlanner from gen.ground_forces.ai_ground_planner import GroundPlanner
from . import persistency from . import persistency
from .commander import TheaterCommander
from .debriefing import Debriefing from .debriefing import Debriefing
from .event.event import Event from .event.event import Event
from .event.frontlineattack import FrontlineAttackEvent from .event.frontlineattack import FrontlineAttackEvent
@ -32,7 +33,7 @@ from .income import Income
from .infos.information import Information from .infos.information import Information
from .navmesh import NavMesh from .navmesh import NavMesh
from .procurement import AircraftProcurementRequest, ProcurementAi from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration from .profiling import logged_duration, MultiEventTracer
from .settings import Settings, AutoAtoBehavior from .settings import Settings, AutoAtoBehavior
from .squadrons import AirWing from .squadrons import AirWing
from .theater import ConflictTheater, ControlPoint from .theater import ConflictTheater, ControlPoint
@ -504,13 +505,15 @@ class Game:
with logged_duration("Transport planning"): with logged_duration("Transport planning"):
self.transfers.plan_transports() self.transfers.plan_transports()
if not player or ( color = "Blue" if player else "Red"
player and self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled with MultiEventTracer() as tracer:
): mission_planner = CoalitionMissionPlanner(self, player)
color = "Blue" if player else "Red" with tracer.trace(f"{color} mission planning"):
with logged_duration(f"{color} mission planning"): with tracer.trace(f"{color} mission identification"):
mission_planner = CoalitionMissionPlanner(self, player) commander = TheaterCommander(self, player)
mission_planner.plan_missions() commander.plan_missions(mission_planner, tracer)
with tracer.trace(f"{color} mission fulfillment"):
mission_planner.fulfill_missions()
self.plan_procurement_for(player) self.plan_procurement_for(player)

View File

@ -397,6 +397,7 @@ class Operation:
player_gp, player_gp,
enemy_gp, enemy_gp,
player_cp.stances[enemy_cp.id], player_cp.stances[enemy_cp.id],
enemy_cp.stances[player_cp.id],
cls.unit_map, cls.unit_map,
) )
ground_conflict_gen.generate() ground_conflict_gen.generate()

View File

@ -55,6 +55,7 @@ class Settings:
automate_runway_repair: bool = False automate_runway_repair: bool = False
automate_front_line_reinforcements: bool = False automate_front_line_reinforcements: bool = False
automate_aircraft_reinforcements: bool = False automate_aircraft_reinforcements: bool = False
automate_front_line_stance: bool = True
restrict_weapons_by_date: bool = False restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = True disable_legacy_aewc: bool = True
disable_legacy_tanker: bool = True disable_legacy_tanker: bool = True

View File

@ -271,6 +271,9 @@ class ControlPointStatus(IntEnum):
class ControlPoint(MissionTarget, ABC): class ControlPoint(MissionTarget, ABC):
# Not sure what distance DCS uses, but assuming it's about 2NM since that's roughly
# the distance of the circle on the map.
CAPTURE_DISTANCE = nautical_miles(2)
position = None # type: Point position = None # type: Point
name = None # type: str name = None # type: str
@ -727,6 +730,10 @@ class ControlPoint(MissionTarget, ABC):
return self.captured != other.captured return self.captured != other.captured
@property
def deployable_front_line_units(self) -> int:
return min(self.frontline_unit_count_limit, self.base.total_armor)
@property @property
def frontline_unit_count_limit(self) -> int: def frontline_unit_count_limit(self) -> int:
return ( return (

View File

@ -71,15 +71,26 @@ class FrontLine(MissionTarget):
self.point_from_a(self._position_distance), self.point_from_a(self._position_distance),
) )
def __eq__(self, other: Any) -> bool:
if not isinstance(other, FrontLine):
return False
return (self.blue_cp, self.red_cp) == (other.blue_cp, other.red_cp)
def __hash__(self) -> int:
return hash((self.blue_cp, self.red_cp))
def __setstate__(self, state: dict[str, Any]) -> None: def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state) self.__dict__.update(state)
if not hasattr(self, "position"): if not hasattr(self, "position"):
self.position = self.point_from_a(self._position_distance) self.position = self.point_from_a(self._position_distance)
def control_point_hostile_to(self, player: bool) -> ControlPoint: def control_point_friendly_to(self, player: bool) -> ControlPoint:
if player: if player:
return self.red_cp return self.blue_cp
return self.blue_cp return self.red_cp
def control_point_hostile_to(self, player: bool) -> ControlPoint:
return self.control_point_friendly_to(not player)
def is_friendly(self, to_player: bool) -> bool: def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory.""" """Returns True if the objective is in friendly territory."""

View File

@ -86,43 +86,19 @@ class GroundConflictGenerator:
player_planned_combat_groups: List[CombatGroup], player_planned_combat_groups: List[CombatGroup],
enemy_planned_combat_groups: List[CombatGroup], enemy_planned_combat_groups: List[CombatGroup],
player_stance: CombatStance, player_stance: CombatStance,
enemy_stance: CombatStance,
unit_map: UnitMap, unit_map: UnitMap,
) -> None: ) -> None:
self.mission = mission self.mission = mission
self.conflict = conflict self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups self.enemy_planned_combat_groups = enemy_planned_combat_groups
self.player_planned_combat_groups = player_planned_combat_groups self.player_planned_combat_groups = player_planned_combat_groups
self.player_stance = CombatStance(player_stance) self.player_stance = player_stance
self.enemy_stance = self._enemy_stance() self.enemy_stance = enemy_stance
self.game = game self.game = game
self.unit_map = unit_map self.unit_map = unit_map
self.jtacs: List[JtacInfo] = [] self.jtacs: List[JtacInfo] = []
def _enemy_stance(self) -> CombatStance:
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
if len(self.enemy_planned_combat_groups) > len(
self.player_planned_combat_groups
):
return random.choice(
[
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH,
]
)
else:
return random.choice(
[
CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
CombatStance.AGGRESSIVE,
]
)
def generate(self) -> None: def generate(self) -> None:
position = Conflict.frontline_position( position = Conflict.frontline_position(
self.conflict.front_line, self.game.theater self.conflict.front_line, self.game.theater

View File

@ -247,14 +247,9 @@ class CoalitionMissionPlanner:
""" """
return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type) return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type)
def plan_missions(self) -> None: def fulfill_missions(self) -> None:
"""Identifies and plans mission for the turn.""" """Identifies and plans mission for the turn."""
player = "Blue" if self.is_player else "Red" player = "Blue" if self.is_player else "Red"
with logged_duration(f"{player} mission identification and fulfillment"):
with MultiEventTracer() as tracer:
commander = TheaterCommander(self.game, self.is_player)
commander.plan_missions(self, tracer)
with logged_duration(f"{player} mission scheduling"): with logged_duration(f"{player} mission scheduling"):
self.stagger_missions() self.stagger_missions()

View File

@ -101,7 +101,7 @@ class HqAutomationSettingsBox(QGroupBox):
front_line = QCheckBox() front_line = QCheckBox()
front_line.setChecked(self.game.settings.automate_front_line_reinforcements) front_line.setChecked(self.game.settings.automate_front_line_reinforcements)
front_line.toggled.connect(self.set_front_line_automation) front_line.toggled.connect(self.set_front_line_reinforcement_automation)
layout.addWidget(QLabel("Automate front-line purchases"), 1, 0) layout.addWidget(QLabel("Automate front-line purchases"), 1, 0)
layout.addWidget(front_line, 1, 1, Qt.AlignRight) layout.addWidget(front_line, 1, 1, Qt.AlignRight)
@ -147,12 +147,30 @@ class HqAutomationSettingsBox(QGroupBox):
) )
layout.addWidget(self.auto_ato_player_missions_asap, 4, 1, Qt.AlignRight) layout.addWidget(self.auto_ato_player_missions_asap, 4, 1, Qt.AlignRight)
self.automate_front_line_stance = QCheckBox()
self.automate_front_line_stance.setChecked(
self.game.settings.automate_front_line_stance
)
self.automate_front_line_stance.toggled.connect(
self.set_front_line_stance_automation
)
layout.addWidget(
QLabel("Automatically manage front line stances"),
5,
0,
)
layout.addWidget(self.automate_front_line_stance, 5, 1, Qt.AlignRight)
def set_runway_automation(self, value: bool) -> None: def set_runway_automation(self, value: bool) -> None:
self.game.settings.automate_runway_repair = value self.game.settings.automate_runway_repair = value
def set_front_line_automation(self, value: bool) -> None: def set_front_line_reinforcement_automation(self, value: bool) -> None:
self.game.settings.automate_front_line_reinforcements = value self.game.settings.automate_front_line_reinforcements = value
def set_front_line_stance_automation(self, value: bool) -> None:
self.game.settings.automate_front_line_stance = value
def set_aircraft_automation(self, value: bool) -> None: def set_aircraft_automation(self, value: bool) -> None:
self.game.settings.automate_aircraft_reinforcements = value self.game.settings.automate_aircraft_reinforcements = value