From 6cffc47f3cbfd641eb21304392a039355d2603d0 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 20 Apr 2021 22:21:42 -0700 Subject: [PATCH] Clean up convoy code. --- game/debriefing.py | 2 +- game/event/event.py | 8 +- game/game.py | 12 +- game/transfers.py | 117 ++++++++++++++++-- game/unitmap.py | 8 +- gen/aircraft.py | 2 +- gen/convoys.py | 26 ++-- gen/naming.py | 2 +- qt_ui/widgets/map/QLiberationMap.py | 18 ++- qt_ui/widgets/map/SupplyRouteSegment.py | 21 ++-- .../windows/basemenu/DepartingConvoysMenu.py | 13 +- 11 files changed, 159 insertions(+), 70 deletions(-) diff --git a/game/debriefing.py b/game/debriefing.py index ea5b8758..3e6b4689 100644 --- a/game/debriefing.py +++ b/game/debriefing.py @@ -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) diff --git a/game/event/event.py b/game/event/event.py index 5f5917d3..c2b77aa2 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -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: diff --git a/game/game.py b/game/game.py index bdd0a896..f59e140f 100644 --- a/game/game.py +++ b/game/game.py @@ -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 diff --git a/game/transfers.py b/game/transfers.py index dce97546..df96b0ea 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -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." diff --git a/game/unitmap.py b/game/unitmap.py index 119eae50..520e80f9 100644 --- a/game/unitmap.py +++ b/game/unitmap.py @@ -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) diff --git a/gen/aircraft.py b/gen/aircraft.py index b4893704..174b2c8f 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -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", diff --git a/gen/convoys.py b/gen/convoys.py index 9f021135..21d48c8f 100644 --- a/gen/convoys.py +++ b/gen/convoys.py @@ -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( diff --git a/gen/naming.py b/gen/naming.py index dcd09c2c..d8c7f768 100644 --- a/gen/naming.py +++ b/gen/naming.py @@ -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): diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 1b902e03..540f9e0c 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -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( diff --git a/qt_ui/widgets/map/SupplyRouteSegment.py b/qt_ui/widgets/map/SupplyRouteSegment.py index 68a23cfc..78401bc2 100644 --- a/qt_ui/widgets/map/SupplyRouteSegment.py +++ b/qt_ui/widgets/map/SupplyRouteSegment.py @@ -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: diff --git a/qt_ui/windows/basemenu/DepartingConvoysMenu.py b/qt_ui/windows/basemenu/DepartingConvoysMenu.py index 193aafdd..3e49900a 100644 --- a/qt_ui/windows/basemenu/DepartingConvoysMenu.py +++ b/qt_ui/windows/basemenu/DepartingConvoysMenu.py @@ -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)