Clean up convoy code.

This commit is contained in:
Dan Albert 2021-04-20 22:21:42 -07:00
parent 50d8e08a34
commit 6cffc47f3c
11 changed files with 159 additions and 70 deletions

View File

@ -206,7 +206,7 @@ class Debriefing:
convoy_unit = self.unit_map.convoy_unit(unit_name)
if convoy_unit is not None:
if convoy_unit.transfer.player:
if convoy_unit.convoy.player_owned:
losses.player_convoy.append(convoy_unit)
else:
losses.enemy_convoy.append(convoy_unit)

View File

@ -159,9 +159,9 @@ class Event:
def commit_convoy_losses(debriefing: Debriefing) -> None:
for loss in debriefing.convoy_losses:
unit_type = loss.unit_type
transfer = loss.transfer
available = loss.transfer.units.get(unit_type, 0)
convoy_name = f"convoy from {transfer.position} to {transfer.destination}"
convoy = loss.convoy
available = loss.convoy.units.get(unit_type, 0)
convoy_name = f"convoy from {convoy.origin} to {convoy.destination}"
if available <= 0:
logging.error(
f"Found killed {unit_type} in {convoy_name} but that convoy has "
@ -170,7 +170,7 @@ class Event:
continue
logging.info(f"{unit_type} destroyed in {convoy_name}")
transfer.units[unit_type] -= 1
convoy.kill_unit(unit_type)
@staticmethod
def commit_ground_object_losses(debriefing: Debriefing) -> None:

View File

@ -34,7 +34,7 @@ from .settings import Settings
from .theater import ConflictTheater, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import MissileSiteGroundObject
from .threatzones import ThreatZones
from .transfers import PendingTransfers, RoadTransferOrder
from .transfers import Convoy, ConvoyMap, PendingTransfers, RoadTransferOrder
from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay
@ -122,7 +122,7 @@ class Game:
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self._transfers = PendingTransfers()
self.transfers = PendingTransfers()
self.sanitize_sides()
@ -154,14 +154,6 @@ class Game:
# Regenerate any state that was not persisted.
self.on_load()
@property
def transfers(self) -> PendingTransfers:
try:
return self._transfers
except AttributeError:
self._transfers = PendingTransfers()
return self._transfers
def generate_conditions(self) -> Conditions:
return Conditions.generate(
self.theater, self.current_day, self.current_turn_time_of_day, self.settings

View File

@ -1,8 +1,14 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Dict, Iterator, List, Type
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type
from dcs.unittype import VehicleType
if TYPE_CHECKING:
pass
from game.theater import ControlPoint, MissionTarget
from game.theater.supplyroutes import SupplyRoute
from gen.naming import namegen
@ -37,8 +43,6 @@ class RoadTransferOrder(TransferOrder):
#: point a turn through the supply line.
position: ControlPoint = field(init=False)
name: str = field(init=False, default_factory=namegen.next_convoy_name)
def __post_init__(self) -> None:
self.position = self.origin
@ -51,14 +55,11 @@ class RoadTransferOrder(TransferOrder):
class Convoy(MissionTarget):
def __init__(self, transfer: RoadTransferOrder) -> None:
self.transfer = transfer
count = sum(c for c in transfer.units.values())
super().__init__(
f"{transfer.name} of {count} units moving from {transfer.position} to "
f"{transfer.destination}",
transfer.position.position,
)
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(namegen.next_convoy_name(), origin.position)
self.origin = origin
self.destination = destination
self.transfers: List[RoadTransferOrder] = []
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
if self.is_friendly(for_player):
@ -68,11 +69,93 @@ class Convoy(MissionTarget):
yield from super().mission_types(for_player)
def is_friendly(self, to_player: bool) -> bool:
return self.transfer.position.captured
return self.origin.captured
def add_units(self, transfer: RoadTransferOrder) -> None:
self.transfers.append(transfer)
def remove_units(self, transfer: RoadTransferOrder) -> None:
self.transfers.remove(transfer)
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
for transfer in self.transfers:
if unit_type in transfer.units:
transfer.units[unit_type] -= 1
return
raise KeyError
@property
def size(self) -> int:
return sum(sum(t.units.values()) for t in self.transfers)
@property
def units(self) -> Dict[Type[VehicleType], int]:
units: Dict[Type[VehicleType], int] = defaultdict(int)
for transfer in self.transfers:
for unit_type, count in transfer.units.items():
units[unit_type] += count
return units
@property
def player_owned(self) -> bool:
return self.origin.captured
class ConvoyMap:
def __init__(self) -> None:
# Dict of origin -> destination -> convoy.
self.convoys: Dict[ControlPoint, Dict[ControlPoint, Convoy]] = defaultdict(dict)
def convoy_exists(self, origin: ControlPoint, destination: ControlPoint) -> bool:
return destination in self.convoys[origin]
def find_convoy(
self, origin: ControlPoint, destination: ControlPoint
) -> Optional[Convoy]:
return self.convoys[origin].get(destination)
def find_or_create_convoy(
self, origin: ControlPoint, destination: ControlPoint
) -> Convoy:
convoy = self.find_convoy(origin, destination)
if convoy is None:
convoy = Convoy(origin, destination)
self.convoys[origin][destination] = convoy
return convoy
def departing_from(self, origin: ControlPoint) -> Iterator[Convoy]:
yield from self.convoys[origin].values()
def disband_convoy(self, convoy: Convoy) -> None:
del self.convoys[convoy.origin][convoy.destination]
def add(self, transfer: RoadTransferOrder) -> None:
next_stop = transfer.next_stop()
self.find_or_create_convoy(transfer.position, next_stop).add_units(transfer)
def remove(self, transfer: RoadTransferOrder) -> None:
next_stop = transfer.next_stop()
convoy = self.find_convoy(transfer.position, next_stop)
if convoy is None:
logging.error(
f"Attempting to remove {transfer} from convoy but it is in no convoy."
)
return
convoy.remove_units(transfer)
if not convoy.transfers:
self.disband_convoy(convoy)
def disband_all(self) -> None:
self.convoys = defaultdict(dict)
def __iter__(self) -> Iterator[Convoy]:
for destination_dict in self.convoys.values():
yield from destination_dict.values()
class PendingTransfers:
def __init__(self) -> None:
self.convoys = ConvoyMap()
self.pending_transfers: List[RoadTransferOrder] = []
def __iter__(self) -> Iterator[RoadTransferOrder]:
@ -88,8 +171,10 @@ class PendingTransfers:
def new_transfer(self, transfer: RoadTransferOrder) -> None:
transfer.origin.base.commit_losses(transfer.units)
self.pending_transfers.append(transfer)
self.convoys.add(transfer)
def cancel_transfer(self, transfer: RoadTransferOrder) -> None:
self.convoys.remove(transfer)
self.pending_transfers.remove(transfer)
transfer.origin.base.commision_units(transfer.units)
@ -99,8 +184,16 @@ class PendingTransfers:
if not self.perform_transfer(transfer):
incomplete.append(transfer)
self.pending_transfers = incomplete
self.rebuild_convoys()
def rebuild_convoys(self) -> None:
self.convoys.disband_all()
for transfer in self.pending_transfers:
self.convoys.add(transfer)
def perform_transfer(self, transfer: RoadTransferOrder) -> bool:
# TODO: Can be improved to use the convoy map.
# The convoy map already has a lot of the data that we're recomputing here.
if transfer.player != transfer.destination.captured:
logging.info(
f"Transfer destination {transfer.destination.name} was captured."

View File

@ -9,7 +9,7 @@ from dcs.unittype import VehicleType
from game import db
from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject
from game.transfers import RoadTransferOrder
from game.transfers import Convoy, RoadTransferOrder
from gen.flights.flight import Flight
@ -29,7 +29,7 @@ class GroundObjectUnit:
@dataclass(frozen=True)
class ConvoyUnit:
unit_type: Type[VehicleType]
transfer: RoadTransferOrder
convoy: Convoy
@dataclass(frozen=True)
@ -121,7 +121,7 @@ class UnitMap:
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
return self.ground_object_units.get(name, None)
def add_convoy_units(self, group: Group, transfer: RoadTransferOrder) -> None:
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
for unit in group.units:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
@ -135,7 +135,7 @@ class UnitMap:
raise RuntimeError(
f"{name} is a {unit_type.__name__}, expected a VehicleType"
)
self.convoys[name] = ConvoyUnit(unit_type, transfer)
self.convoys[name] = ConvoyUnit(unit_type, convoy)
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
return self.convoys.get(name, None)

View File

@ -1697,7 +1697,7 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
if isinstance(target_group, TheaterGroundObject):
group_name = target_group.group_name
elif isinstance(target_group, Convoy):
group_name = target_group.transfer.name
group_name = target_group.name
else:
logging.error(
"Unexpected target type for BAI mission: %s",

View File

@ -10,7 +10,7 @@ from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType
from game.transfers import RoadTransferOrder
from game.transfers import Convoy, RoadTransferOrder
from game.unitmap import UnitMap
from game.utils import kph
@ -27,25 +27,23 @@ class ConvoyGenerator:
def generate(self) -> None:
# Reset the count to make generation deterministic.
for transfer in self.game.transfers:
self.generate_convoy_for(transfer)
def generate_convoy_for(self, transfer: RoadTransferOrder) -> VehicleGroup:
next_hop = transfer.path()[0]
origin = transfer.position.convoy_spawns[next_hop]
destination = next_hop.convoy_spawns[transfer.position]
for convoy in self.game.transfers.convoys:
self.generate_convoy(convoy)
def generate_convoy(self, convoy: Convoy) -> VehicleGroup:
group = self._create_mixed_unit_group(
transfer.name,
origin,
transfer.units,
transfer.player,
convoy.name,
convoy.origin.position,
convoy.units,
convoy.player_owned,
)
group.add_waypoint(
destination, speed=kph(40).kph, move_formation=PointAction.OnRoad
convoy.destination.position,
speed=kph(40).kph,
move_formation=PointAction.OnRoad,
)
self.make_drivable(group)
self.unit_map.add_convoy_units(group, transfer)
self.unit_map.add_convoy_units(group, convoy)
return group
def _create_mixed_unit_group(

View File

@ -333,7 +333,7 @@ class NameGenerator:
@classmethod
def next_convoy_name(cls) -> str:
cls.convoy_number += 1
return f"Convoy {cls.convoy_number:04}"
return f"Convoy {cls.convoy_number:03}"
@classmethod
def random_objective_name(cls):

View File

@ -44,7 +44,7 @@ from game.theater.conflicttheater import FrontLine, ReferencePoint
from game.theater.theatergroundobject import (
TheaterGroundObject,
)
from game.transfers import RoadTransferOrder
from game.transfers import Convoy, RoadTransferOrder
from game.utils import Distance, meters, nautical_miles
from game.weather import TimeOfDay
from gen import Conflict, Package
@ -827,7 +827,7 @@ class QLiberationMap(QGraphicsView):
self,
scene: QGraphicsScene,
frontline: FrontLine,
convoys: List[RoadTransferOrder],
convoys: List[Convoy],
) -> None:
"""
Thanks to Alquimista for sharing a python implementation of the bezier algorithm this is adapted from.
@ -895,7 +895,15 @@ class QLiberationMap(QGraphicsView):
def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None:
scene = self.scene()
convoys = self._transfers_between(a, b)
convoy_map = self.game.transfers.convoys
convoys = []
convoy = convoy_map.find_convoy(a, b)
if convoy is not None:
convoys.append(convoy)
convoy = convoy_map.find_convoy(b, a)
if convoy is not None:
convoys.append(convoy)
frontline = FrontLine(a, b, self.game.theater)
if a.front_is_active(b):
if DisplayOptions.actual_frontline_pos:
@ -909,7 +917,7 @@ class QLiberationMap(QGraphicsView):
self,
scene: QGraphicsScene,
frontline: FrontLine,
convoys: List[RoadTransferOrder],
convoys: List[Convoy],
) -> None:
posx = frontline.position
h = frontline.attack_heading
@ -925,7 +933,7 @@ class QLiberationMap(QGraphicsView):
self,
scene: QGraphicsScene,
frontline: FrontLine,
convoys: List[RoadTransferOrder],
convoys: List[Convoy],
) -> None:
self.draw_bezier_frontline(scene, frontline, convoys)
vector = Conflict.frontline_vector(

View File

@ -9,7 +9,7 @@ from PySide2.QtWidgets import (
)
from game.theater import ControlPoint
from game.transfers import RoadTransferOrder
from game.transfers import Convoy
from qt_ui.uiconstants import COLORS
@ -22,7 +22,7 @@ class SupplyRouteSegment(QGraphicsLineItem):
y1: float,
control_point_a: ControlPoint,
control_point_b: ControlPoint,
convoys: List[RoadTransferOrder],
convoys: List[Convoy],
parent: Optional[QGraphicsItem] = None,
) -> None:
super().__init__(x0, y0, x1, y1, parent)
@ -37,19 +37,18 @@ class SupplyRouteSegment(QGraphicsLineItem):
def has_convoys(self) -> bool:
return bool(self.convoys)
@cached_property
def convoy_size(self) -> int:
return sum(sum(c.units.values()) for c in self.convoys)
def make_tooltip(self) -> str:
if not self.has_convoys:
return "No convoys present on this supply route."
units = "units" if self.convoy_size > 1 else "unit"
return (
f"{self.convoy_size} {units} transferring between {self.control_point_a} "
f"and {self.control_point_b}."
)
convoys = []
for convoy in self.convoys:
units = "units" if convoy.size > 1 else "unit"
convoys.append(
f"{convoy.size} {units} transferring from {convoy.origin} to "
f"{convoy.destination}"
)
return "\n".join(convoys)
@property
def line_color(self) -> QColor:

View File

@ -12,15 +12,15 @@ from PySide2.QtWidgets import (
from game import db
from game.theater import ControlPoint
from game.transfers import Convoy, RoadTransferOrder
from game.transfers import Convoy
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.uiconstants import VEHICLES_ICONS
class DepartingConvoyInfo(QGroupBox):
def __init__(self, convoy: RoadTransferOrder, game_model: GameModel) -> None:
super().__init__(f"To {convoy.destination}")
def __init__(self, convoy: Convoy, game_model: GameModel) -> None:
super().__init__(f"{convoy.name} to {convoy.destination}")
self.convoy = convoy
main_layout = QVBoxLayout()
@ -61,7 +61,7 @@ class DepartingConvoyInfo(QGroupBox):
# complicated. We could instead generate this at the start of the turn (and
# update whenever transfers are created or canceled) and also use that time to
# precalculate things like the next stop and group names.
Dialog.open_new_package_dialog(Convoy(self.convoy), parent=self.window())
Dialog.open_new_package_dialog(self.convoy, parent=self.window())
class DepartingConvoysList(QFrame):
@ -78,9 +78,8 @@ class DepartingConvoysList(QFrame):
task_box_layout = QGridLayout()
scroll_content.setLayout(task_box_layout)
for convoy in game_model.game.transfers:
if convoy.position != cp:
continue
convoy_map = game_model.game.transfers.convoys
for convoy in convoy_map.departing_from(cp):
group_info = DepartingConvoyInfo(convoy, game_model)
task_box_layout.addWidget(group_info)