Move the AI to the normal procurement system.

The procurement AI now uses the same system as the players. Orders are
placed and take a turn to fulfill.

This has a few advantages:

* We no longer need special case purchase logic for the turn 0
  population of opfor airbases.
* Players using auto-purchase can cancel orders they don't like.
This commit is contained in:
Dan Albert 2020-12-05 22:36:17 -08:00
parent 7226359e64
commit d519aa1dad
4 changed files with 34 additions and 80 deletions

View File

@ -374,7 +374,11 @@ class UnitsDeliveryEvent(Event):
def skip(self) -> None:
for k, v in self.units.items():
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
self.game.informations.append(info)
if self.to_cp.captured:
name = "Ally "
else:
name = "Enemy "
self.game.message(
f"{name} reinforcements: {k.id} x {v} at {self.to_cp.name}")
self.to_cp.base.commision_units(self.units)

View File

@ -91,9 +91,7 @@ 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.enemy_budget = PLAYER_BUDGET_INITIAL
self.current_unit_id = 0
self.current_group_id = 0
@ -110,6 +108,8 @@ class Game:
cp.pending_unit_deliveries = self.units_delivery_event(cp)
self.sanitize_sides()
# Turn 0 procurement.
self.plan_procurement()
self.on_load()
def generate_conditions(self) -> Conditions:
@ -229,24 +229,8 @@ class Game:
control_point.process_turn()
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=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements
).spend_budget(self.budget)
if not no_action and self.turn > 1:
for cp in self.theater.player_points():
@ -288,6 +272,27 @@ class Game:
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
self.plan_procurement()
def plan_procurement(self) -> None:
self.budget = ProcurementAi(
self,
for_player=True,
faction=self.player_faction,
manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements
).spend_budget(self.budget)
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)
def message(self, text: str) -> None:
self.informations.append(Information(text, turn=self.turn))

View File

@ -86,8 +86,8 @@ class ProcurementAi:
break
budget -= db.PRICES[unit]
cp.base.armor[unit] = cp.base.armor.get(unit, 0) + 1
self.reinforcement_message(unit, cp, group_size=1)
assert cp.pending_unit_deliveries is not None
cp.pending_unit_deliveries.deliver({unit: 1})
if cp.base.total_armor >= armor_limit:
candidates.remove(cp)
@ -127,8 +127,8 @@ class ProcurementAi:
break
budget -= db.PRICES[unit] * group_size
cp.base.aircraft[unit] = cp.base.aircraft.get(unit, 0) + group_size
self.reinforcement_message(unit, cp, group_size)
assert cp.pending_unit_deliveries is not None
cp.pending_unit_deliveries.deliver({unit: group_size})
if cp.base.total_aircraft >= aircraft_limit:
candidates.remove(cp)
@ -137,15 +137,6 @@ class ProcurementAi:
return budget
def reinforcement_message(self, unit_type: Type[UnitType],
control_point: ControlPoint,
group_size: int) -> None:
description = f"{unit_type.id} x {group_size} 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:

View File

@ -91,7 +91,6 @@ class GameGenerator:
# Reset name generator
namegen.reset()
self.prepare_theater()
self.populate_red_airbases()
game = Game(player_name=self.player,
enemy_name=self.enemy,
theater=self.theater,
@ -134,51 +133,6 @@ class GameGenerator:
else:
cp.captured = True
def populate_red_airbases(self) -> None:
for control_point in self.theater.enemy_points():
if control_point.captured:
continue
self.populate_red_airbase(control_point)
def populate_red_airbase(self, control_point: ControlPoint) -> None:
# Force reset cp on generation
control_point.base.aircraft = {}
control_point.base.armor = {}
control_point.base.aa = {}
control_point.base.commision_points = {}
control_point.base.strength = 1
# The tasks here are confusing. PinpointStrike for some reason means
# ground units.
for task in [PinpointStrike, CAP, CAS, AirDefence]:
if isinstance(control_point, OffMapSpawn):
# Off-map spawn locations start with no aircraft.
continue
if IMPORTANCE_HIGH <= control_point.importance <= IMPORTANCE_LOW:
raise ValueError(
f"CP importance must be between {IMPORTANCE_LOW} and "
f"{IMPORTANCE_HIGH}, is {control_point.importance}")
importance_factor = ((control_point.importance - IMPORTANCE_LOW) /
(IMPORTANCE_HIGH - IMPORTANCE_LOW))
# noinspection PyTypeChecker
unit_types = db.choose_units(task, importance_factor, UNIT_VARIETY,
self.enemy)
if not unit_types:
continue
count_log = math.log(control_point.importance + 0.01,
UNIT_COUNT_IMPORTANCE_LOG)
count = max((COUNT_BY_TASK[task] *
self.generator_settings.multiplier *
(count_log + 1)),
1)
count_per_type = max(int(float(count) / len(unit_types)), 1)
for unit_type in unit_types:
control_point.base.commision_units({unit_type: count_per_type})
class LocationFinder:
def __init__(self, game: Game, control_point: ControlPoint) -> None: