diff --git a/game/game.py b/game/game.py index df9cae2a..7aba517e 100644 --- a/game/game.py +++ b/game/game.py @@ -8,6 +8,7 @@ from dcs.vehicles import * from game.game_stats import GameStats from gen.conflictgen import Conflict +from gen.flights.ai_flight_planner import FlightPlanner from userdata.debriefing import Debriefing from theater import * @@ -111,6 +112,7 @@ class Game: self.date = datetime(start_date.year, start_date.month, start_date.day) self.game_stats = GameStats() self.game_stats.update(self) + self.planners = {} def _roll(self, prob, mult): if self.settings.version == "dev": @@ -310,6 +312,15 @@ class Game: # Update statistics self.game_stats.update(self) + # Plan flights for next turn + self.planners = {} + for cp in self.theater.controlpoints: + planner = FlightPlanner(cp, self) + planner.plan_flights() + self.planners[cp.id] = planner + print(planner) + + @property def current_turn_daytime(self): return ["dawn", "day", "dusk", "night"][self.turn % 4] diff --git a/game/operation/operation.py b/game/operation/operation.py index 1b82ded7..9edf33a6 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -135,10 +135,9 @@ class Operation: # Generate ground object first self.groundobjectgen.generate() - # air support + # Air Support (Tanker & Awacs) self.airsupportgen.generate(self.is_awacs_enabled) - # Generate Activity on the map for cp in self.game.theater.controlpoints: side = cp.captured @@ -157,20 +156,15 @@ class Operation: self.airgen.generate_dead_sead(cp, country) - for i, tanker_type in enumerate(self.airsupportgen.generated_tankers): - self.briefinggen.append_frequency("Tanker {} ({})".format(TANKER_CALLSIGNS[i], tanker_type), "{}X/{} MHz AM".format(97+i, 130+i)) - if self.is_awacs_enabled: - self.briefinggen.append_frequency("AWACS", "133 MHz AM") - # combined arms + #Setup combined arms parameters self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0 if self.game.player_country in [country.name for country in self.current_mission.coalition["blue"].countries.values()]: self.current_mission.groundControl.blue_tactical_commander = self.ca_slots else: self.current_mission.groundControl.red_tactical_commander = self.ca_slots - #self.extra_aagen.generate() # triggers if self.game.is_player_attack(self.conflict.attackers_country): @@ -192,18 +186,10 @@ class Operation: # options self.forcedoptionsgen.generate() - # main frequencies - self.briefinggen.append_frequency("Flight", "251 MHz AM") - if self.departure_cp.is_global or self.conflict.to_cp.is_global: - self.briefinggen.append_frequency("Carrier", "20X/ICLS CHAN1") - - # briefing - self.briefinggen.generate() - - # visuals + # Generate Visuals Smoke Effects self.visualgen.generate() - # Scripts + # Inject Lua Scripts load_mist = TriggerStart(comment="Load Mist Lua Framework") with open(os.path.abspath("./resources/scripts/mist_4_3_74.lua")) as f: load_mist.add_action(DoScript(String(f.read()))) @@ -219,5 +205,19 @@ class Operation: load_dcs_libe.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_dcs_libe) + # Briefing Generation + for i, tanker_type in enumerate(self.airsupportgen.generated_tankers): + self.briefinggen.append_frequency("Tanker {} ({})".format(TANKER_CALLSIGNS[i], tanker_type), "{}X/{} MHz AM".format(97+i, 130+i)) + + if self.is_awacs_enabled: + self.briefinggen.append_frequency("AWACS", "133 MHz AM") + + self.briefinggen.append_frequency("Flight", "251 MHz AM") + if self.departure_cp.is_global or self.conflict.to_cp.is_global: + self.briefinggen.append_frequency("Carrier", "20X/ICLS CHAN1") + + # Generate the briefing + self.briefinggen.generate() + diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py new file mode 100644 index 00000000..c3ca7cca --- /dev/null +++ b/gen/flights/ai_flight_planner.py @@ -0,0 +1,209 @@ +import math +import operator +import typing +import random + +from gen.flights.ai_flight_planner_db import INTERCEPT_CAPABLE, CAP_CAPABLE, CAS_CAPABLE +from gen.flights.flight import Flight, FlightType + + +# TODO : Ideally should be based on the aircraft type instead / Availability of fuel +STRIKE_MAX_RANGE = 30000 +SEAD_MAX_RANGE = 30000 + +MAX_NUMBER_OF_INTERCEPTION_GROUP = 3 +MISSION_DURATION = 120 # in minutes +CAP_EVERY_X_MINUTES = 20 + + +class FlightPlanner: + + from_cp = None + game = None + + interceptor_flights = [] + cap_flights = [] + cas_flights = [] + strike_flights = [] + sead_flights = [] + flights = [] + + def __init__(self, from_cp, game): + # TODO : have the flight planner depend on a 'stance' setting : [Defensive, Aggresive... etc] and faction doctrine + self.from_cp = from_cp + self.game = game + self.aircraft_inventory = {} # local copy of the airbase inventory + + def reset(self): + """ + Reset the planned flights and avaialble units + """ + self.aircraft_inventory = dict({k: v for k, v in self.from_cp.base.aircraft.items()}) + self.interceptor_flights = [] + self.cap_flights = [] + self.cas_flights = [] + self.strike_flights = [] + self.sead_flights = [] + self.flights = [] + + def plan_flights(self): + + self.reset() + + # The priority is to assign air-superiority fighter or interceptor to interception roles, so they can scramble if there is an attacker + self.commision_interceptors() + + # Then some CAP patrol for the next 2 hours + self.commision_barcap() + + # TODO : commision CAS / BAI + + # TODO : commision SEAD + + # TODO : commision STRIKE / ANTISHIP + + def commision_interceptors(self): + """ + Pick some aircraft to assign them to interception roles + """ + + # At least try to generate one interceptor group + number_of_interceptor_groups = min(max(sum([v for k, v in self.aircraft_inventory.items()]) / 4, MAX_NUMBER_OF_INTERCEPTION_GROUP), 1) + possible_interceptors = [k for k in self.aircraft_inventory.keys() if k in INTERCEPT_CAPABLE] + + if len(possible_interceptors) <= 0: + possible_interceptors = [k for k,v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2] + + if number_of_interceptor_groups > 0: + inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_interceptors}) + for i in range(number_of_interceptor_groups): + try: + unit = random.choice([k for k,v in inventory.items() if v >= 2]) + except IndexError: + break + inventory[unit] = inventory[unit] - 2 + flight = Flight(unit, 2, self.from_cp, FlightType.INTERCEPTION) + flight.points = [] + + self.interceptor_flights.append(flight) + self.flights.append(flight) + + # Update inventory + for k, v in inventory.items(): + self.aircraft_inventory[k] = v + + def commision_barcap(self): + """ + Pick some aircraft to assign them to defensive CAP roles (BARCAP) + """ + + possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2] + inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft}) + + offset = random.randint(0,5) + for i in range(int(MISSION_DURATION/CAP_EVERY_X_MINUTES)): + + try: + unit = random.choice([k for k, v in inventory.items() if v >= 2]) + except IndexError: + break + + inventory[unit] = inventory[unit] - 2 + flight = Flight(unit, 2, self.from_cp, FlightType.BARCAP) + + # Flight path : fly over each ground object (TODO : improve) + flight.points = [] + flight.scheduled_in = offset + i*random.randint(CAP_EVERY_X_MINUTES-5, CAP_EVERY_X_MINUTES+5) + + patrol_alt = random.randint(3600, 7000) + + patrolled = [] + for ground_object in self.from_cp.ground_objects: + if ground_object.group_id not in patrolled and not ground_object.airbase_group: + flight.points.append([ground_object.position.x, ground_object.position.y, patrol_alt]) + patrolled.append(ground_object.group_id) + + self.cap_flights.append(flight) + self.flights.append(flight) + + # Update inventory + for k, v in inventory.items(): + self.aircraft_inventory[k] = v + + def _get_strike_targets_in_range(self): + """ + @return a list of potential strike targets in range + """ + + # target, distance + potential_targets = [] + + for cp in [c for c in self.game.theater.controlpoints if c.captured != self.from_cp.captured]: + + # Compute distance to current cp + distance = math.hypot(cp.position.x - self.from_cp.position.x, + cp.position.y - self.from_cp.position.y) + + if distance > 2*STRIKE_MAX_RANGE: + # Then it's unlikely any child ground object is in range + return + + added_group = [] + for g in cp.ground_objects: + if g.group_id in added_group: continue + + # Compute distance to current cp + distance = math.hypot(cp.position.x - self.from_cp.position.x, + cp.position.y - self.from_cp.position.y) + + if distance < SEAD_MAX_RANGE: + potential_targets.append((g, distance)) + added_group.append(g) + + return potential_targets.sort(key=operator.itemgetter(1)) + + def _get_sead_targets_in_range(self): + """ + @return a list of potential sead targets in range + """ + + # target, distance + potential_targets = [] + + for cp in [c for c in self.game.theater.controlpoints if c.captured != self.from_cp.captured]: + + # Compute distance to current cp + distance = math.hypot(cp.position.x - self.from_cp.position.x, + cp.position.y - self.from_cp.position.y) + + # Then it's unlikely any ground object is range + if distance > 2*SEAD_MAX_RANGE: + return + + for g in cp.ground_objects: + + if g.dcs_identifier == "AA": + + # Check that there is at least one unit with a radar in the ground objects unit groups + number_of_units = sum([len([r for r in group.units if hasattr(r, "detection_range") and r.detection_range > 1000]) for group in g.groups]) + if number_of_units <= 0: + continue + + # Compute distance to current cp + distance = math.hypot(cp.position.x - self.from_cp.position.x, + cp.position.y - self.from_cp.position.y) + + if distance < SEAD_MAX_RANGE: + potential_targets.append((g, distance)) + + return potential_targets.sort(key=operator.itemgetter(1)) + + def __repr__(self): + return "-"*40 + "\n" + self.from_cp.name + " planned flights :\n"\ + + "-"*40 + "\n" + "\n".join([repr(f) for f in self.flights]) + "\n" + "-"*40 + + + + + + diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py new file mode 100644 index 00000000..f3754bd8 --- /dev/null +++ b/gen/flights/ai_flight_planner_db.py @@ -0,0 +1,177 @@ +from dcs.planes import * +from dcs.helicopters import * + +# Interceptor are the aircraft prioritized for interception tasks +# If none is available, the AI will use regular CAP-capable aircraft instead +INTERCEPT_CAPABLE = [ + MiG_21Bis, + MiG_25PD, + MiG_31, + + M_2000C, + Mirage_2000_5, + + F_14B, + F_15C, + +] + +# Used for CAP, Escort, and intercept if there is not a specialised aircraft available +CAP_CAPABLE = [ + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_29A, + MiG_29G, + MiG_29S, + + Su_27, + J_11A, + Su_30, + Su_33, + + M_2000C, + Mirage_2000_5, + + F_86F_Sabre, + F_4E, + F_5E_3, + F_14B, + F_15C, + F_16C_50, + FA_18C_hornet, + + C_101CC, + L_39ZA, + + P_51D_30_NA, + P_51D, + + SpitfireLFMkIXCW, + SpitfireLFMkIX, + + Bf_109K_4, + FW_190D9, + FW_190A8, +] + +# USed for CAS (Close air support) and BAI (Battlefield Interdiction) +CAS_CAPABLE = [ + + MiG_15bis, + MiG_29A, + MiG_27K, + MiG_29S, + + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_34, + + M_2000C, + + A_10A, + A_10C, + AV8BNA, + + F_86F_Sabre, + F_5E_3, + F_14B, + F_16C_50, + FA_18C_hornet, + + C_101CC, + L_39ZA, + AJS37, + + SA342M, + SA342L, + + AH_64A, + AH_64D, + + UH_1H, + + Mi_8MT, + Mi_28N, + Mi_24V, + Ka_50, + + P_51D_30_NA, + P_51D, + + SpitfireLFMkIXCW, + SpitfireLFMkIX, + + Bf_109K_4, + FW_190D9, + FW_190A8, +] + +# Aircraft used for SEAD / DEAD tasks +SEAD_CAPABLE = [ + F_4E, + FA_18C_hornet, + F_16C_50, + AV8BNA, + + Su_24M, + Su_25T, + Su_25TM, + Su_17M4, + Su_30, + Su_34, + MiG_27K, +] + +# Aircraft used for Strike mission +STRIKE_CAPABLE = [ + MiG_15bis, + MiG_29A, + MiG_27K, + MiG_29S, + + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_34, + + M_2000C, + + A_10A, + A_10C, + AV8BNA, + + F_86F_Sabre, + F_5E_3, + F_14B, + F_16C_50, + FA_18C_hornet, + + C_101CC, + L_39ZA, + AJS37, + + M_2000C, + + P_51D_30_NA, + P_51D, + + SpitfireLFMkIXCW, + SpitfireLFMkIX, + + Bf_109K_4, + FW_190D9, + FW_190A8, +] + +ANTISHIP_CAPABLE = [ + Su_24M, + F_A_18C, + AV8BNA, +] \ No newline at end of file diff --git a/gen/flights/flight.py b/gen/flights/flight.py new file mode 100644 index 00000000..d35e8d92 --- /dev/null +++ b/gen/flights/flight.py @@ -0,0 +1,56 @@ +from dcs.unittype import UnitType +from enum import Enum +from game import db + + +class FlightType(Enum): + CAP = 0 + TARCAP = 1 + BARCAP = 2 + CAS = 3 + INTERCEPTION = 4 + STRIKE = 5 + ANTISHIP = 6 + SEAD = 7 + DEAD = 8 + ESCORT = 9 + BAI = 10 + + # Helos + TROOP_TRANSPORT = 11 + LOGISTICS = 12 + EVAC = 13 + + +class Flight: + + unit_type: UnitType + from_cp = None + points = [] + type = "" + count = 0 + client_count = 0 + + # How long before this flight should take off + scheduled_in = 0 + + def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType): + self.unit_type = unit_type + self.count = count + self.from_cp = from_cp + self.flight_type = flight_type + + def __repr__(self): + return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type)\ + + " in " + str(self.scheduled_in) + " minutes (" + str(len(self.points)) + " wpt)" + +# Test +if __name__ == '__main__': + + from dcs.planes import A_10C + from theater import ControlPoint, Point + + from_cp = ControlPoint(0, "AA", Point(0,0), None, [], 0, 0) + f = Flight(A_10C, 4, from_cp, FlightType.CAS) + f.scheduled_in = 50 + print(f) \ No newline at end of file diff --git a/resources/normandy.gif b/resources/normandy.gif new file mode 100644 index 00000000..1bf03480 Binary files /dev/null and b/resources/normandy.gif differ diff --git a/theater/controlpoint.py b/theater/controlpoint.py index 25865e42..b17e8d53 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -1,8 +1,7 @@ -import typing import re +import typing from dcs.mapping import * -from dcs.country import * from dcs.terrain import Airport from .theatergroundobject import TheaterGroundObject