mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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:
parent
575aca5886
commit
0a416ab758
@ -5,6 +5,7 @@ Saves from 3.x are not compatible with 5.0.
|
||||
## Features/Improvements
|
||||
|
||||
* **[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
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.theater import ConflictTheater
|
||||
from game.theater import ConflictTheater, ControlPoint
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
from game.utils import meters, nautical_miles
|
||||
|
||||
@ -53,9 +53,7 @@ class Garrisons:
|
||||
continue
|
||||
|
||||
for garrison in garrisons:
|
||||
# Not sure what distance DCS uses, but assuming it's about 2NM since
|
||||
# that's roughly the distance of the circle on the map.
|
||||
if meters(garrison.distance_to(cp)) < nautical_miles(2):
|
||||
if meters(garrison.distance_to(cp)) < ControlPoint.CAPTURE_DISTANCE:
|
||||
blocking.append(garrison)
|
||||
else:
|
||||
defending.append(garrison)
|
||||
|
||||
19
game/commander/tasks/compound/capturebase.py
Normal file
19
game/commander/tasks/compound/capturebase.py
Normal 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)]
|
||||
13
game/commander/tasks/compound/capturebases.py
Normal file
13
game/commander/tasks/compound/capturebases.py
Normal 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)]
|
||||
19
game/commander/tasks/compound/defendbase.py
Normal file
19
game/commander/tasks/compound/defendbase.py
Normal 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)]
|
||||
13
game/commander/tasks/compound/defendbases.py
Normal file
13
game/commander/tasks/compound/defendbases.py
Normal 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)]
|
||||
17
game/commander/tasks/compound/destroyenemygroundunits.py
Normal file
17
game/commander/tasks/compound/destroyenemygroundunits.py
Normal 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)]
|
||||
@ -1,19 +1,19 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.compound.aewcsupport import PlanAewcSupport
|
||||
from game.commander.tasks.compound.attackairinfrastructure import (
|
||||
AttackAirInfrastructure,
|
||||
)
|
||||
from game.commander.tasks.compound.attackbuildings import AttackBuildings
|
||||
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.frontlinedefense import FrontLineDefense
|
||||
from game.commander.tasks.compound.interdictreinforcements import (
|
||||
InterdictReinforcements,
|
||||
)
|
||||
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.htn import CompoundTask, Method
|
||||
|
||||
@ -23,10 +23,10 @@ class PlanNextAction(CompoundTask[TheaterState]):
|
||||
aircraft_cold_start: bool
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [PlanAewcSupport()]
|
||||
yield [PlanRefuelingSupport()]
|
||||
yield [TheaterSupport()]
|
||||
yield [ProtectAirSpace()]
|
||||
yield [FrontLineDefense()]
|
||||
yield [CaptureBases()]
|
||||
yield [DefendBases()]
|
||||
yield [InterdictReinforcements()]
|
||||
yield [AttackGarrisons()]
|
||||
yield [AttackAirInfrastructure(self.aircraft_cold_start)]
|
||||
|
||||
14
game/commander/tasks/compound/theatersupport.py
Normal file
14
game/commander/tasks/compound/theatersupport.py
Normal 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()]
|
||||
75
game/commander/tasks/frontlinestancetask.py
Normal file
75
game/commander/tasks/frontlinestancetask.py
Normal 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
|
||||
@ -40,6 +40,9 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
def __post_init__(self) -> None:
|
||||
self.flights = []
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return not state.player or state.ato_automation_enabled
|
||||
|
||||
def execute(
|
||||
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
|
||||
) -> None:
|
||||
|
||||
@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
|
||||
@dataclass
|
||||
class PlanAewc(PackagePlanningTask[MissionTarget]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
return self.target in state.aewc_targets
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
|
||||
@ -13,6 +13,8 @@ from gen.flights.flight import FlightType
|
||||
@dataclass
|
||||
class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
if self.target not in state.threatening_air_defenses:
|
||||
return False
|
||||
return self.target_area_preconditions_met(state, ignore_iads=True)
|
||||
|
||||
@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
|
||||
@dataclass
|
||||
class PlanAntiShipping(PackagePlanningTask[CargoShip]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
if self.target not in state.enemy_shipping:
|
||||
return False
|
||||
return self.target_area_preconditions_met(state)
|
||||
|
||||
@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
|
||||
@dataclass
|
||||
class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
if self.target not in state.enemy_garrisons:
|
||||
return False
|
||||
return self.target_area_preconditions_met(state)
|
||||
|
||||
@ -19,6 +19,8 @@ class PlanBarcap(TheaterCommanderTask):
|
||||
target: ControlPoint
|
||||
|
||||
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
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
|
||||
37
game/commander/tasks/primitive/breakthroughattack.py
Normal file
37
game/commander/tasks/primitive/breakthroughattack.py
Normal 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)
|
||||
@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
|
||||
@dataclass
|
||||
class PlanCas(PackagePlanningTask[FrontLine]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
return self.target in state.vulnerable_front_lines
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
|
||||
@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
|
||||
@dataclass
|
||||
class PlanConvoyInterdiction(PackagePlanningTask[Convoy]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
if self.target not in state.enemy_convoys:
|
||||
return False
|
||||
return self.target_area_preconditions_met(state)
|
||||
|
||||
@ -13,6 +13,8 @@ from gen.flights.flight import FlightType
|
||||
@dataclass
|
||||
class PlanDead(PackagePlanningTask[IadsGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
if (
|
||||
self.target not in state.threatening_air_defenses
|
||||
and self.target not in state.detecting_air_defenses
|
||||
|
||||
14
game/commander/tasks/primitive/defensivestance.py
Normal file
14
game/commander/tasks/primitive/defensivestance.py
Normal 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
|
||||
14
game/commander/tasks/primitive/eliminationattack.py
Normal file
14
game/commander/tasks/primitive/eliminationattack.py
Normal 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
|
||||
@ -14,6 +14,8 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
|
||||
aircraft_cold_start: bool
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
if self.target not in state.oca_targets:
|
||||
return False
|
||||
return self.target_area_preconditions_met(state)
|
||||
|
||||
@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
|
||||
@dataclass
|
||||
class PlanRefueling(PackagePlanningTask[MissionTarget]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
return self.target in state.refueling_targets
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
|
||||
14
game/commander/tasks/primitive/retreatstance.py
Normal file
14
game/commander/tasks/primitive/retreatstance.py
Normal 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
|
||||
@ -13,6 +13,8 @@ from gen.flights.flight import FlightType
|
||||
@dataclass
|
||||
class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
if self.target not in state.strike_targets:
|
||||
return False
|
||||
return self.target_area_preconditions_met(state)
|
||||
|
||||
@ -3,12 +3,13 @@ from __future__ import annotations
|
||||
import dataclasses
|
||||
import itertools
|
||||
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.objectivefinder import ObjectiveFinder
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.htn import WorldState
|
||||
from game.settings import AutoAtoBehavior
|
||||
from game.theater import ControlPoint, FrontLine, MissionTarget
|
||||
from game.theater.theatergroundobject import (
|
||||
TheaterGroundObject,
|
||||
@ -17,6 +18,7 @@ from game.theater.theatergroundobject import (
|
||||
)
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import Convoy, CargoShip
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -24,7 +26,12 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class TheaterState(WorldState["TheaterState"]):
|
||||
player: bool
|
||||
stance_automation_enabled: bool
|
||||
ato_automation_enabled: bool
|
||||
vulnerable_control_points: list[ControlPoint]
|
||||
active_front_lines: list[FrontLine]
|
||||
front_line_stances: dict[FrontLine, Optional[CombatStance]]
|
||||
vulnerable_front_lines: list[FrontLine]
|
||||
aewc_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
|
||||
# expensive.
|
||||
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),
|
||||
active_front_lines=list(self.active_front_lines),
|
||||
front_line_stances=dict(self.front_line_stances),
|
||||
vulnerable_front_lines=list(self.vulnerable_front_lines),
|
||||
aewc_targets=list(self.aewc_targets),
|
||||
refueling_targets=list(self.refueling_targets),
|
||||
@ -96,8 +108,15 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
@classmethod
|
||||
def from_game(cls, game: Game, player: bool) -> TheaterState:
|
||||
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(
|
||||
player=player,
|
||||
stance_automation_enabled=auto_stance,
|
||||
ato_automation_enabled=auto_ato,
|
||||
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()),
|
||||
aewc_targets=[finder.farthest_friendly_control_point()],
|
||||
refueling_targets=[finder.closest_friendly_control_point()],
|
||||
|
||||
19
game/game.py
19
game/game.py
@ -24,6 +24,7 @@ from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import FlightType
|
||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from . import persistency
|
||||
from .commander import TheaterCommander
|
||||
from .debriefing import Debriefing
|
||||
from .event.event import Event
|
||||
from .event.frontlineattack import FrontlineAttackEvent
|
||||
@ -32,7 +33,7 @@ from .income import Income
|
||||
from .infos.information import Information
|
||||
from .navmesh import NavMesh
|
||||
from .procurement import AircraftProcurementRequest, ProcurementAi
|
||||
from .profiling import logged_duration
|
||||
from .profiling import logged_duration, MultiEventTracer
|
||||
from .settings import Settings, AutoAtoBehavior
|
||||
from .squadrons import AirWing
|
||||
from .theater import ConflictTheater, ControlPoint
|
||||
@ -504,13 +505,15 @@ class Game:
|
||||
with logged_duration("Transport planning"):
|
||||
self.transfers.plan_transports()
|
||||
|
||||
if not player or (
|
||||
player and self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
|
||||
):
|
||||
color = "Blue" if player else "Red"
|
||||
with logged_duration(f"{color} mission planning"):
|
||||
mission_planner = CoalitionMissionPlanner(self, player)
|
||||
mission_planner.plan_missions()
|
||||
color = "Blue" if player else "Red"
|
||||
with MultiEventTracer() as tracer:
|
||||
mission_planner = CoalitionMissionPlanner(self, player)
|
||||
with tracer.trace(f"{color} mission planning"):
|
||||
with tracer.trace(f"{color} mission identification"):
|
||||
commander = TheaterCommander(self, player)
|
||||
commander.plan_missions(mission_planner, tracer)
|
||||
with tracer.trace(f"{color} mission fulfillment"):
|
||||
mission_planner.fulfill_missions()
|
||||
|
||||
self.plan_procurement_for(player)
|
||||
|
||||
|
||||
@ -397,6 +397,7 @@ class Operation:
|
||||
player_gp,
|
||||
enemy_gp,
|
||||
player_cp.stances[enemy_cp.id],
|
||||
enemy_cp.stances[player_cp.id],
|
||||
cls.unit_map,
|
||||
)
|
||||
ground_conflict_gen.generate()
|
||||
|
||||
@ -55,6 +55,7 @@ class Settings:
|
||||
automate_runway_repair: bool = False
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
automate_front_line_stance: bool = True
|
||||
restrict_weapons_by_date: bool = False
|
||||
disable_legacy_aewc: bool = True
|
||||
disable_legacy_tanker: bool = True
|
||||
|
||||
@ -271,6 +271,9 @@ class ControlPointStatus(IntEnum):
|
||||
|
||||
|
||||
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
|
||||
name = None # type: str
|
||||
@ -727,6 +730,10 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
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
|
||||
def frontline_unit_count_limit(self) -> int:
|
||||
return (
|
||||
|
||||
@ -71,15 +71,26 @@ class FrontLine(MissionTarget):
|
||||
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:
|
||||
self.__dict__.update(state)
|
||||
if not hasattr(self, "position"):
|
||||
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:
|
||||
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:
|
||||
"""Returns True if the objective is in friendly territory."""
|
||||
|
||||
30
gen/armor.py
30
gen/armor.py
@ -86,43 +86,19 @@ class GroundConflictGenerator:
|
||||
player_planned_combat_groups: List[CombatGroup],
|
||||
enemy_planned_combat_groups: List[CombatGroup],
|
||||
player_stance: CombatStance,
|
||||
enemy_stance: CombatStance,
|
||||
unit_map: UnitMap,
|
||||
) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.enemy_planned_combat_groups = enemy_planned_combat_groups
|
||||
self.player_planned_combat_groups = player_planned_combat_groups
|
||||
self.player_stance = CombatStance(player_stance)
|
||||
self.enemy_stance = self._enemy_stance()
|
||||
self.player_stance = player_stance
|
||||
self.enemy_stance = enemy_stance
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
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:
|
||||
position = Conflict.frontline_position(
|
||||
self.conflict.front_line, self.game.theater
|
||||
|
||||
@ -247,14 +247,9 @@ class CoalitionMissionPlanner:
|
||||
"""
|
||||
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."""
|
||||
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"):
|
||||
self.stagger_missions()
|
||||
|
||||
|
||||
@ -101,7 +101,7 @@ class HqAutomationSettingsBox(QGroupBox):
|
||||
|
||||
front_line = QCheckBox()
|
||||
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(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)
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
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:
|
||||
self.game.settings.automate_aircraft_reinforcements = value
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user