Stop ad-hoc constructing FrontLines.

The UI needs to be able to identify these to the server and vice versa,
so they'll need IDs that don't change. Rather than constructing an ID
based on the control points names, make them an owned part of the
control point. The constructed ID would be fine, but a UUID will make
them more suitable for the database, and this was always fairly gross
anyway.

Some follow up work if anyone is interested: a bunch of the data that's
computed in the various properties can now probably be computed *once*
and persisted to the FrontLine type.
This commit is contained in:
Dan Albert 2022-03-03 00:38:52 -08:00
parent 4dfc42528d
commit e5f4974e9a
6 changed files with 65 additions and 26 deletions

View File

@ -273,6 +273,9 @@ class Game:
"""Initialization for the first turn of the game.""" """Initialization for the first turn of the game."""
from .sim import GameUpdateEvents from .sim import GameUpdateEvents
for control_point in self.theater.controlpoints:
control_point.initialize_turn_0()
self.blue.preinit_turn_0() self.blue.preinit_turn_0()
self.red.preinit_turn_0() self.red.preinit_turn_0()
# We don't need to actually stream events for turn zero because we haven't given # We don't need to actually stream events for turn zero because we haven't given

View File

@ -10,7 +10,6 @@ from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.theater import FrontLine
from game.transfers import Convoy from game.transfers import Convoy
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import kph from game.utils import kph
@ -46,7 +45,7 @@ class ConvoyGenerator:
# convoys_travel_full_distance is disabled, so have the convoy only move the first segment on the route. # convoys_travel_full_distance is disabled, so have the convoy only move the first segment on the route.
# This option aims to remove long routes for ground vehicles between control points, # This option aims to remove long routes for ground vehicles between control points,
# since the CPU load for pathfinding long routes on DCS can be pretty heavy. # since the CPU load for pathfinding long routes on DCS can be pretty heavy.
frontline = FrontLine(convoy.origin, convoy.destination) frontline = convoy.origin.front_line_with(convoy.destination)
# Select the first route segment from the origin towards the destination # Select the first route segment from the origin towards the destination
# so the convoy spawns at the origin CP. This allows the convoy to be # so the convoy spawns at the origin CP. This allows the convoy to be

View File

@ -137,11 +137,8 @@ class ConflictTheater:
return list(self.control_points_for(player=True)) return list(self.control_points_for(player=True))
def conflicts(self) -> Iterator[FrontLine]: def conflicts(self) -> Iterator[FrontLine]:
for player_cp in [x for x in self.controlpoints if x.captured]: for cp in self.player_points():
for enemy_cp in [ yield from cp.front_lines.values()
x for x in player_cp.connected_points if not x.is_friendly_to(player_cp)
]:
yield FrontLine(player_cp, enemy_cp)
def enemy_points(self) -> List[ControlPoint]: def enemy_points(self) -> List[ControlPoint]:
return list(self.control_points_for(player=False)) return list(self.control_points_for(player=False))

View File

