Merge branch 'ground_tasking' into develop_2_3_x

This commit is contained in:
walterroach 2020-12-12 15:41:00 -06:00
commit 0371b62acb
6 changed files with 113 additions and 41 deletions

View File

@ -9,6 +9,9 @@ from itertools import tee
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
from shapely import geometry
from shapely import ops
from dcs import Mission from dcs import Mission
from dcs.countries import ( from dcs.countries import (
CombinedJointTaskForcesBlue, CombinedJointTaskForcesBlue,
@ -472,6 +475,32 @@ class ConflictTheater:
return True return True
def nearest_land_pos(self, point: Point, extend_dist: int = 50) -> Point:
"""Returns the nearest point inside a land exclusion zone from point
`extend_dist` determines how far inside the zone the point should be placed"""
if self.is_on_land(point):
return point
point = geometry.Point(point.x, point.y)
nearest_points = []
for inclusion_zone in self.landmap[0]:
nearest_pair = ops.nearest_points(point, inclusion_zone)
nearest_points.append(nearest_pair[1])
min_distance = None # type: Optional[geometry.Point]
nearest_point = None # type: Optional[geometry.Point]
for pt in nearest_points:
distance = point.distance(pt)
if not min_distance or distance < min_distance:
min_distance = distance
nearest_point = pt
assert isinstance(nearest_point, geometry.Point)
point = Point(point.x, point.y)
nearest_point = Point(nearest_point.x, nearest_point.y)
new_point = point.point_from_heading(
point.heading_between_point(nearest_point),
point.distance_to_point(nearest_point) + extend_dist
)
return new_point
def player_points(self) -> List[ControlPoint]: def player_points(self) -> List[ControlPoint]:
return [point for point in self.controlpoints if point.captured] return [point for point in self.controlpoints if point.captured]

View File

@ -177,7 +177,12 @@ class GroundConflictGenerator:
forward_heading: int forward_heading: int
) -> None: ) -> None:
infantry_position = group.points[0].position.random_point_within(250, 50) infantry_position = self.conflict.find_ground_position(
group.points[0].position.random_point_within(250, 50),
500,
forward_heading,
self.conflict.theater
)
if side == self.conflict.attackers_country: if side == self.conflict.attackers_country:
cp = self.conflict.from_cp cp = self.conflict.from_cp
@ -228,6 +233,17 @@ class GroundConflictGenerator:
heading=forward_heading, heading=forward_heading,
move_formation=PointAction.OffRoad) move_formation=PointAction.OffRoad)
def _set_reform_waypoint(
self,
dcs_group: VehicleGroup,
forward_heading: int
) -> None:
"""Setting a waypoint close to the spawn position allows the group to reform gracefully
rather than spin
"""
reform_point = dcs_group.position.point_from_heading(forward_heading, 50)
dcs_group.add_waypoint(reform_point)
def _plan_artillery_action( def _plan_artillery_action(
self, self,
stance: CombatStance, stance: CombatStance,
@ -240,6 +256,7 @@ class GroundConflictGenerator:
Handles adding the DCS tasks for artillery groups for all combat stances. Handles adding the DCS tasks for artillery groups for all combat stances.
Returns True if tasking was added, returns False if the stance was not a combat stance. Returns True if tasking was added, returns False if the stance was not a combat stance.
""" """
self._set_reform_waypoint(dcs_group, forward_heading)
if stance != CombatStance.RETREAT: if stance != CombatStance.RETREAT:
hold_task = Hold() hold_task = Hold()
hold_task.number = 1 hold_task.number = 1
@ -259,10 +276,10 @@ class GroundConflictGenerator:
if stance != CombatStance.RETREAT: if stance != CombatStance.RETREAT:
# Hold position # Hold position
dcs_group.points[0].tasks.append(Hold())
retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3))
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
dcs_group.points[1].tasks.append(Hold()) dcs_group.points[1].tasks.append(Hold())
retreat = self.find_retreat_point(dcs_group, heading_sum(forward_heading, 180), (int)(RETREAT_DISTANCE/3))
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
dcs_group.points[2].tasks.append(Hold())
dcs_group.add_waypoint(retreat, PointAction.OffRoad) dcs_group.add_waypoint(retreat, PointAction.OffRoad)
artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id)) artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id))
@ -300,6 +317,7 @@ class GroundConflictGenerator:
Handles adding the DCS tasks for tank and IFV groups for all combat stances. Handles adding the DCS tasks for tank and IFV groups for all combat stances.
Returns True if tasking was added, returns False if the stance was not a combat stance. Returns True if tasking was added, returns False if the stance was not a combat stance.
""" """
self._set_reform_waypoint(dcs_group, forward_heading)
if stance == CombatStance.AGGRESSIVE: if stance == CombatStance.AGGRESSIVE:
# Attack nearest enemy if any # Attack nearest enemy if any
# Then move forward OR Attack enemy base if it is not too far away # Then move forward OR Attack enemy base if it is not too far away
@ -313,15 +331,20 @@ class GroundConflictGenerator:
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK -RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
) )
) )
dcs_group.add_waypoint(target.points[0].position + rand_offset, PointAction.OffRoad) target_point = self.conflict.theater.nearest_land_pos(
dcs_group.points[1].tasks.append(AttackGroup(target.id)) target.points[0].position + rand_offset
)
dcs_group.add_waypoint(target_point)
dcs_group.points[2].tasks.append(AttackGroup(target.id))
if ( if (
to_cp.position.distance_to_point(dcs_group.points[0].position) to_cp.position.distance_to_point(dcs_group.points[0].position)
<= <=
AGGRESIVE_MOVE_DISTANCE AGGRESIVE_MOVE_DISTANCE
): ):
attack_point = to_cp.position.random_point_within(500, 0) attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0)
)
else: else:
attack_point = self.find_offensive_point( attack_point = self.find_offensive_point(
dcs_group, dcs_group,
@ -334,7 +357,9 @@ class GroundConflictGenerator:
# If the enemy base is close enough, the units will attack the base # If the enemy base is close enough, the units will attack the base
if to_cp.position.distance_to_point( if to_cp.position.distance_to_point(
dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE: dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0) attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0)
)
else: else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE) attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
dcs_group.add_waypoint(attack_point, PointAction.OffRoad) dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
@ -351,10 +376,15 @@ class GroundConflictGenerator:
RANDOM_OFFSET_ATTACK RANDOM_OFFSET_ATTACK
) )
) )
dcs_group.add_waypoint(target.points[0].position+rand_offset, PointAction.OffRoad) target_point = self.conflict.theater.nearest_land_pos(
dcs_group.points[i].tasks.append(AttackGroup(target.id)) target.points[0].position+rand_offset
)
dcs_group.add_waypoint(target_point, PointAction.OffRoad)
dcs_group.points[i + 1].tasks.append(AttackGroup(target.id))
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE: if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0) attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0)
)
dcs_group.add_waypoint(attack_point) dcs_group.add_waypoint(attack_point)
if stance != CombatStance.RETREAT: if stance != CombatStance.RETREAT:
@ -373,10 +403,11 @@ class GroundConflictGenerator:
Handles adding the DCS tasks for APC and ATGM groups for all combat stances. Handles adding the DCS tasks for APC and ATGM groups for all combat stances.
Returns True if tasking was added, returns False if the stance was not a combat stance. Returns True if tasking was added, returns False if the stance was not a combat stance.
""" """
self._set_reform_waypoint(dcs_group, forward_heading)
if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]: if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
# APC & ATGM will never move too much forward, but will follow along any offensive # APC & ATGM will never move too much forward, but will follow along any offensive
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE: if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0) attack_point = self.conflict.theater.nearest_land_pos(to_cp.position.random_point_within(500, 0))
else: else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE) attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
dcs_group.add_waypoint(attack_point, PointAction.OffRoad) dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
@ -464,8 +495,8 @@ class GroundConflictGenerator:
self.mission.triggerrules.triggers.append(fallback) self.mission.triggerrules.triggers.append(fallback)
@staticmethod
def find_retreat_point( def find_retreat_point(
self,
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
frontline_heading: int, frontline_heading: int,
distance: int = RETREAT_DISTANCE distance: int = RETREAT_DISTANCE
@ -476,10 +507,14 @@ class GroundConflictGenerator:
:param frontline_heading: Heading of the frontline :param frontline_heading: Heading of the frontline
:return: dcs.mapping.Point object with the desired position :return: dcs.mapping.Point object with the desired position
""" """
return dcs_group.points[0].position.point_from_heading(frontline_heading-180, distance) desired_point = dcs_group.points[0].position.point_from_heading(heading_sum(frontline_heading, +180), distance)
if self.conflict.theater.is_on_land(desired_point):
return desired_point
return self.conflict.theater.nearest_land_pos(desired_point)
@staticmethod
def find_offensive_point( def find_offensive_point(
self,
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
frontline_heading: int, frontline_heading: int,
distance: int distance: int
@ -491,7 +526,10 @@ class GroundConflictGenerator:
:param distance: Distance of the offensive (how far unit should move) :param distance: Distance of the offensive (how far unit should move)
:return: dcs.mapping.Point object with the desired position :return: dcs.mapping.Point object with the desired position
""" """
return dcs_group.points[0].position.point_from_heading(frontline_heading, distance) desired_point = dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
if self.conflict.theater.is_on_land(desired_point):
return desired_point
return self.conflict.theater.nearest_land_pos(desired_point)
@staticmethod @staticmethod
def find_n_nearest_enemy_groups( def find_n_nearest_enemy_groups(
@ -562,10 +600,10 @@ class GroundConflictGenerator:
For artilery group, decide the distance from frontline with the range of the unit For artilery group, decide the distance from frontline with the range of the unit
""" """
rg = group.units[0].threat_range - 7500 rg = group.units[0].threat_range - 7500
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]: if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY] rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]
if rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK]: if rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] + 100 rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK]
return rg return rg
def get_valid_position_for_group( def get_valid_position_for_group(
@ -576,16 +614,13 @@ class GroundConflictGenerator:
heading: int, heading: int,
spawn_heading: int spawn_heading: int
): ):
i = 0 shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
while i < 1000: desired_point = shifted.point_from_heading(
shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width)) spawn_heading,
final_position = shifted.point_from_heading(spawn_heading, distance_from_frontline) random.randint(distance_from_frontline[0], distance_from_frontline[1])
)
return Conflict.find_ground_position(desired_point, combat_width, heading, self.conflict.theater)
if self.conflict.theater.is_on_land(final_position):
return final_position
i += 1
continue
return None
def _generate_groups( def _generate_groups(
self, self,
@ -618,7 +653,7 @@ class GroundConflictGenerator:
group.units[0], group.units[0],
len(group.units), len(group.units),
final_position, final_position,
distance_from_frontline, random.randint(distance_from_frontline[0], distance_from_frontline[1]),
heading=opposite_heading(spawn_heading), heading=opposite_heading(spawn_heading),
) )
if is_player: if is_player:
@ -657,7 +692,7 @@ class GroundConflictGenerator:
group = self.mission.vehicle_group( group = self.mission.vehicle_group(
side, side,
namegen.next_unit_name(side, cp.id, unit), unit, namegen.next_unit_name(side, cp.id, unit), unit,
position=self._group_point(at, distance_from_frontline), position=at,
group_size=count, group_size=count,
heading=heading, heading=heading,
move_formation=move_formation) move_formation=move_formation)

