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."""
from .sim import GameUpdateEvents
for control_point in self.theater.controlpoints:
control_point.initialize_turn_0()
self.blue.preinit_turn_0()
self.red.preinit_turn_0()
# 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 game.dcs.groundunittype import GroundUnitType
from game.theater import FrontLine
from game.transfers import Convoy
from game.unitmap import UnitMap
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.
# 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.
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
# 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))
def conflicts(self) -> Iterator[FrontLine]:
for player_cp in [x for x in self.controlpoints if x.captured]:
for enemy_cp in [
x for x in player_cp.connected_points if not x.is_friendly_to(player_cp)
]:
yield FrontLine(player_cp, enemy_cp)
for cp in self.player_points():
yield from cp.front_lines.values()
def enemy_points(self) -> List[ControlPoint]:
return list(self.control_points_for(player=False))

View File

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

View File

@ -2,15 +2,16 @@ from __future__ import annotations
import logging
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 .controlpoint import ControlPoint, MissionTarget
from .missiontarget import MissionTarget
from ..utils import Heading, pairwise
if TYPE_CHECKING:
from game.ato import FlightType
from .controlpoint import ControlPoint
FRONTLINE_MIN_CP_DISTANCE = 5000
@ -193,3 +194,15 @@ class FrontLine(MissionTarget):
):
distance = FRONTLINE_MIN_CP_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.ato.flighttype import FlightType
from game.config import RUNWAY_REPAIR_COST
from game.server import EventStream
from game.sim import GameUpdateEvents
from game.theater import (
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION,
ControlPoint,
@ -125,7 +127,9 @@ class QBaseMenu2(QDialog):
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
# 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)
@property