dcs-retribution/theater/controlpoint.py
Dan Albert 1e041b6249 Perform coalition-wide mission planning.
Mission planning on a per-control point basis lacked the context it
needed to make good decisions, and the ability to make larger missions
that pulled aircraft from multiple airfields.

The per-CP planners have been replaced in favor of a global planner
per coalition. The planner generates a list of potential missions in
order of priority and then allocates aircraft to the proposed flights
until no missions remain.

Mission planning behavior has changed:

* CAP flights will now only be generated for airfields within a
  predefined threat range of an enemy airfield.
* CAS, SEAD, and strike missions get escorts. Strike missions get a
  SEAD flight.
* CAS, SEAD, and strike missions will not be planned unless
  they have an escort available.
* Missions may originate from multiple airfields.

There's more to do:

* The range limitations imposed on the mission planner should take
  aircraft range limitations into account.
* Air superiority aircraft like the F-15 should be preferred for CAP
  over multi-role aircraft like the F/A-18 since otherwise we run the
  risk of running out of ground attack capable aircraft even though
  there are still unused aircraft.
* Mission priorities may need tuning.
* Target areas could be analyzed for potential threats, allowing
  escort flights to be optional or omitted if there is no threat to
  defend against. For example, late game a SEAD flight for a strike
  mission probably is not necessary.
* SAM threat should be judged by how close the extent of the SAM's
  range is to friendly locations, not the distance to the site itself.
  An SA-10 30 nm away is more threatening than an SA-6 25 nm away.
* Much of the planning behavior should be factored out into the
  coalition's doctrine.

But as-is this is an improvement over the existing behavior, so those
things can be follow ups.

The potential regression in behavior here is that we're no longer
planning multiple cycles of missions. Each objective will get one CAP.
I think this fits better with the turn cycle of the game, as a CAP
flight should be able to remain on station for the duration of the
turn (especially with refueling).

Note that this does break save compatibility as the old planner was a
part of the game object, and since that class is now gone it can't be
unpickled.
2020-10-04 12:24:26 -07:00

218 lines
7.4 KiB
Python

import re
import typing
from enum import Enum
from dcs.mapping import *
from dcs.ships import (
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
LHA_1_Tarawa,
Type_071_Amphibious_Transport_Dock,
)
from dcs.terrain.terrain import Airport
from game import db
from gen.ground_forces.combat_stance import CombatStance
from .missiontarget import MissionTarget
from .theatergroundobject import TheaterGroundObject
class ControlPointType(Enum):
AIRBASE = 0 # An airbase with slots for everything
AIRCRAFT_CARRIER_GROUP = 1 # A group with a Stennis type carrier (F/A-18, F-14 compatible)
LHA_GROUP = 2 # A group with a Tarawa carrier (Helicopters & Harrier)
FARP = 4 # A FARP, with slots for helicopters
FOB = 5 # A FOB (ground units only)
class ControlPoint(MissionTarget):
id = 0
position = None # type: Point
name = None # type: str
full_name = None # type: str
base = None # type: theater.base.Base
at = None # type: db.StartPosition
allow_sea_units = True
connected_points = None # type: typing.List[ControlPoint]
ground_objects = None # type: typing.List[TheaterGroundObject]
captured = False
has_frontline = True
frontline_offset = 0.0
cptype: ControlPointType = None
alt = 0
def __init__(self, id: int, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: float,
has_frontline=True, cptype=ControlPointType.AIRBASE):
import theater.base
self.id = id
self.name = " ".join(re.split(r" |-", name)[:2])
self.full_name = name
self.position: Point = position
self.at = at
self.ground_objects = []
self.ships = []
self.size = size
self.importance = importance
self.captured = False
self.captured_invert = False
self.has_frontline = has_frontline
self.radials = radials
self.connected_points = []
self.base = theater.base.Base()
self.cptype = cptype
self.stances = {}
self.airport = None
@classmethod
def from_airport(cls, airport: Airport, radials: typing.Collection[int], size: int, importance: float, has_frontline=True):
assert airport
obj = cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline, cptype=ControlPointType.AIRBASE)
obj.airport = airport()
return obj
@classmethod
def carrier(cls, name: str, at: Point, id: int):
import theater.conflicttheater
cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
return cp
@classmethod
def lha(cls, name: str, at: Point, id: int):
import theater.conflicttheater
cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
return cp
@property
def heading(self):
if self.cptype == ControlPointType.AIRBASE:
return self.airport.runways[0].heading
elif self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
return 0 # TODO compute heading
else:
return 0
def __str__(self):
return self.name
@property
def is_global(self):
return not self.connected_points
@property
def is_carrier(self):
"""
:return: Whether this control point is an aircraft carrier
"""
return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
@property
def is_fleet(self):
"""
:return: Whether this control point is a boat (mobile)
"""
return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
@property
def is_lha(self):
"""
:return: Whether this control point is an LHA
"""
return self.cptype in [ControlPointType.LHA_GROUP]
@property
def sea_radials(self) -> typing.Collection[int]:
# TODO: fix imports
all_radials = [0, 45, 90, 135, 180, 225, 270, 315, ]
result = []
for r in all_radials:
if r not in self.radials:
result.append(r)
return result
@property
def available_aircraft_slots(self):
"""
:return: The maximum number of aircraft that can be stored in this control point
"""
if self.cptype == ControlPointType.AIRBASE:
return len(self.airport.parking_slots)
elif self.is_lha:
return 20
elif self.is_carrier:
return 90
else:
return 0
def connect(self, to):
self.connected_points.append(to)
self.stances[to.id] = CombatStance.DEFENSIVE
def has_runway(self):
"""
Check whether this control point can have aircraft taking off or landing.
:return:
"""
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] :
for g in self.ground_objects:
if g.dcs_identifier in ["CARRIER", "LHA"]:
for group in g.groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [CVN_74_John_C__Stennis, LHA_1_Tarawa, CV_1143_5_Admiral_Kuznetsov, Type_071_Amphibious_Transport_Dock]:
return True
return False
elif self.cptype in [ControlPointType.AIRBASE, ControlPointType.FARP]:
return True
else:
return True
def get_carrier_group_name(self):
"""
Get the carrier group name if the airbase is a carrier
:return: Carrier group name
"""
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] :
for g in self.ground_objects:
if g.dcs_identifier == "CARRIER":
for group in g.groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [CVN_74_John_C__Stennis, CV_1143_5_Admiral_Kuznetsov]:
return group.name
elif g.dcs_identifier == "LHA":
for group in g.groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]:
return group.name
return None
def is_connected(self, to) -> bool:
return to in self.connected_points
def find_radial(self, heading: int, ignored_radial: int = None):
closest_radial = 0
closest_radial_delta = 360
for radial in [x for x in self.radials if x != ignored_radial]:
delta = math.fabs(radial - heading)
if delta < closest_radial_delta:
closest_radial = radial
closest_radial_delta = delta
return closest_radial
def find_ground_objects_by_obj_name(self, obj_name):
found = []
for g in self.ground_objects:
if g.obj_name == obj_name:
found.append(g)
return found
def is_friendly(self, to_player: bool) -> bool:
return self.captured == to_player