mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Factor AI purchases out of Game.
For now this is mostly behavior preserving. I slightly improved the ability to buy units when multiple front lines exist by removing full bases as candidates, but it should be a minor change at best. A larger improvement will come later. This is also written such that it will work for the player as well. The procurer currently runs for the player but with all the options off, so it does nothing. The next patch allows adds options for the player to use auto-procurement.
This commit is contained in:
parent
bac47dad83
commit
1adee0af17
@ -31,25 +31,25 @@ class Faction:
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[UnitType] = field(default_factory=list)
|
||||
aircrafts: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[UnitType] = field(default_factory=list)
|
||||
awacs: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available tanker aircraft
|
||||
tankers: List[UnitType] = field(default_factory=list)
|
||||
tankers: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available frontline units
|
||||
frontline_units: List[VehicleType] = field(default_factory=list)
|
||||
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Available artillery units
|
||||
artillery_units: List[VehicleType] = field(default_factory=list)
|
||||
artillery_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Infantry units used
|
||||
infantry_units: List[VehicleType] = field(default_factory=list)
|
||||
infantry_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Logistics units used
|
||||
logistics_units: List[VehicleType] = field(default_factory=list)
|
||||
logistics_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Possible SAMS site generators for this faction
|
||||
air_defenses: List[str] = field(default_factory=list)
|
||||
@ -64,10 +64,10 @@ class Faction:
|
||||
requirements: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# possible aircraft carrier units
|
||||
aircraft_carrier: List[UnitType] = field(default_factory=list)
|
||||
aircraft_carrier: List[Type[UnitType]] = field(default_factory=list)
|
||||
|
||||
# possible helicopter carrier units
|
||||
helicopter_carrier: List[UnitType] = field(default_factory=list)
|
||||
helicopter_carrier: List[Type[UnitType]] = field(default_factory=list)
|
||||
|
||||
# Possible carrier names
|
||||
carrier_names: List[str] = field(default_factory=list)
|
||||
@ -79,10 +79,10 @@ class Faction:
|
||||
navy_generators: List[str] = field(default_factory=list)
|
||||
|
||||
# Available destroyers
|
||||
destroyers: List[str] = field(default_factory=list)
|
||||
destroyers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# Available cruisers
|
||||
cruisers: List[str] = field(default_factory=list)
|
||||
cruisers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# How many navy group should we try to generate per CP on startup for this faction
|
||||
navy_group_count: int = field(default=1)
|
||||
@ -94,7 +94,7 @@ class Faction:
|
||||
has_jtac: bool = field(default=False)
|
||||
|
||||
# Unit to use as JTAC for this faction
|
||||
jtac_unit: Optional[FlyingType] = field(default=None)
|
||||
jtac_unit: Optional[Type[FlyingType]] = field(default=None)
|
||||
|
||||
# doctrine
|
||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||
@ -103,7 +103,8 @@ class Faction:
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[UnitType, List[str]] = field(default_factory=dict)
|
||||
liveries_overrides: Dict[Type[UnitType], List[str]] = field(
|
||||
default_factory=dict)
|
||||
|
||||
#: Set to True if the faction should force the "Unrestricted satnav" option
|
||||
#: for the mission. This option enables GPS for capable aircraft regardless
|
||||
@ -210,13 +211,14 @@ class Faction:
|
||||
return faction
|
||||
|
||||
@property
|
||||
def units(self) -> List[UnitType]:
|
||||
def units(self) -> List[Type[UnitType]]:
|
||||
return (self.infantry_units + self.aircrafts + self.awacs +
|
||||
self.artillery_units + self.frontline_units +
|
||||
self.tankers + self.logistics_units)
|
||||
|
||||
|
||||
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[UnitType]:
|
||||
def unit_loader(
|
||||
unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
|
||||
"""
|
||||
Find unit by name
|
||||
:param unit: Unit name as string
|
||||
@ -239,13 +241,13 @@ def unit_loader(unit: str, class_repository: List[Any]) -> Optional[UnitType]:
|
||||
return None
|
||||
|
||||
|
||||
def load_aircraft(name: str) -> Optional[FlyingType]:
|
||||
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_aircraft(data) -> List[FlyingType]:
|
||||
def load_all_aircraft(data) -> List[Type[FlyingType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_aircraft(name)
|
||||
@ -254,13 +256,13 @@ def load_all_aircraft(data) -> List[FlyingType]:
|
||||
return items
|
||||
|
||||
|
||||
def load_vehicle(name: str) -> Optional[VehicleType]:
|
||||
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_vehicles(data) -> List[VehicleType]:
|
||||
def load_all_vehicles(data) -> List[Type[VehicleType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_vehicle(name)
|
||||
@ -269,11 +271,11 @@ def load_all_vehicles(data) -> List[VehicleType]:
|
||||
return items
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[ShipType]:
|
||||
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
|
||||
|
||||
|
||||
def load_all_ships(data) -> List[ShipType]:
|
||||
def load_all_ships(data) -> List[Type[ShipType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_ship(name)
|
||||
|
||||
127
game/game.py
127
game/game.py
@ -25,8 +25,9 @@ from .event.event import Event, UnitsDeliveryEvent
|
||||
from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .factions.faction import Faction
|
||||
from .infos.information import Information
|
||||
from .procurement import ProcurementAi
|
||||
from .settings import Settings
|
||||
from .theater import Airfield, ConflictTheater, ControlPoint, OffMapSpawn
|
||||
from .theater import ConflictTheater, ControlPoint
|
||||
from .unitmap import UnitMap
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
@ -90,6 +91,9 @@ class Game:
|
||||
self.__destroyed_units: List[str] = []
|
||||
self.savepath = ""
|
||||
self.budget = PLAYER_BUDGET_INITIAL
|
||||
# The enemy currently doesn't buy anything on turn zero; they get
|
||||
# pre-populated airbases that are generated by the new game generator.
|
||||
self.enemy_budget = 0
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
|
||||
@ -158,9 +162,21 @@ class Game:
|
||||
reward += REWARDS[g.category]
|
||||
return int(reward * self.settings.player_income_multiplier)
|
||||
|
||||
def _budget_player(self):
|
||||
def process_player_income(self):
|
||||
self.budget += self.budget_reward_amount
|
||||
|
||||
def process_enemy_income(self):
|
||||
if not hasattr(self, "enemy_budget"):
|
||||
self.enemy_budget = 0
|
||||
|
||||
production = 0.0
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
production = production + REWARDS[g.category]
|
||||
|
||||
self.enemy_budget += production * self.settings.enemy_income_multiplier
|
||||
|
||||
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
|
||||
event = UnitsDeliveryEvent(attacker_name=self.player_name,
|
||||
defender_name=self.player_name,
|
||||
@ -212,8 +228,25 @@ class Game:
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.process_turn()
|
||||
|
||||
self._enemy_reinforcement()
|
||||
self._budget_player()
|
||||
self.process_enemy_income()
|
||||
self.enemy_budget = ProcurementAi(
|
||||
self,
|
||||
for_player=False,
|
||||
faction=self.enemy_faction,
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True
|
||||
).spend_budget(self.enemy_budget)
|
||||
|
||||
self.process_player_income()
|
||||
self.budget = ProcurementAi(
|
||||
self,
|
||||
for_player=True,
|
||||
faction=self.player_faction,
|
||||
manage_runways=False,
|
||||
manage_front_line=False,
|
||||
manage_aircraft=False
|
||||
).spend_budget(self.budget)
|
||||
|
||||
if not no_action and self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
@ -255,90 +288,8 @@ class Game:
|
||||
gplanner.plan_groundwar()
|
||||
self.ground_planners[cp.id] = gplanner
|
||||
|
||||
def _enemy_reinforcement(self):
|
||||
"""
|
||||
Compute and commision reinforcement for enemy bases
|
||||
"""
|
||||
|
||||
MAX_ARMOR = 30 * self.settings.multiplier
|
||||
MAX_AIRCRAFT = 25 * self.settings.multiplier
|
||||
|
||||
production = 0.0
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
production = production + REWARDS[g.category]
|
||||
|
||||
budget = production * self.settings.enemy_income_multiplier
|
||||
|
||||
for control_point in self.theater.enemy_points():
|
||||
if budget < db.RUNWAY_REPAIR_COST:
|
||||
break
|
||||
if control_point.runway_can_be_repaired:
|
||||
control_point.begin_runway_repair()
|
||||
budget -= db.RUNWAY_REPAIR_COST
|
||||
self.informations.append(Information(
|
||||
f"OPFOR has begun repairing the runway at {control_point}"))
|
||||
|
||||
budget_for_armored_units = budget / 2
|
||||
budget_for_aircraft = budget / 2
|
||||
|
||||
potential_cp_armor = []
|
||||
for cp in self.theater.enemy_points():
|
||||
for cpe in cp.connected_points:
|
||||
if cpe.captured and cp.base.total_armor < MAX_ARMOR:
|
||||
potential_cp_armor.append(cp)
|
||||
if len(potential_cp_armor) == 0:
|
||||
potential_cp_armor = self.theater.enemy_points()
|
||||
|
||||
potential_cp_armor = [p for p in potential_cp_armor if
|
||||
not isinstance(p, OffMapSpawn)]
|
||||
|
||||
i = 0
|
||||
potential_units = db.FACTIONS[self.enemy_name].frontline_units
|
||||
|
||||
print("Enemy Recruiting")
|
||||
print(potential_cp_armor)
|
||||
print(budget_for_armored_units)
|
||||
print(potential_units)
|
||||
|
||||
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
|
||||
while budget_for_armored_units > 0:
|
||||
i = i + 1
|
||||
if i > 50 or budget_for_armored_units <= 0:
|
||||
break
|
||||
target_cp = random.choice(potential_cp_armor)
|
||||
if target_cp.base.total_armor >= MAX_ARMOR:
|
||||
continue
|
||||
unit = random.choice(potential_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
budget_for_armored_units -= price * 2
|
||||
target_cp.base.armor[unit] = target_cp.base.armor.get(unit, 0) + 2
|
||||
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
|
||||
print(str(info))
|
||||
self.informations.append(info)
|
||||
|
||||
if budget_for_armored_units > 0:
|
||||
budget_for_aircraft += budget_for_armored_units
|
||||
|
||||
potential_units = [u for u in db.FACTIONS[self.enemy_name].aircrafts
|
||||
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
|
||||
|
||||
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
|
||||
while budget_for_aircraft > 0:
|
||||
i = i + 1
|
||||
if i > 50 or budget_for_aircraft <= 0:
|
||||
break
|
||||
target_cp = random.choice(potential_cp_armor)
|
||||
if target_cp.base.total_aircraft >= MAX_AIRCRAFT:
|
||||
continue
|
||||
unit = random.choice(potential_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
budget_for_aircraft -= price * 2
|
||||
target_cp.base.aircraft[unit] = target_cp.base.aircraft.get(unit, 0) + 2
|
||||
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
|
||||
print(str(info))
|
||||
self.informations.append(info)
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
|
||||
@property
|
||||
def current_turn_time_of_day(self) -> TimeOfDay:
|
||||
|
||||
179
game/procurement.py
Normal file
179
game/procurement.py
Normal file
@ -0,0 +1,179 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from typing import List, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.task import CAP, CAS
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import ControlPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class AircraftProcurer:
|
||||
def __init__(self, faction: Faction) -> None:
|
||||
self.faction = faction
|
||||
|
||||
|
||||
class ProcurementAi:
|
||||
def __init__(self, game: Game, for_player: bool, faction: Faction,
|
||||
manage_runways: bool, manage_front_line: bool,
|
||||
manage_aircraft: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = for_player
|
||||
self.faction = faction
|
||||
self.manage_runways = manage_runways
|
||||
self.manage_front_line = manage_front_line
|
||||
self.manage_aircraft = manage_aircraft
|
||||
|
||||
def spend_budget(self, budget: int) -> int:
|
||||
if self.manage_runways:
|
||||
budget = self.repair_runways(budget)
|
||||
if self.manage_front_line:
|
||||
armor_budget = math.ceil(budget / 2)
|
||||
budget -= armor_budget
|
||||
budget += self.reinforce_front_line(armor_budget)
|
||||
if self.manage_aircraft:
|
||||
budget = self.purchase_aircraft(budget)
|
||||
return budget
|
||||
|
||||
def repair_runways(self, budget: int) -> int:
|
||||
for control_point in self.owned_points:
|
||||
if budget < db.RUNWAY_REPAIR_COST:
|
||||
break
|
||||
if control_point.runway_can_be_repaired:
|
||||
control_point.begin_runway_repair()
|
||||
budget -= db.RUNWAY_REPAIR_COST
|
||||
if self.is_player:
|
||||
self.game.message(
|
||||
"OPFOR has begun repairing the runway at "
|
||||
f"{control_point}"
|
||||
)
|
||||
else:
|
||||
self.game.message(
|
||||
"We have begun repairing the runway at "
|
||||
f"{control_point}"
|
||||
)
|
||||
return budget
|
||||
|
||||
def reinforce_front_line(self, budget: int) -> int:
|
||||
if not self.faction.frontline_units:
|
||||
return budget
|
||||
|
||||
armor_limit = int(30 * self.game.settings.multiplier)
|
||||
candidates = self.front_line_candidates(armor_limit)
|
||||
if not candidates:
|
||||
return budget
|
||||
|
||||
# TODO: No need to limit?
|
||||
for _ in range(50):
|
||||
if budget <= 0:
|
||||
break
|
||||
|
||||
cp = random.choice(candidates)
|
||||
unit = random.choice(self.faction.frontline_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
# TODO: Don't allow negative budget.
|
||||
# Build a list of only affordable units and choose from those.
|
||||
budget -= price * 2
|
||||
cp.base.armor[unit] = cp.base.armor.get(unit, 0) + 2
|
||||
self.reinforcement_message(unit, cp)
|
||||
|
||||
if cp.base.total_armor >= armor_limit:
|
||||
candidates.remove(cp)
|
||||
if not candidates:
|
||||
break
|
||||
|
||||
return budget
|
||||
|
||||
def purchase_aircraft(self, budget: int) -> int:
|
||||
aircraft_limit = int(25 * self.game.settings.multiplier)
|
||||
candidates = self.airbase_candidates(aircraft_limit)
|
||||
if not candidates:
|
||||
return budget
|
||||
|
||||
unit_pool = [u for u in self.faction.aircrafts
|
||||
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
|
||||
if not unit_pool:
|
||||
return budget
|
||||
|
||||
# TODO: No need to limit?
|
||||
for _ in range(50):
|
||||
if budget <= 0:
|
||||
break
|
||||
|
||||
cp = random.choice(candidates)
|
||||
unit = random.choice(unit_pool)
|
||||
price = db.PRICES[unit] * 2
|
||||
# TODO: Don't allow negative budget.
|
||||
# Build a list of only affordable units and choose from those.
|
||||
budget -= price * 2
|
||||
cp.base.aircraft[unit] = cp.base.aircraft.get(unit, 0) + 2
|
||||
self.reinforcement_message(unit, cp)
|
||||
|
||||
if cp.base.total_aircraft >= aircraft_limit:
|
||||
candidates.remove(cp)
|
||||
if not candidates:
|
||||
break
|
||||
|
||||
return budget
|
||||
|
||||
def reinforcement_message(self, unit_type: Type[UnitType],
|
||||
control_point: ControlPoint) -> None:
|
||||
description = f"{unit_type.id} x 2 at {control_point.name}"
|
||||
if self.is_player:
|
||||
self.game.message(f"Our reinforcements: {description}")
|
||||
else:
|
||||
self.game.message(f"OPFOR reinforcements: {description}")
|
||||
|
||||
@property
|
||||
def owned_points(self) -> List[ControlPoint]:
|
||||
if self.is_player:
|
||||
return self.game.theater.player_points()
|
||||
else:
|
||||
return self.game.theater.enemy_points()
|
||||
|
||||
def airbase_candidates(self, unit_limit: int) -> List[ControlPoint]:
|
||||
candidates = []
|
||||
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
# TODO: Buy aircraft where they are needed, not at front lines.
|
||||
for cp in self.owned_points:
|
||||
if cp.base.total_aircraft >= unit_limit:
|
||||
continue
|
||||
for connected in cp.connected_points:
|
||||
if not connected.is_friendly(to_player=self.is_player):
|
||||
candidates.append(cp)
|
||||
|
||||
if not candidates:
|
||||
# Otherwise buy them anywhere valid.
|
||||
candidates = [p for p in self.owned_points
|
||||
if p.can_deploy_ground_units]
|
||||
|
||||
return candidates
|
||||
|
||||
def front_line_candidates(self, unit_limit: int) -> List[ControlPoint]:
|
||||
candidates = []
|
||||
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
for cp in self.owned_points:
|
||||
if cp.base.total_armor >= unit_limit:
|
||||
continue
|
||||
for connected in cp.connected_points:
|
||||
if not connected.is_friendly(to_player=self.is_player):
|
||||
candidates.append(cp)
|
||||
|
||||
if not candidates:
|
||||
# Otherwise buy them anywhere valid.
|
||||
candidates = [p for p in self.owned_points
|
||||
if p.can_deploy_ground_units]
|
||||
|
||||
return candidates
|
||||
@ -280,6 +280,11 @@ class ControlPoint(MissionTarget, ABC):
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def total_aircraft_parking(self):
|
||||
@ -498,6 +503,10 @@ class Airfield(ControlPoint):
|
||||
def parking_slots(self) -> Iterator[ParkingSlot]:
|
||||
yield from self.airport.parking_slots
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
@ -553,6 +562,10 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
def moveable(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class Carrier(NavalControlPoint):
|
||||
|
||||
@ -637,6 +650,10 @@ class OffMapSpawn(ControlPoint):
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return RunwayStatus()
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class Fob(ControlPoint):
|
||||
|
||||
@ -684,3 +701,7 @@ class Fob(ControlPoint):
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user