View File

@ -91,8 +91,12 @@ class Conflict:
return pos return pos
@classmethod @classmethod
def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater, coerce=True) -> Optional[Point]:
"""Finds the nearest valid ground position along a provided heading and it's inverse""" """
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
`coerce=True` will return the closest land position to `initial` regardless of heading or distance
`coerce=False` will return None if a point isn't found
"""
pos = initial pos = initial
if theater.is_on_land(pos): if theater.is_on_land(pos):
return pos return pos
@ -101,5 +105,9 @@ class Conflict:
if theater.is_on_land(pos): if theater.is_on_land(pos):
return pos return pos
pos = initial.point_from_heading(opposite_heading(heading), distance) pos = initial.point_from_heading(opposite_heading(heading), distance)
if coerce:
pos = theater.nearest_land_pos(initial)
return pos
logging.error("Didn't find ground position ({})!".format(initial)) logging.error("Didn't find ground position ({})!".format(initial))
return initial return None

View File

@ -187,14 +187,14 @@ class CombatGroupRole(Enum):
DISTANCE_FROM_FRONTLINE = { DISTANCE_FROM_FRONTLINE = {
CombatGroupRole.TANK:3200, CombatGroupRole.TANK: (2200, 3200),
CombatGroupRole.APC:8000, CombatGroupRole.APC: (7500, 8500),
CombatGroupRole.IFV:3700, CombatGroupRole.IFV: (2700, 3700),
CombatGroupRole.ARTILLERY:18000, CombatGroupRole.ARTILLERY: (16000, 18000),
CombatGroupRole.SHORAD:13000, CombatGroupRole.SHORAD: (12000, 13000),
CombatGroupRole.LOGI:20000, CombatGroupRole.LOGI: (18000, 20000),
CombatGroupRole.INFANTRY:3000, CombatGroupRole.INFANTRY: (2800, 3300),
CombatGroupRole.ATGM:6200 CombatGroupRole.ATGM: (5200, 6200),
} }
GROUP_SIZES_BY_COMBAT_STANCE = { GROUP_SIZES_BY_COMBAT_STANCE = {

Binary file not shown.

Binary file not shown.