mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
225 lines
8.9 KiB
Python
225 lines
8.9 KiB
Python
import logging
|
|
import typing
|
|
import math
|
|
import itertools
|
|
|
|
from dcs.planes import *
|
|
from dcs.vehicles import *
|
|
from dcs.task import *
|
|
|
|
from game import db
|
|
from gen import aaa
|
|
|
|
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
|
PLANES_SCRAMBLE_MIN_BASE = 2
|
|
PLANES_SCRAMBLE_MAX_BASE = 8
|
|
PLANES_SCRAMBLE_FACTOR = 0.3
|
|
|
|
BASE_MAX_STRENGTH = 1
|
|
BASE_MIN_STRENGTH = 0
|
|
|
|
|
|
class Base:
|
|
aircraft = {} # type: typing.Dict[PlaneType, int]
|
|
armor = {} # type: typing.Dict[Armor, int]
|
|
aa = {} # type: typing.Dict[AirDefence, int]
|
|
strength = 1 # type: float
|
|
commision_points = {}
|
|
|
|
def __init__(self):
|
|
self.aircraft = {}
|
|
self.armor = {}
|
|
self.aa = {}
|
|
self.commision_points = {}
|
|
self.strength = 1
|
|
|
|
@property
|
|
def total_planes(self) -> int:
|
|
return sum(self.aircraft.values())
|
|
|
|
@property
|
|
def total_armor(self) -> int:
|
|
return sum(self.armor.values())
|
|
|
|
@property
|
|
def total_aa(self) -> int:
|
|
return sum(self.aa.values())
|
|
|
|
def total_units(self, task: Task) -> int:
|
|
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]])
|
|
|
|
def total_units_of_type(self, unit_type) -> int:
|
|
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type])
|
|
|
|
@property
|
|
def all_units(self):
|
|
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items())
|
|
|
|
def _find_best_unit(self, dict, for_type: Task, count: int) -> typing.Dict:
|
|
if count <= 0:
|
|
logging.warning("{}: no units for {}".format(self, for_type))
|
|
return {}
|
|
|
|
sorted_units = [key for key in dict.keys() if key in db.UNIT_BY_TASK[for_type]]
|
|
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
|
|
|
|
result = {}
|
|
for unit_type in sorted_units:
|
|
existing_count = dict[unit_type] # type: int
|
|
if not existing_count:
|
|
continue
|
|
|
|
if count <= 0:
|
|
break
|
|
|
|
result_unit_count = min(count, existing_count)
|
|
count -= result_unit_count
|
|
|
|
assert result_unit_count > 0
|
|
result[unit_type] = result.get(unit_type, 0) + result_unit_count
|
|
|
|
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
|
|
return result
|
|
|
|
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]:
|
|
return self._find_best_unit(self.aircraft, for_type, count)
|
|
|
|
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
|
|
return self._find_best_unit(self.armor, for_type, count)
|
|
|
|
def append_commision_points(self, for_type, points: float) -> int:
|
|
self.commision_points[for_type] = self.commision_points.get(for_type, 0) + points
|
|
points = self.commision_points[for_type]
|
|
if points >= 1:
|
|
self.commision_points[for_type] = points - math.floor(points)
|
|
return int(math.floor(points))
|
|
|
|
return 0
|
|
|
|
def filter_units(self, applicable_units: typing.Collection):
|
|
self.aircraft = {k: v for k, v in self.aircraft.items() if k in applicable_units}
|
|
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
|
|
|
|
def commision_units(self, units: typing.Dict[typing.Any, int]):
|
|
for value in units.values():
|
|
assert value > 0
|
|
assert value == math.floor(value)
|
|
|
|
for unit_type, unit_count in units.items():
|
|
for_task = db.unit_task(unit_type)
|
|
|
|
target_dict = None
|
|
if for_task == CAS or for_task == CAP or for_task == Embarking:
|
|
target_dict = self.aircraft
|
|
elif for_task == PinpointStrike:
|
|
target_dict = self.armor
|
|
elif for_task == AirDefence:
|
|
target_dict = self.aa
|
|
|
|
assert target_dict is not None
|
|
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
|
|
|
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
|
|
# advanced SAM sites have multiple units - this code was not at all set up to handle that
|
|
# to avoid having to restructure a bunch of upstream code, we track total destroyed units and
|
|
# use that to determine if a site was destroyed
|
|
# this can be thought of as the enemy re-distributing parts of SAM sites to keep as many
|
|
# operational as possible (pulling specific units from ...storage... to bring them back online
|
|
# if non-letal damage was done)
|
|
# in the future, I may add more depth to this (e.g. a base having a certain number of spares and tracking
|
|
# the number of pieces of each site), but for now this is what we get
|
|
sams_destroyed = {}
|
|
# we count complex SAM sites at the end - don't double count
|
|
aa_skip = [
|
|
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
|
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
|
AirDefence.SAM_SA_11_Buk_LN_9A310M1
|
|
]
|
|
for unit_type, count in units_lost.items():
|
|
if unit_type in db.SAM_CONVERT or unit_type in db.SAM_CONVERT['except']:
|
|
# unit is part of an advanced SAM site, which means it will fail the below check
|
|
try:
|
|
sams_destroyed[unit_type] += 1
|
|
except KeyError:
|
|
sams_destroyed[unit_type] = 1
|
|
if unit_type in self.aircraft:
|
|
target_array = self.aircraft
|
|
elif unit_type in self.armor:
|
|
target_array = self.armor
|
|
elif unit_type in self.aa and unit_type not in aa_skip:
|
|
target_array = self.aa
|
|
else:
|
|
print("Base didn't find event type {}".format(unit_type))
|
|
continue
|
|
|
|
if unit_type not in target_array:
|
|
print("Base didn't find event type {}".format(unit_type))
|
|
continue
|
|
|
|
target_array[unit_type] = max(target_array[unit_type] - count, 0)
|
|
if target_array[unit_type] == 0:
|
|
del target_array[unit_type]
|
|
|
|
# now that we have a complete picture of the SAM sites destroyed, determine if any were destroyed
|
|
for sam_site, count in sams_destroyed.items():
|
|
dead_count = aaa.num_sam_dead(sam_site, count)
|
|
try:
|
|
modified_sam_site = db.SAM_CONVERT[sam_site]
|
|
except KeyError:
|
|
modified_sam_site = db.SAM_CONVERT[sam_site]['except']
|
|
|
|
if modified_sam_site in self.aa:
|
|
self.aa[modified_sam_site] = max(
|
|
self.aa[modified_sam_site] - dead_count,
|
|
0
|
|
)
|
|
if self.aa[modified_sam_site] == 0:
|
|
del self.aa[modified_sam_site]
|
|
|
|
def affect_strength(self, amount):
|
|
self.strength += amount
|
|
if self.strength > BASE_MAX_STRENGTH:
|
|
self.strength = BASE_MAX_STRENGTH
|
|
elif self.strength <= 0:
|
|
self.strength = BASE_MIN_STRENGTH
|
|
|
|
def scramble_count(self, multiplier: float, task: Task = None) -> int:
|
|
if task:
|
|
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
|
|
else:
|
|
count = self.total_planes
|
|
|
|
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
|
|
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
|
|
|
|
def assemble_count(self):
|
|
return int(self.total_armor * 0.5)
|
|
|
|
def assemble_aa_count(self) -> int:
|
|
# previous logic removed because we always want the full air defense capabilities.
|
|
return self.total_aa
|
|
|
|
def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
|
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
|
|
|
def scramble_last_defense(self):
|
|
# return as many CAP-capable aircraft as we can since this is the last defense of the base
|
|
# (but not more than 20 - that's just nuts)
|
|
return self._find_best_planes(CAP, min(self.total_planes, 20))
|
|
|
|
def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
|
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
|
|
|
|
def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
|
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
|
|
|
def assemble_attack(self) -> typing.Dict[Armor, int]:
|
|
return self._find_best_armor(PinpointStrike, self.assemble_count())
|
|
|
|
def assemble_defense(self) -> typing.Dict[Armor, int]:
|
|
count = int(self.total_armor * min(self.strength + 0.5, 1))
|
|
return self._find_best_armor(PinpointStrike, count)
|
|
|
|
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
|
|
return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count())
|