@ -43,6 +43,7 @@ from game.sidc import (
) )
from game.utils import Heading from game.utils import Heading
from .base import Base from .base import Base
from .frontline import FrontLine
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import ( from .theatergroundobject import (
GenericCarrierGroundObject, GenericCarrierGroundObject,
@ -62,7 +63,7 @@ if TYPE_CHECKING:
from game.squadrons.squadron import Squadron from game.squadrons.squadron import Squadron
from ..coalition import Coalition from ..coalition import Coalition
from ..transfers import PendingTransfers from ..transfers import PendingTransfers
from . import ConflictTheater from .conflicttheater import ConflictTheater
FREE_FRONTLINE_UNIT_SUPPLY: int = 15 FREE_FRONTLINE_UNIT_SUPPLY: int = 15
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12 AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
@ -287,15 +288,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
# the distance of the circle on the map. # the distance of the circle on the map.
CAPTURE_DISTANCE = nautical_miles(2) CAPTURE_DISTANCE = nautical_miles(2)
position = None # type: Point
name = None # type: str
has_frontline = True
alt = 0
# TODO: Only airbases have IDs. # TODO: Only airbases have IDs.
# TODO: has_frontline is only reasonable for airbases.
# TODO: cptype is obsolete. # TODO: cptype is obsolete.
def __init__( def __init__(
self, self,
@ -304,7 +297,6 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
position: Point, position: Point,
at: StartingPosition, at: StartingPosition,
starts_blue: bool, starts_blue: bool,
has_frontline: bool = True,
cptype: ControlPointType = ControlPointType.AIRBASE, cptype: ControlPointType = ControlPointType.AIRBASE,
) -> None: ) -> None:
super().__init__(name, position) super().__init__(name, position)
@ -319,8 +311,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self._coalition: Optional[Coalition] = None self._coalition: Optional[Coalition] = None
self.captured_invert = False self.captured_invert = False
self.front_lines: dict[ControlPoint, FrontLine] = {}
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.has_frontline = has_frontline
self.connected_points: List[ControlPoint] = [] self.connected_points: List[ControlPoint] = []
self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {} self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {}
self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {} self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {}
@ -347,6 +339,41 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
assert self._coalition is None assert self._coalition is None
self._coalition = game.coalition_for(self.starts_blue) self._coalition = game.coalition_for(self.starts_blue)
def initialize_turn_0(self) -> None:
self._recreate_front_lines()
def _recreate_front_lines(self) -> None:
self._clear_front_lines()
for connection in self.convoy_routes.keys():
if not connection.front_line_active_with(
self
) and not connection.is_friendly_to(self):
self._create_front_line_with(connection)
def _create_front_line_with(self, connection: ControlPoint) -> None:
blue, red = FrontLine.sort_control_points(self, connection)
front = FrontLine(blue, red)
self.front_lines[connection] = front
connection.front_lines[self] = front
def _remove_front_line_with(self, connection: ControlPoint) -> None:
del self.front_lines[connection]
del connection.front_lines[self]
def _clear_front_lines(self) -> None:
for opponent in list(self.front_lines.keys()):
self._remove_front_line_with(opponent)
@property
def has_frontline(self) -> bool:
return bool(self.front_lines)
def front_line_active_with(self, other: ControlPoint) -> bool:
return other in self.front_lines
def front_line_with(self, other: ControlPoint) -> FrontLine:
return self.front_lines[other]
@property @property
def captured(self) -> bool: def captured(self) -> bool:
return self.coalition.player return self.coalition.player
@ -695,6 +722,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self._coalition = new_coalition self._coalition = new_coalition
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
self._recreate_front_lines()
@property @property
def required_aircraft_start_type(self) -> Optional[StartType]: def required_aircraft_start_type(self) -> Optional[StartType]:
@ -894,7 +922,6 @@ class Airfield(ControlPoint):
airport.position, airport.position,
airport, airport,
starts_blue, starts_blue,
has_frontline=True,
cptype=ControlPointType.AIRBASE, cptype=ControlPointType.AIRBASE,
) )
self.airport = airport self.airport = airport
@ -1083,7 +1110,6 @@ class Carrier(NavalControlPoint):
at, at,
at, at,
starts_blue, starts_blue,
has_frontline=False,
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
) )
@ -1128,7 +1154,6 @@ class Lha(NavalControlPoint):
at, at,
at, at,
starts_blue, starts_blue,
has_frontline=False,
cptype=ControlPointType.LHA_GROUP, cptype=ControlPointType.LHA_GROUP,
) )
@ -1166,7 +1191,6 @@ class OffMapSpawn(ControlPoint):
position, position,
position, position,
starts_blue, starts_blue,
has_frontline=False,
cptype=ControlPointType.OFF_MAP, cptype=ControlPointType.OFF_MAP,
) )
@ -1231,7 +1255,6 @@ class Fob(ControlPoint):
at, at,
at, at,
starts_blue, starts_blue,
has_frontline=True,
cptype=ControlPointType.FOB, cptype=ControlPointType.FOB,
) )
self.name = name self.name = name

View File

@ -2,15 +2,16 @@ from __future__ import annotations
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator, List, Tuple, Any, TYPE_CHECKING from typing import Any, Iterator, List, TYPE_CHECKING, Tuple
from dcs.mapping import Point from dcs.mapping import Point
from .controlpoint import ControlPoint, MissionTarget from .missiontarget import MissionTarget
from ..utils import Heading, pairwise from ..utils import Heading, pairwise
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import FlightType from game.ato import FlightType
from .controlpoint import ControlPoint
FRONTLINE_MIN_CP_DISTANCE = 5000 FRONTLINE_MIN_CP_DISTANCE = 5000
@ -193,3 +194,15 @@ class FrontLine(MissionTarget):
): ):
distance = FRONTLINE_MIN_CP_DISTANCE distance = FRONTLINE_MIN_CP_DISTANCE
return distance return distance
@staticmethod
def sort_control_points(
a: ControlPoint, b: ControlPoint
) -> tuple[ControlPoint, ControlPoint]:
if a.is_friendly_to(b):
raise ValueError(
"Cannot sort control points that are friendly to each other"
)
if a.captured:
return a, b
return b, a

View File

@ -13,6 +13,8 @@ from PySide2.QtWidgets import (
from game import Game from game import Game
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.config import RUNWAY_REPAIR_COST from game.config import RUNWAY_REPAIR_COST
from game.server import EventStream
from game.sim import GameUpdateEvents
from game.theater import ( from game.theater import (
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION, AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION,
ControlPoint, ControlPoint,
@ -125,7 +127,9 @@ class QBaseMenu2(QDialog):
self.cp.capture(self.game_model.game, for_player=not self.cp.captured) self.cp.capture(self.game_model.game, for_player=not self.cp.captured)
# Reinitialized ground planners and the like. The ATO needs to be reset because # Reinitialized ground planners and the like. The ATO needs to be reset because
# missions planned against the flipped base are no longer valid. # missions planned against the flipped base are no longer valid.
self.game_model.game.initialize_turn() events = GameUpdateEvents()
self.game_model.game.initialize_turn(events)
EventStream.put_nowait(events)
GameUpdateSignal.get_instance().updateGame(self.game_model.game) GameUpdateSignal.get_instance().updateGame(self.game_model.game)
@property @property