frontline

This commit is contained in:
walterroach 2020-11-12 18:21:37 -06:00
parent 9620ac7e7e
commit ede5ee60c3

View File

@ -1,44 +1,199 @@
"""Battlefield front lines.""" """Battlefield front lines."""
from typing import Tuple from __future__ import annotations
from dataclasses import dataclass
import logging
from itertools import tee
from typing import Tuple, List, Union, Dict
from dcs.mapping import Point from dcs.mapping import Point
from . import ControlPoint, MissionTarget
from .controlpoint import ControlPoint, MissionTarget
Numeric = Union[int, float]
# TODO: Dedup by moving everything to using this class. # TODO: Dedup by moving everything to using this class.
FRONTLINE_MIN_CP_DISTANCE = 5000 FRONTLINE_MIN_CP_DISTANCE = 5000
def compute_position(control_point_a: ControlPoint, def pairwise(iterable):
control_point_b: ControlPoint) -> Point: "s -> (s0,s1), (s1,s2), (s2, s3), ..."
a = control_point_a.position a, b = tee(iterable)
b = control_point_b.position next(b, None)
attack_heading = a.heading_between_point(b) return zip(a, b)
attack_distance = a.distance_to_point(b)
middle_point = a.point_from_heading(attack_heading, attack_distance / 2)
strength_delta = float(control_point_a.base.strength -
control_point_b.base.strength) @dataclass
position = middle_point.point_from_heading(attack_heading, class ComplexFrontLine:
strength_delta * """
attack_distance / 2 - Stores data necessary for building a multi-segment frontline.
FRONTLINE_MIN_CP_DISTANCE) "points" should be ordered from closest to farthest distance originating from start_cp.position
return position """
start_cp: ControlPoint
points: List[Point]
# control_points: List[ControlPoint]
@dataclass
class FrontLineSegment:
"""
Describes a line segment of a FrontLine
"""
point_a: Point
point_b: Point
@property
def attack_heading(self) -> Numeric:
return self.point_a.heading_between_point(self.point_b)
@property
def attack_distance(self) -> Numeric:
return self.point_a.distance_to_point(self.point_b)
class FrontLine(MissionTarget): class FrontLine(MissionTarget):
"""Defines a front line location between two control points. """Defines a front line location between two control points.
Front lines are the area where ground combat happens. Front lines are the area where ground combat happens.
Overwrites the entirety of MissionTarget __init__ method to allow for
dynamic position calculation.
""" """
def __init__(self, control_point_a: ControlPoint, def __init__(
control_point_b: ControlPoint) -> None: self,
super().__init__(f"Front line {control_point_a}/{control_point_b}", control_point_a: ControlPoint,
compute_position(control_point_a, control_point_b)) control_point_b: ControlPoint,
frontline_data: Dict[str, ComplexFrontLine],
) -> None:
self.control_point_a = control_point_a self.control_point_a = control_point_a
self.control_point_b = control_point_b self.control_point_b = control_point_b
self.segments: List[FrontLineSegment] = []
self._build_segments(frontline_data)
self.name = f"Front line {control_point_a}/{control_point_b}"
@property
def position(self):
return self._calculate_position()
@property @property
def control_points(self) -> Tuple[ControlPoint, ControlPoint]: def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""Returns a tuple of the two control points.""" """Returns a tuple of the two control points."""
return self.control_point_a, self.control_point_b return self.control_point_a, self.control_point_b
@property
def middle_point(self):
self.point_from_a(self.attack_distance / 2)
@property
def attack_distance(self):
return sum(i.attack_distance for i in self.segments)
@property
def attack_heading(self):
return self.active_segment.attack_heading
@property
def active_segment(self) -> FrontLineSegment:
"""The FrontLine segment where there can be an active conflict"""
if self._position_distance <= self.segments[0].attack_distance:
return self.segments[0]
remaining_dist = self._position_distance
for segment in self.segments:
if remaining_dist <= segment.attack_distance:
return segment
else:
remaining_dist -= segment.attack_distance
logging.error(
"Frontline attack distance is greater than the sum of its segments"
)
return self.segments[0]
@property
def _position_distance(self) -> float:
"""
The distance from point a where the conflict should occur
according to the current strength of each control point
"""
total_strength = (
self.control_point_a.base.strength + self.control_point_b.base.strength
)
if total_strength == 0:
return self._adjust_for_min_dist(0)
strength_pct = self.control_point_a.base.strength / total_strength
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
def _calculate_position(self) -> Point:
"""
The position where the conflict should occur
according to the current strength of each control point.
"""
return self.point_from_a(self._position_distance)
def _build_segments(self, frontline_data: Dict[str, ComplexFrontLine]) -> None:
control_point_ids = "|".join(
[str(self.control_point_a.id), str(self.control_point_b.id)]
)
reversed_cp_ids = "|".join(
[str(self.control_point_b.id), str(self.control_point_a.id)]
)
complex_frontlines = frontline_data
if (complex_frontlines) and (
(control_point_ids in complex_frontlines)
or (reversed_cp_ids in complex_frontlines)
):
# The frontline segments must be stored in the correct order for the distance algorithms to work.
# The points in the frontline are ordered from the id before the | to the id after.
# First, check if control point id pair matches in order, and create segments if a match is found.
if control_point_ids in complex_frontlines:
point_pairs = pairwise(complex_frontlines[control_point_ids].points)
for i in point_pairs:
self.segments.append(FrontLineSegment(i[0], i[1]))
# Check the reverse order and build in reverse if found.
elif reversed_cp_ids in complex_frontlines:
point_pairs = pairwise(
reversed(complex_frontlines[reversed_cp_ids].points)
)
for i in point_pairs:
self.segments.append(FrontLineSegment(i[0], i[1]))
# If no complex frontline has been configured, fall back to the old straight line method.
else:
self.segments.append(
FrontLineSegment(
self.control_point_a.position, self.control_point_b.position
)
)
def _adjust_for_min_dist(self, distance: Numeric) -> Numeric:
"""
Ensures the frontline conflict is never located within the minimum distance
constant of either end control point.
"""
if (distance > self.attack_distance / 2) and (
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
):
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
elif (distance < self.attack_distance / 2) and (
distance < FRONTLINE_MIN_CP_DISTANCE
):
distance = FRONTLINE_MIN_CP_DISTANCE
return distance
def point_from_a(self, distance: Numeric) -> Point:
"""
Returns a point {distance} away from control_point_a along the frontline segments.
"""
if distance < self.segments[0].attack_distance:
return self.control_point_a.position.point_from_heading(
self.segments[0].attack_heading, distance
)
remaining_dist = distance
for segment in self.segments:
if remaining_dist < segment.attack_distance:
return self.control_point_a.position.point_from_heading(
segment.attack_heading, remaining_dist
)
else:
remaining_dist -= segment.attack_distance