Maintain composition when buying ground units.

Unit composition is defined by the doctrine. The most understaffed CP
will now get the most underrepresented unit type. Previously a random
understaffed CP would get a random unit type.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1057.
This commit is contained in:
Florian
2021-05-30 12:52:35 -07:00
committed by Dan Albert
parent d4fe893539
commit d61382f4e2
11 changed files with 443 additions and 319 deletions

View File

@@ -14,7 +14,8 @@ from game.utils import Distance
from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
from game.data.groundunitclass import GroundUnitClass
if TYPE_CHECKING:
from game import Game
@@ -114,28 +115,14 @@ class ProcurementAi:
)
return budget
def random_affordable_ground_unit(
self, budget: float, cp: ControlPoint
def affordable_ground_unit_of_class(
self, budget: float, unit_class: GroundUnitClass
) -> Optional[Type[VehicleType]]:
affordable_units = [
u
for u in self.faction.frontline_units + self.faction.artillery_units
if db.PRICES[u] <= budget
]
total_number_aa = (
cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
faction_units = set(self.faction.frontline_units) | set(
self.faction.artillery_units
)
total_non_aa = (
cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
)
max_aa = math.ceil(total_non_aa / 8)
# Limit the number of AA units the AI will buy
if not total_number_aa < max_aa:
for unit in [u for u in affordable_units if u in TYPE_SHORAD]:
affordable_units.remove(unit)
of_class = set(unit_class.unit_list) & faction_units
affordable_units = [u for u in of_class if db.PRICES[u] <= budget]
if not affordable_units:
return None
return random.choice(affordable_units)
@@ -147,12 +134,12 @@ class ProcurementAi:
# TODO: Attempt to transfer from reserves.
while budget > 0:
candidates = self.front_line_candidates()
if not candidates:
cp = self.ground_reinforcement_candidate()
if cp is None:
break
cp = random.choice(candidates)
unit = self.random_affordable_ground_unit(budget, cp)
most_needed_type = self.most_needed_unit_class(cp)
unit = self.affordable_ground_unit_of_class(budget, most_needed_type)
if unit is None:
# Can't afford any more units.
break
@@ -162,6 +149,31 @@ class ProcurementAi:
return budget
def most_needed_unit_class(self, cp: ControlPoint) -> GroundUnitClass:
worst_balanced: Optional[GroundUnitClass] = None
worst_fulfillment = math.inf
for unit_class in GroundUnitClass:
if not self.faction.has_access_to_unittype(unit_class):
continue
current_ratio = self.cost_ratio_of_ground_unit(cp, unit_class)
desired_ratio = (
self.faction.doctrine.ground_unit_procurement_ratios.for_unit_class(
unit_class
)
)
if not desired_ratio:
continue
if current_ratio >= desired_ratio:
continue
fulfillment = current_ratio / desired_ratio
if fulfillment < worst_fulfillment:
worst_fulfillment = fulfillment
worst_balanced = unit_class
if worst_balanced is None:
return GroundUnitClass.Tank
return worst_balanced
def _affordable_aircraft_for_task(
self,
task: FlightType,
@@ -256,56 +268,71 @@ class ProcurementAi:
yield cp
yield from threatened
def front_line_candidates(self) -> List[ControlPoint]:
candidates = []
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
worst_supply = math.inf
understaffed: Optional[ControlPoint] = None
# Prefer to buy front line units at active front lines that are not
# already overloaded.
for cp in self.owned_points:
total_ground_units_allocated_to_this_control_point = (
self.total_ground_units_allocated_to(cp)
)
if not cp.has_ground_unit_source(self.game):
if not cp.has_active_frontline:
continue
if not cp.has_ground_unit_source(self.game):
# No source of ground units, so can't buy anything.
continue
allocated = cp.allocated_ground_units(self.game.transfers)
if (
total_ground_units_allocated_to_this_control_point >= 50
or total_ground_units_allocated_to_this_control_point
>= cp.frontline_unit_count_limit
allocated.total >= self.game.settings.front_line_procurement_target
or allocated.total >= cp.frontline_unit_count_limit
):
# Control point is already sufficiently defended.
continue
for connected in cp.connected_points:
if not connected.is_friendly(to_player=self.is_player):
candidates.append(cp)
if allocated.total < worst_supply:
worst_supply = allocated.total
understaffed = cp
if not candidates:
# Otherwise buy reserves, but don't exceed 10 reserve units per CP.
# These units do not exist in the world until the CP becomes
# connected to an active front line, at which point all these units
# will suddenly appear at the gates of the newly captured CP.
#
# To avoid sudden overwhelming numbers of units we avoid buying
# many.
#
# Also, do not bother buying units at bases that will never connect
# to a front line.
for cp in self.owned_points:
if not cp.can_recruit_ground_units(self.game):
continue
if self.total_ground_units_allocated_to(cp) >= 10:
continue
if cp.is_global:
continue
candidates.append(cp)
if understaffed is not None:
return understaffed
return candidates
# Otherwise buy reserves, but don't exceed the amount defined in the settings.
# These units do not exist in the world until the CP becomes
# connected to an active front line, at which point all these units
# will suddenly appear at the gates of the newly captured CP.
#
# To avoid sudden overwhelming numbers of units we avoid buying
# many.
#
# Also, do not bother buying units at bases that will never connect
# to a front line.
for cp in self.owned_points:
if cp.is_global:
continue
if not cp.can_recruit_ground_units(self.game):
continue
def total_ground_units_allocated_to(self, control_point: ControlPoint) -> int:
total = control_point.expected_ground_units_next_turn.total
for transfer in self.game.transfers:
if transfer.destination == control_point:
total += sum(transfer.units.values())
return total
allocated = cp.allocated_ground_units(self.game.transfers)
if allocated.total >= self.game.settings.reserves_procurement_target:
continue
if allocated.total < worst_supply:
worst_supply = allocated.total
understaffed = cp
return understaffed
def cost_ratio_of_ground_unit(
self, control_point: ControlPoint, unit_class: GroundUnitClass
) -> float:
allocations = control_point.allocated_ground_units(self.game.transfers)
class_cost = 0
total_cost = 0
for unit_type, count in allocations.all.items():
cost = db.PRICES[unit_type] * count
total_cost += cost
if unit_type in unit_class:
class_cost += cost
if not total_cost:
return 0
return class_cost / total_cost