Refactor control points into individual classes.

This commit is contained in:
Dan Albert 2020-11-23 02:28:07 -08:00
parent c67263662d
commit 63bdbebcaa
5 changed files with 250 additions and 154 deletions

View File

@ -42,7 +42,13 @@ from dcs.unitgroup import (
from dcs.vehicles import AirDefence, Armor
from gen.flights.flight import FlightType
from .controlpoint import ControlPoint, MissionTarget, OffMapSpawn
from .controlpoint import (
Airfield,
Carrier,
ControlPoint,
Lha, MissionTarget,
OffMapSpawn,
)
from .landmap import Landmap, load_landmap, poly_contains
from ..utils import nm_to_meter
@ -160,7 +166,7 @@ class MizCampaignLoader:
else:
importance = airport.periodicity / 10
cp = ControlPoint.from_airport(airport, radials, size, importance)
cp = Airfield(airport, radials, size, importance)
cp.captured = airport.is_blue()
# Use the unlimited aircraft option to determine if an airfield should
@ -258,14 +264,14 @@ class MizCampaignLoader:
control_points[control_point.id] = control_point
for group in self.carriers(blue):
# TODO: Name the carrier.
control_point = ControlPoint.carrier(
control_point = Carrier(
"carrier", group.position, next(self.control_point_id))
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
for group in self.lhas(blue):
# TODO: Name the LHA.
control_point = ControlPoint.lha(
control_point = Lha(
"lha", group.position, next(self.control_point_id))
control_point.captured = blue
control_point.captured_invert = group.late_activation
@ -466,7 +472,7 @@ class ConflictTheater:
return closest
def add_json_cp(self, theater, p: dict) -> ControlPoint:
cp: ControlPoint
if p["type"] == "airbase":
airbase = theater.terrain.airports[p["id"]]
@ -486,11 +492,11 @@ class ConflictTheater:
else:
importance = IMPORTANCE_MEDIUM
cp = ControlPoint.from_airport(airbase, radials, size, importance)
cp = Airfield(airbase, radials, size, importance)
elif p["type"] == "carrier":
cp = ControlPoint.carrier("carrier", Point(p["x"], p["y"]), p["id"])
cp = Carrier("carrier", Point(p["x"], p["y"]), p["id"])
else:
cp = ControlPoint.lha("lha", Point(p["x"], p["y"]), p["id"])
cp = Lha("lha", Point(p["x"], p["y"]), p["id"])
if "captured_invert" in p.keys():
cp.captured_invert = p["captured_invert"]

View File

@ -4,9 +4,10 @@ import itertools
import logging
import random
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
from dcs.mapping import Point
from dcs.ships import (
@ -15,10 +16,11 @@ from dcs.ships import (
LHA_1_Tarawa,
Type_071_Amphibious_Transport_Dock,
)
from dcs.terrain.terrain import Airport
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unittype import FlyingType
from game import db
from gen.runways import RunwayAssigner, RunwayData
from gen.ground_forces.combat_stance import CombatStance
from .base import Base
from .missiontarget import MissionTarget
@ -29,6 +31,7 @@ from .theatergroundobject import (
TheaterGroundObject,
VehicleGroupGroundObject,
)
from ..weather import Conditions
if TYPE_CHECKING:
from game import Game
@ -37,11 +40,16 @@ if TYPE_CHECKING:
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)
#: An airbase with slots for everything.
AIRBASE = 0
#: A group with a Stennis type carrier (F/A-18, F-14 compatible).
AIRCRAFT_CARRIER_GROUP = 1
#: A group with a Tarawa carrier (Helicopters & Harrier).
LHA_GROUP = 2
#: A FARP, with slots for helicopters
FARP = 4
#: A FOB (ground units only)
FOB = 5
OFF_MAP = 6
@ -136,7 +144,7 @@ class PendingOccupancy:
return self.present + self.ordered + self.transferring
class ControlPoint(MissionTarget):
class ControlPoint(MissionTarget, ABC):
position = None # type: Point
name = None # type: str
@ -147,29 +155,36 @@ class ControlPoint(MissionTarget):
alt = 0
def __init__(self, id: int, name: str, position: Point,
# TODO: Only airbases have IDs.
# TODO: Radials seem to be pointless.
# TODO: has_frontline is only reasonable for airbases.
# TODO: cptype is obsolete.
def __init__(self, cp_id: int, name: str, position: Point,
at: db.StartingPosition, radials: List[int], size: int,
importance: float, has_frontline=True,
cptype=ControlPointType.AIRBASE):
super().__init__(" ".join(re.split(r" |-", name)[:2]), position)
self.id = id
super().__init__(" ".join(re.split(r"[ \-]", name)[:2]), position)
# TODO: Should be Airbase specific.
self.id = cp_id
self.full_name = name
self.at = at
self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[BaseDefenseGroundObject] = []
self.preset_locations = PresetLocations()
# TODO: Should be Airbase specific.
self.size = size
self.importance = importance
self.captured = False
self.captured_invert = False
# TODO: Should be Airbase specific.
self.has_frontline = has_frontline
self.radials = radials
self.connected_points: List[ControlPoint] = []
self.base: Base = Base()
self.cptype = cptype
# TODO: Should be Airbase specific.
self.stances: Dict[int, CombatStance] = {}
self.airport = None
self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None
def __repr__(self):
@ -180,35 +195,10 @@ class ControlPoint(MissionTarget):
return list(
itertools.chain(self.connected_objectives, self.base_defenses))
@classmethod
def from_airport(cls, airport: Airport, radials: List[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 game.theater.conflicttheater
cp = cls(id, name, at, at, game.theater.conflicttheater.LAND, game.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 game.theater.conflicttheater
cp = cls(id, name, at, at, game.theater.conflicttheater.LAND, game.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
@abstractmethod
def heading(self) -> int:
...
def __str__(self):
return self.name
@ -222,21 +212,21 @@ class ControlPoint(MissionTarget):
"""
:return: Whether this control point is an aircraft carrier
"""
return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
return False
@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]
return False
@property
def is_lha(self):
"""
:return: Whether this control point is an LHA
"""
return self.cptype in [ControlPointType.LHA_GROUP]
return False
@property
def sea_radials(self) -> List[int]:
@ -249,52 +239,42 @@ class ControlPoint(MissionTarget):
return result
@property
@abstractmethod
def total_aircraft_parking(self):
"""
:return: The maximum number of aircraft that can be stored in this control point
: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
...
# TODO: Should be Airbase specific.
def connect(self, to: ControlPoint) -> None:
self.connected_points.append(to)
self.stances[to.id] = CombatStance.DEFENSIVE
def has_runway(self):
@abstractmethod
def has_runway(self) -> bool:
"""
Check whether this control point can have aircraft taking off or landing.
Check whether this control point supports taking offs and landings.
: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
...
# TODO: Should be naval specific.
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] :
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]:
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:
@ -303,6 +283,7 @@ class ControlPoint(MissionTarget):
return group.name
return None
# TODO: Should be Airbase specific.
def is_connected(self, to) -> bool:
return to in self.connected_points
@ -327,6 +308,7 @@ class ControlPoint(MissionTarget):
def is_friendly(self, to_player: bool) -> bool:
return self.captured == to_player
# TODO: Should be Airbase specific.
def clear_base_defenses(self) -> None:
for base_defense in self.base_defenses:
if isinstance(base_defense, EwrGroundObject):
@ -345,6 +327,7 @@ class ControlPoint(MissionTarget):
base_defense.position)
self.base_defenses = []
# TODO: Should be Airbase specific.
def capture(self, game: Game, for_player: bool) -> None:
if for_player:
self.captured = True
@ -360,35 +343,9 @@ class ControlPoint(MissionTarget):
from .start_generator import BaseDefenseGenerator
BaseDefenseGenerator(game, self).generate()
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from super().mission_types(for_player)
if self.is_friendly(for_player):
if self.is_fleet:
yield from [
# TODO: FlightType.INTERCEPTION
# TODO: Buddy tanking for the A-4?
# TODO: Rescue chopper?
# TODO: Inter-ship logistics?
]
else:
yield from [
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
]
else:
if self.is_fleet:
yield FlightType.ANTISHIP
else:
yield from [
# TODO: FlightType.STRIKE
]
@abstractmethod
def can_land(self, aircraft: FlyingType) -> bool:
if self.is_carrier and aircraft not in db.CARRIER_CAPABLE:
return False
if self.is_lha and aircraft not in db.LHA_CAPABLE:
return False
return True
...
def aircraft_transferring(self, game: Game) -> int:
if self.captured:
@ -421,11 +378,158 @@ class ControlPoint(MissionTarget):
return (self.total_aircraft_parking -
self.expected_aircraft_next_turn(game).total)
@abstractmethod
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
...
@property
def parking_slots(self) -> Iterator[ParkingSlot]:
yield from []
class Airfield(ControlPoint):
def __init__(self, airport: Airport, radials: List[int], size: int,
importance: float, has_frontline=True):
super().__init__(airport.id, airport.name, airport.position, airport,
radials, size, importance, has_frontline,
cptype=ControlPointType.AIRBASE)
self.airport = airport
def can_land(self, aircraft: FlyingType) -> bool:
return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from super().mission_types(for_player)
if self.is_friendly(for_player):
yield from [
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
]
else:
yield from [
# TODO: FlightType.STRIKE
]
@property
def total_aircraft_parking(self) -> int:
return len(self.airport.parking_slots)
@property
def heading(self) -> int:
return self.airport.runways[0].heading
def has_runway(self) -> bool:
return True
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
assigner = RunwayAssigner(conditions)
return assigner.get_preferred_runway(self.airport)
@property
def parking_slots(self) -> Iterator[ParkingSlot]:
yield from self.airport.parking_slots
class NavalControlPoint(ControlPoint, ABC):
@property
def is_fleet(self) -> bool:
return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from super().mission_types(for_player)
if self.is_friendly(for_player):
yield from [
# TODO: FlightType.INTERCEPTION
# TODO: Buddy tanking for the A-4?
# TODO: Rescue chopper?
# TODO: Inter-ship logistics?
]
else:
yield FlightType.ANTISHIP
@property
def heading(self) -> int:
return 0 # TODO compute heading
def has_runway(self) -> bool:
# Necessary because it's possible for the carrier itself to have sunk
# while its escorts are still alive.
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
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
# TODO: Assign TACAN and ICLS earlier so we don't need this.
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
return dynamic_runways.get(self.name, fallback)
class Carrier(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater
super().__init__(cp_id, name, at, at, game.theater.conflicttheater.LAND,
game.theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False,
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Carriers cannot be captured")
@property
def is_carrier(self):
return True
def can_land(self, aircraft: FlyingType) -> bool:
return aircraft in db.CARRIER_CAPABLE
@property
def total_aircraft_parking(self) -> int:
return 90
class Lha(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater
super().__init__(cp_id, name, at, at, game.theater.conflicttheater.LAND,
game.theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("LHAs cannot be captured")
@property
def is_lha(self) -> bool:
return True
def can_land(self, aircraft: FlyingType) -> bool:
return aircraft in db.LHA_CAPABLE
@property
def total_aircraft_parking(self) -> int:
return 20
class OffMapSpawn(ControlPoint):
def __init__(self, id: int, name: str, position: Point):
def has_runway(self) -> bool:
return True
def __init__(self, cp_id: int, name: str, position: Point):
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
super().__init__(id, name, position, at=position, radials=[],
super().__init__(cp_id, name, position, at=position, radials=[],
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
has_frontline=False, cptype=ControlPointType.OFF_MAP)
@ -438,3 +542,15 @@ class OffMapSpawn(ControlPoint):
@property
def total_aircraft_parking(self) -> int:
return 1000
def can_land(self, aircraft: FlyingType) -> bool:
return True
@property
def heading(self) -> int:
return 0
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
logging.warning("TODO: Off map spawns have no runways.")
return RunwayData(self.full_name, runway_heading=0, runway_name="")

View File

@ -5,12 +5,14 @@ import logging
import random
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from typing import Optional, TYPE_CHECKING
from dcs.weather import Weather as PydcsWeather, Wind
from game.settings import Settings
from game.theater import ConflictTheater
if TYPE_CHECKING:
from game.theater import ConflictTheater
class TimeOfDay(Enum):

View File

@ -71,8 +71,10 @@ from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings
from game.theater.controlpoint import (
Airfield,
ControlPoint,
ControlPointType,
NavalControlPoint,
OffMapSpawn,
)
from game.theater.theatergroundobject import TheaterGroundObject
@ -696,18 +698,6 @@ class AircraftConflictGenerator:
return StartType.Cold
return StartType.Warm
def determine_runway(self, cp: ControlPoint, dynamic_runways) -> RunwayData:
fallback = RunwayData(cp.full_name, runway_heading=0, runway_name="")
if cp.cptype == ControlPointType.AIRBASE:
assigner = RunwayAssigner(self.game.conditions)
return assigner.get_preferred_runway(cp.airport)
elif cp.is_fleet:
return dynamic_runways.get(cp.name, fallback)
else:
logging.warning(
f"Unhandled departure/arrival control point: {cp.cptype}")
return fallback
def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
package: Package, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
@ -767,7 +757,8 @@ class AircraftConflictGenerator:
divert = None
if flight.divert is not None:
divert = self.determine_runway(flight.divert, dynamic_runways)
divert = flight.divert.active_runway(self.game.conditions,
dynamic_runways)
self.flights.append(FlightData(
package=package,
@ -777,8 +768,10 @@ class AircraftConflictGenerator:
friendly=flight.from_cp.captured,
# Set later.
departure_delay=timedelta(),
departure=self.determine_runway(flight.departure, dynamic_runways),
arrival=self.determine_runway(flight.arrival, dynamic_runways),
departure=flight.departure.active_runway(self.game.conditions,
dynamic_runways),
arrival=flight.arrival.active_runway(self.game.conditions,
dynamic_runways),
divert=divert,
# Waypoints are added later, after they've had their TOTs set.
waypoints=[],
@ -911,9 +904,8 @@ class AircraftConflictGenerator:
def clear_parking_slots(self) -> None:
for cp in self.game.theater.controlpoints:
if cp.airport is not None:
for parking_slot in cp.airport.parking_slots:
parking_slot.unit_id = None
for parking_slot in cp.parking_slots:
parking_slot.unit_id = None
def generate_flights(self, country, ato: AirTaskingOrder,
dynamic_runways: Dict[str, RunwayData]) -> None:
@ -938,10 +930,7 @@ class AircraftConflictGenerator:
enemy_country: Country) -> None:
inventories = self.game.aircraft_inventory.inventories
for control_point, inventory in inventories.items():
if isinstance(control_point, OffMapSpawn):
continue
if control_point.is_fleet:
# Don't crowd the deck since the AI will struggle.
if not isinstance(control_point, Airfield):
continue
if control_point.captured:
@ -957,7 +946,7 @@ class AircraftConflictGenerator:
# If we run out of parking, stop spawning aircraft.
return
def _spawn_unused_at(self, control_point: ControlPoint, country: Country,
def _spawn_unused_at(self, control_point: Airfield, country: Country,
aircraft: Type[FlyingType], number: int) -> None:
for _ in range(number):
# Creating a flight even those this isn't a fragged mission lets us
@ -1033,7 +1022,7 @@ class AircraftConflictGenerator:
side=country,
flight=flight,
origin=cp)
elif cp.is_fleet:
elif isinstance(cp, NavalControlPoint):
group_name = cp.get_carrier_group_name()
group = self._generate_at_group(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
@ -1043,8 +1032,12 @@ class AircraftConflictGenerator:
start_type=flight.start_type,
at=self.m.find_group(group_name))
else:
if not isinstance(cp, Airfield):
raise RuntimeError(
f"Attempted to spawn at airfield for non-airfield {cp}")
group = self._generate_at_airport(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
name=namegen.next_unit_name(country, cp.id,
flight.unit_type),
side=country,
unit_type=flight.unit_type,
count=flight.count,

View File

@ -7,7 +7,6 @@ from typing import Iterator, Optional
from dcs.terrain.terrain import Airport
from game.theater import ControlPoint, ControlPointType
from game.weather import Conditions
from .airfields import AIRFIELD_DATA
from .radios import RadioFrequency
@ -117,23 +116,3 @@ class RunwayAssigner:
# Otherwise the only difference between the two is the distance from
# parking, which we don't know, so just pick the first one.
return best_runways[0]
def takeoff_heading(self, departure: ControlPoint) -> int:
if departure.cptype == ControlPointType.AIRBASE:
return self.get_preferred_runway(departure.airport).runway_heading
elif departure.is_fleet:
# The carrier will be angled into the wind automatically.
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
logging.warning(
f"Unhandled departure control point: {departure.cptype}")
return 0
def landing_heading(self, arrival: ControlPoint) -> int:
if arrival.cptype == ControlPointType.AIRBASE:
return self.get_preferred_runway(arrival.airport).runway_heading
elif arrival.is_fleet:
# The carrier will be angled into the wind automatically.
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
logging.warning(
f"Unhandled departure control point: {arrival.cptype}")
return 0