mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Replace existing campaign planner with an HTN.
An HTN (https://en.wikipedia.org/wiki/Hierarchical_task_network) is similar to a decision tree, but it is able to reset to an earlier stage if a subtask fails and tasks are able to account for the changes in world state caused by earlier tasks. Currently this just uses exactly the same strategy as before so we can prove the system, but it should make it simpler to improve on task planning.
This commit is contained in:
11
game/commander/tasks/compound/aewcsupport.py
Normal file
11
game/commander/tasks/compound/aewcsupport.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.aewc import PlanAewc
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class PlanAewcSupport(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for target in state.aewc_targets:
|
||||
yield [PlanAewc(target)]
|
||||
15
game/commander/tasks/compound/attackairinfrastructure.py
Normal file
15
game/commander/tasks/compound/attackairinfrastructure.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.primitive.oca import PlanOcaStrike
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AttackAirInfrastructure(CompoundTask[TheaterState]):
|
||||
aircraft_cold_start: bool
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for garrison in state.oca_targets:
|
||||
yield [PlanOcaStrike(garrison, self.aircraft_cold_start)]
|
||||
11
game/commander/tasks/compound/attackbuildings.py
Normal file
11
game/commander/tasks/compound/attackbuildings.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.strike import PlanStrike
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class AttackBuildings(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for garrison in state.strike_targets:
|
||||
yield [PlanStrike(garrison)]
|
||||
11
game/commander/tasks/compound/attackgarrisons.py
Normal file
11
game/commander/tasks/compound/attackgarrisons.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.bai import PlanBai
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class AttackGarrisons(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for garrison in state.enemy_garrisons:
|
||||
yield [PlanBai(garrison)]
|
||||
11
game/commander/tasks/compound/degradeiads.py
Normal file
11
game/commander/tasks/compound/degradeiads.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.dead import PlanDead
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class DegradeIads(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for air_defense in state.threatening_air_defenses:
|
||||
yield [PlanDead(air_defense)]
|
||||
11
game/commander/tasks/compound/destroyships.py
Normal file
11
game/commander/tasks/compound/destroyships.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.antiship import PlanAntiShip
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class DestroyShips(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for ship in state.threatening_ships:
|
||||
yield [PlanAntiShip(ship)]
|
||||
11
game/commander/tasks/compound/frontlinedefense.py
Normal file
11
game/commander/tasks/compound/frontlinedefense.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.cas import PlanCas
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class FrontLineDefense(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for front_line in state.vulnerable_front_lines:
|
||||
yield [PlanCas(front_line)]
|
||||
27
game/commander/tasks/compound/interdictreinforcements.py
Normal file
27
game/commander/tasks/compound/interdictreinforcements.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.antishipping import PlanAntiShipping
|
||||
from game.commander.tasks.primitive.convoyinterdiction import PlanConvoyInterdiction
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class InterdictReinforcements(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
# These will only rarely get planned. When a convoy is travelling multiple legs,
|
||||
# they're targetable after the first leg. The reason for this is that
|
||||
# procurement happens *after* mission planning so that the missions that could
|
||||
# not be filled will guide the procurement process. Procurement is the stage
|
||||
# that convoys are created (because they're created to move ground units that
|
||||
# were just purchased), so we haven't created any yet. Any incomplete transfers
|
||||
# from the previous turn (multi-leg journeys) will still be present though so
|
||||
# they can be targeted.
|
||||
#
|
||||
# Even after this is fixed, the player's convoys that were created through the
|
||||
# UI will never be targeted on the first turn of their journey because the AI
|
||||
# stops planning after the start of the turn. We could potentially fix this by
|
||||
# moving opfor mission planning until the takeoff button is pushed.
|
||||
for convoy in state.enemy_convoys:
|
||||
yield [PlanConvoyInterdiction(convoy)]
|
||||
for ship in state.enemy_shipping:
|
||||
yield [PlanAntiShipping(ship)]
|
||||
36
game/commander/tasks/compound/nextaction.py
Normal file
36
game/commander/tasks/compound/nextaction.py
Normal file
@@ -0,0 +1,36 @@
|
||||
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.degradeiads import DegradeIads
|
||||
from game.commander.tasks.compound.destroyships import DestroyShips
|
||||
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.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlanNextAction(CompoundTask[TheaterState]):
|
||||
aircraft_cold_start: bool
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [PlanAewcSupport()]
|
||||
yield [PlanRefuelingSupport()]
|
||||
yield [ProtectAirSpace()]
|
||||
yield [FrontLineDefense()]
|
||||
yield [DegradeIads()]
|
||||
yield [InterdictReinforcements()]
|
||||
yield [DestroyShips()]
|
||||
yield [AttackGarrisons()]
|
||||
yield [AttackAirInfrastructure(self.aircraft_cold_start)]
|
||||
yield [AttackBuildings()]
|
||||
11
game/commander/tasks/compound/protectairspace.py
Normal file
11
game/commander/tasks/compound/protectairspace.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.barcap import PlanBarcap
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class ProtectAirSpace(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for cp in state.vulnerable_control_points:
|
||||
yield [PlanBarcap(cp)]
|
||||
11
game/commander/tasks/compound/refuelingsupport.py
Normal file
11
game/commander/tasks/compound/refuelingsupport.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.refueling import PlanRefueling
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class PlanRefuelingSupport(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for target in state.refueling_targets:
|
||||
yield [PlanRefueling(target)]
|
||||
73
game/commander/tasks/packageplanningtask.py
Normal file
73
game/commander/tasks/packageplanningtask.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Optional, Generic, TypeVar
|
||||
|
||||
from game.commander.missionproposals import ProposedFlight, EscortType, ProposedMission
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.htn import PrimitiveTask
|
||||
from game.profiling import MultiEventTracer
|
||||
from game.theater import MissionTarget
|
||||
from game.utils import Distance
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
|
||||
|
||||
MissionTargetT = TypeVar("MissionTargetT", bound=MissionTarget)
|
||||
|
||||
|
||||
# TODO: Refactor so that we don't need to call up to the mission planner.
|
||||
# Bypass type checker due to https://github.com/python/mypy/issues/5374
|
||||
@dataclass # type: ignore
|
||||
class PackagePlanningTask(PrimitiveTask[TheaterState], Generic[MissionTargetT]):
|
||||
target: MissionTargetT
|
||||
flights: list[ProposedFlight] = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.flights = []
|
||||
|
||||
def execute(
|
||||
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
|
||||
) -> None:
|
||||
self.propose_flights(mission_planner.doctrine)
|
||||
mission_planner.plan_mission(ProposedMission(self.target, self.flights), tracer)
|
||||
|
||||
@abstractmethod
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
...
|
||||
|
||||
def propose_flight(
|
||||
self,
|
||||
task: FlightType,
|
||||
num_aircraft: int,
|
||||
max_distance: Optional[Distance],
|
||||
escort_type: Optional[EscortType] = None,
|
||||
) -> None:
|
||||
if max_distance is None:
|
||||
max_distance = Distance.inf()
|
||||
self.flights.append(
|
||||
ProposedFlight(task, num_aircraft, max_distance, escort_type)
|
||||
)
|
||||
|
||||
@property
|
||||
def asap(self) -> bool:
|
||||
return False
|
||||
|
||||
def propose_common_escorts(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(
|
||||
FlightType.SEAD_ESCORT,
|
||||
2,
|
||||
doctrine.mission_ranges.offensive,
|
||||
EscortType.Sead,
|
||||
)
|
||||
|
||||
self.propose_flight(
|
||||
FlightType.ESCORT,
|
||||
2,
|
||||
doctrine.mission_ranges.offensive,
|
||||
EscortType.AirToAir,
|
||||
)
|
||||
26
game/commander/tasks/primitive/aewc.py
Normal file
26
game/commander/tasks/primitive/aewc.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import MissionTarget
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanAewc(PackagePlanningTask[MissionTarget]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.aewc_targets
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.aewc_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.AEWC, 1, doctrine.mission_ranges.aewc)
|
||||
|
||||
@property
|
||||
def asap(self) -> bool:
|
||||
# Supports all the early CAP flights, so should be in the air ASAP.
|
||||
return True
|
||||
28
game/commander/tasks/primitive/antiship.py
Normal file
28
game/commander/tasks/primitive/antiship.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.missionproposals import EscortType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import NavalGroundObject
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.threatening_ships
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.threatening_ships.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.ANTISHIP, 2, doctrine.mission_ranges.offensive)
|
||||
self.propose_flight(
|
||||
FlightType.ESCORT,
|
||||
2,
|
||||
doctrine.mission_ranges.offensive,
|
||||
EscortType.AirToAir,
|
||||
)
|
||||
22
game/commander/tasks/primitive/antishipping.py
Normal file
22
game/commander/tasks/primitive/antishipping.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.transfers import CargoShip
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanAntiShipping(PackagePlanningTask[CargoShip]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.enemy_shipping
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.enemy_shipping.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.ANTISHIP, 2, doctrine.mission_ranges.offensive)
|
||||
self.propose_common_escorts(doctrine)
|
||||
22
game/commander/tasks/primitive/bai.py
Normal file
22
game/commander/tasks/primitive/bai.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.enemy_garrisons
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.enemy_garrisons.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive)
|
||||
self.propose_common_escorts(doctrine)
|
||||
55
game/commander/tasks/primitive/barcap.py
Normal file
55
game/commander/tasks/primitive/barcap.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.commander.missionproposals import ProposedMission, ProposedFlight
|
||||
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.profiling import MultiEventTracer
|
||||
from game.theater import ControlPoint
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanBarcap(TheaterCommanderTask):
|
||||
target: ControlPoint
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.vulnerable_control_points
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.vulnerable_control_points.remove(self.target)
|
||||
|
||||
def execute(
|
||||
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
|
||||
) -> None:
|
||||
# Plan enough rounds of CAP that the target has coverage over the expected
|
||||
# mission duration.
|
||||
mission_duration = int(
|
||||
mission_planner.game.settings.desired_player_mission_duration.total_seconds()
|
||||
)
|
||||
barcap_duration = int(
|
||||
mission_planner.faction.doctrine.cap_duration.total_seconds()
|
||||
)
|
||||
for _ in range(
|
||||
0,
|
||||
mission_duration,
|
||||
barcap_duration,
|
||||
):
|
||||
mission_planner.plan_mission(
|
||||
ProposedMission(
|
||||
self.target,
|
||||
[
|
||||
ProposedFlight(
|
||||
FlightType.BARCAP,
|
||||
2,
|
||||
mission_planner.doctrine.mission_ranges.cap,
|
||||
),
|
||||
],
|
||||
),
|
||||
tracer,
|
||||
)
|
||||
22
game/commander/tasks/primitive/cas.py
Normal file
22
game/commander/tasks/primitive/cas.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import FrontLine
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanCas(PackagePlanningTask[FrontLine]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.vulnerable_front_lines
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.vulnerable_front_lines.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.CAS, 2, doctrine.mission_ranges.cas)
|
||||
self.propose_flight(FlightType.TARCAP, 2, doctrine.mission_ranges.cap)
|
||||
22
game/commander/tasks/primitive/convoyinterdiction.py
Normal file
22
game/commander/tasks/primitive/convoyinterdiction.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.transfers import Convoy
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanConvoyInterdiction(PackagePlanningTask[Convoy]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.enemy_convoys
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.enemy_convoys.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive)
|
||||
self.propose_common_escorts(doctrine)
|
||||
51
game/commander/tasks/primitive/dead.py
Normal file
51
game/commander/tasks/primitive/dead.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.missionproposals import EscortType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater.theatergroundobject import IadsGroundObject
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanDead(PackagePlanningTask[IadsGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.threatening_air_defenses
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.threatening_air_defenses.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.DEAD, 2, doctrine.mission_ranges.offensive)
|
||||
|
||||
# Only include SEAD against SAMs that still have emitters. No need to
|
||||
# suppress an EWR, and SEAD isn't useful against a SAM that no longer has a
|
||||
# working track radar.
|
||||
#
|
||||
# For SAMs without track radars and EWRs, we still want a SEAD escort if
|
||||
# needed.
|
||||
#
|
||||
# Note that there is a quirk here: we should potentially be included a SEAD
|
||||
# escort *and* SEAD when the target is a radar SAM but the flight path is
|
||||
# also threatened by SAMs. We don't want to include a SEAD escort if the
|
||||
# package is *only* threatened by the target though. Could be improved, but
|
||||
# needs a decent refactor to the escort planning to do so.
|
||||
if self.target.has_live_radar_sam:
|
||||
self.propose_flight(FlightType.SEAD, 2, doctrine.mission_ranges.offensive)
|
||||
else:
|
||||
self.propose_flight(
|
||||
FlightType.SEAD_ESCORT,
|
||||
2,
|
||||
doctrine.mission_ranges.offensive,
|
||||
EscortType.Sead,
|
||||
)
|
||||
|
||||
self.propose_flight(
|
||||
FlightType.ESCORT,
|
||||
2,
|
||||
doctrine.mission_ranges.offensive,
|
||||
EscortType.AirToAir,
|
||||
)
|
||||
28
game/commander/tasks/primitive/oca.py
Normal file
28
game/commander/tasks/primitive/oca.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import ControlPoint
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
|
||||
aircraft_cold_start: bool
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.oca_targets
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.oca_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.OCA_RUNWAY, 2, doctrine.mission_ranges.offensive)
|
||||
if self.aircraft_cold_start:
|
||||
self.propose_flight(
|
||||
FlightType.OCA_AIRCRAFT, 2, doctrine.mission_ranges.offensive
|
||||
)
|
||||
self.propose_common_escorts(doctrine)
|
||||
21
game/commander/tasks/primitive/refueling.py
Normal file
21
game/commander/tasks/primitive/refueling.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import MissionTarget
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanRefueling(PackagePlanningTask[MissionTarget]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.refueling_targets
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.refueling_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.REFUELING, 1, doctrine.mission_ranges.refueling)
|
||||
23
game/commander/tasks/primitive/strike.py
Normal file
23
game/commander/tasks/primitive/strike.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
return self.target in state.strike_targets
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.strike_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||
self.propose_flight(FlightType.STRIKE, 2, doctrine.mission_ranges.offensive)
|
||||
self.propose_common_escorts(doctrine)
|
||||
20
game/commander/tasks/theatercommandertask.py
Normal file
20
game/commander/tasks/theatercommandertask.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import PrimitiveTask
|
||||
from game.profiling import MultiEventTracer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
|
||||
|
||||
# TODO: Refactor so that we don't need to call up to the mission planner.
|
||||
class TheaterCommanderTask(PrimitiveTask[TheaterState]):
|
||||
@abstractmethod
|
||||
def execute(
|
||||
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
|
||||
) -> None:
|
||||
...
|
||||
Reference in New Issue
Block a user