diff --git a/.idea/dcs_pmcliberation.iml b/.idea/dcs_pmcliberation.iml index 9eedabcf..1f377c84 100644 --- a/.idea/dcs_pmcliberation.iml +++ b/.idea/dcs_pmcliberation.iml @@ -4,7 +4,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index e524f659..65531ca9 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/__init__.py b/__init__.py index b92cad68..e34e6ab1 100755 --- a/__init__.py +++ b/__init__.py @@ -5,71 +5,87 @@ import os import gen import theater.caucasus -import game.mission +import game.operation +import ui.window +import ui.mainmenu + +from game.game import Game +from theater.controlpoint import * from dcs.planes import * from dcs.vehicles import * m = dcs.Mission() - theater = theater.caucasus.CaucasusTheater() - theater.kutaisi.base.aircraft = { - A_10C: 4, F_15C: 4, + A_10C: 2, } -theater.kutaisi.base.armor = { - Armor.MBT_M1A2_Abrams: 4, -} +g = Game(theater=theater) -theater.senaki.base.aircraft = { - MiG_21Bis: 8, -} +w = ui.window.Window() +m = ui.mainmenu.MainMenu(g, w) -theater.senaki.base.armor = { - Armor.MBT_T_55: 6, -} +w.run() -theater.senaki.base.aa = { - AirDefence.AAA_ZU_23_on_Ural_375: 2, -} -op = game.mission.InterceptOperation( - mission=m, - attacker=m.country("USA"), - defender=m.country("Russia"), - destination=theater.batumi, - destination_port=m.terrain.batumi(), - escort={Su_27: 2}, - transport={An_26B: 2}, - interceptors={M_2000C: 2} -) +""" +selected_cp = None # type: ControlPoint +while True: + ptr = 0 -op = game.mission.GroundInterceptOperation( - mission=m, - attacker=m.country("USA"), - defender=m.country("Russia"), - position=m.terrain.batumi().position, - target={Unarmed.Transport_ZIL_4331: 10}, - strikegroup={A_10C: 2} -) + print("Budget: {}m".format(g.budget)) -op = game.mission.CaptureOperation( - mission=m, - attacker=m.country("USA"), - defender=m.country("Russia"), - from_cp=theater.senaki, - to_cp=theater.batumi, - cas={A_10C: 2}, - escort={F_15C: 2}, - attack={Armor.MBT_M1A2_Abrams: 4}, - intercept={Su_27: 4}, - defense={Armor.MBT_T_55: 4}, - aa={AirDefence.AAA_ZU_23_Insurgent_on_Ural_375: 3}) -op.generate() + if selected_cp is None: + print("Events:") + for event in g.events: + ptr += 1 + print("{}. {} {}".format(ptr, event.attacker != g.side and "!" or " ", event)) + + print("Control Points:") + controlpoints = g.theater.controlpoints + controlpoints.sort(key=lambda x: x.captured) + for cp in g.theater.controlpoints: + ptr += 1 + print("{}. [{}{}] {}{}{}{}".format( + ptr, + cp.captured and "x" or " ", + int(cp.base.readiness * 10), + cp.name, + "^" * cp.base.total_planes, + "." * cp.base.total_armor, + "*" * cp.base.total_aa)) + + events_boundary = len(g.events) + try: + selected_idx = int(input(">").strip()) - 1 + except: + continue + + if selected_idx == -1: + g.pass_turn() + continue + if selected_idx < events_boundary: + event = g.events[selected_idx] + else: + selected_cp = controlpoints[selected_idx - events_boundary] + else: + print("Units on the base: ") + for unit, count in selected_cp.base.all_units: + print("{} ({}) ".format(unit.name and unit.name or unit.id, count), end="") + print("") + + try: + selected_idx = int(input(">").strip()) - 1 + except: + continue + if selected_idx == -1: + selected_cp = None if not os.path.exists("./build"): os.mkdir("./build") m.save("build/output.miz") +""" + diff --git a/game/event.py b/game/event.py index 113e3a9e..0eca4455 100644 --- a/game/event.py +++ b/game/event.py @@ -1,55 +1,185 @@ import typing +import random +import math import dcs from theater.controlpoint import * -from .mission import * +from userdata.debriefing_parser import * +from game.operation import * + +DIFFICULTY_LOG_BASE = 1.5 + class Event: silent = False - operation = None # type: Operation + operation = None # type: Operation + difficulty = 1 # type: int + BONUS_BASE = 0 - def failure(self): + def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint): + self.mission = dcs.mission.Mission() + self.attacker = self.mission.country(attacker_name) + self.defender = self.mission.country(defender_name) + self.to_cp = to_cp + self.from_cp = from_cp + + def bonus(self) -> int: + return math.ceil(math.log(self.difficulty, DIFFICULTY_LOG_BASE) * self.BONUS_BASE) + + def is_successfull(self, debriefing: Debriefing) -> bool: + return self.operation.is_successfull(debriefing) + + def commit(self, debriefing: Debriefing): + for country, losses in debriefing.destroyed_units.items(): + cp = None # type: ControlPoint + if country == self.attacker.name: + cp = self.from_cp + else: + cp = self.to_cp + + cp.base.commit_losses(losses) + + def skip(self): pass - def success(self): - pass + +class GroundInterceptEvent(Event): + BONUS_BASE = 3 + TARGET_AMOUNT_FACTOR = 3 + TARGET_VARIETY = 3 + + def __str__(self): + return "Ground intercept at {} ({})".format(self.to_cp, "*" * self.difficulty) + + def commit(self, debriefing: Debriefing): + super(GroundInterceptEvent, self).commit(debriefing) + + if self.from_cp.captured: + if self.is_successfull(debriefing): + self.to_cp.base.affect_strength(-0.1) + else: + self.to_cp.base.affect_strength(+0.1) + else: + assert False + + def skip(self): + if not self.to_cp.captured: + self.to_cp.base.affect_strength(+0.1) + else: + pass + + def player_attacking(self, position: Point, strikegroup: typing.Dict[PlaneType, int]): + suitable_unittypes = db.find_unittype(CAP, self.defender.name) + random.shuffle(suitable_unittypes) + unittypes = suitable_unittypes[:self.TARGET_VARIETY] + typecount = max(math.floor(self.difficulty * self.TARGET_AMOUNT_FACTOR), 1) + targets = {unittype: typecount for unittype in unittypes} + + self.operation = GroundInterceptOperation(mission=self.mission, + attacker=self.attacker, + defender=self.defender, + position=position, + target=targets, + strikegroup=strikegroup) + class InterceptEvent(Event): - pass + ESCORT_AMOUNT_FACTOR = 2 + BONUS_BASE = 5 + + def __str__(self): + return "Intercept at {} ({})".format(self.to_cp, "*" * self.difficulty) + + def commit(self, debriefing: Debriefing): + super(InterceptEvent, self).commit(debriefing) + if self.is_successfull(debriefing): + self.to_cp.base.affect_strength(0.1 * self.from_cp.captured and -1 or 1) + else: + self.to_cp.base.affect_strength(0.1 * self.from_cp.captured and 1 or -1) + + def skip(self): + if self.to_cp.captured: + self.to_cp.base.affect_strength(-0.2) + + def player_attacking(self, interceptors: typing.Dict[PlaneType, int]): + escort = self.to_cp.base.scramble_sweep(self.to_cp) + transport_unit = random.choice(db.find_unittype(Transport, self.defender.name)) + assert transport_unit is not None + + self.operation = InterceptOperation(mission=self.mission, + attacker=self.attacker, + defender=self.defender, + destination=self.to_cp, + destination_port=self.to_cp.airport, + escort=escort, + transport={transport_unit: 1}, + interceptors=interceptors) + + def player_defending(self, escort: typing.Dict[PlaneType, int]): + interceptors = self.from_cp.base.scramble_interceptors_count(self.difficulty * self.ESCORT_AMOUNT_FACTOR) + transport_unit = random.choice(db.find_unittype(Transport, self.defender.name)) + assert transport_unit is not None + + self.operation = InterceptOperation(mission=self.mission, + attacker=self.attacker, + defender=self.defender, + destination=self.to_cp, + destination_port=self.to_cp.airport, + escort=escort, + transport={transport_unit: 1}, + interceptors=interceptors) + class CaptureEvent(Event): silent = True + BONUS_BASE = 7 - def __init__(self, from_cp: ControlPoint, to_cp: ControlPoint): - pass + def __str__(self): + return "Capture {} ({})".format(self.to_cp, "*" * self.difficulty) - def player_defending(self, from_cp: ControlPoint, to_cp: ControlPoint, interceptors: typing.Dict[PlaneType, int]): - assert not self.operation + def commit(self, debriefing: Debriefing): + super(CaptureEvent, self).commit(debriefing) + if self.is_successfull(debriefing): + if self.from_cp.captured: + self.to_cp.captured = True + else: + if not self.from_cp.captured: + self.to_cp.captured = False + self.to_cp.base.affect_strength(+0.5) - cas = from_cp.base.scramble_cas(to_cp) - escort = from_cp.base.scramble_sweep(to_cp) - attackers = from_cp.base.assemble_cap(to_cp) + def skip(self): + if self.to_cp.captured: + self.to_cp.captured = False - self.operation = CaptureOperation(from_cp=from_cp, - to_cp=to_cp, + def player_defending(self, interceptors: typing.Dict[PlaneType, int]): + cas = self.from_cp.base.scramble_cas(self.to_cp) + escort = self.from_cp.base.scramble_sweep(self.to_cp) + attackers = self.from_cp.base.assemble_cap(self.to_cp) + + self.operation = CaptureOperation(mission=self.mission, + attacker=self.attacker, + defender=self.defender, + from_cp=self.from_cp, + to_cp=self.to_cp, cas=cas, escort=escort, attack=attackers, intercept=interceptors, - defense=to_cp.base.armor, - aa=to_cp.base.aa) + defense=self.to_cp.base.armor, + aa=self.to_cp.base.aa) - def player_attacking(self, from_cp: ControlPoint, to_cp: ControlPoint, cas: typing.Dict[PlaneType, int], escort: typing.Dict[PlaneType, int], armor: typing.Dict[Armor, int]): - assert not self.operation + def player_attacking(self, cas: typing.Dict[PlaneType, int], escort: typing.Dict[PlaneType, int], armor: typing.Dict[Armor, int]): + interceptors = self.to_cp.base.scramble_sweep(for_target=self.to_cp) - interceptors = to_cp.base.scramble_sweep() - - self.operation = CaptureOperation(from_cp=from_cp, - to_cp=to_cp, + self.operation = CaptureOperation(mission=self.mission, + attacker=self.attacker, + defender=self.defender, + from_cp=self.from_cp, + to_cp=self.to_cp, cas=cas, escort=escort, attack=armor, intercept=interceptors, - defense=to_cp.base.armor, - aa=to_cp.base.aa) \ No newline at end of file + defense=self.to_cp.base.armor, + aa=self.to_cp.base.aa) \ No newline at end of file diff --git a/game/event_results.py b/game/event_results.py new file mode 100644 index 00000000..55a78829 --- /dev/null +++ b/game/event_results.py @@ -0,0 +1,8 @@ +import typing +import dcs + +from game.event import * + + + + diff --git a/game/game.py b/game/game.py index b0a74e78..fec7c3c4 100644 --- a/game/game.py +++ b/game/game.py @@ -1,21 +1,135 @@ import typing +import random from theater.conflicttheater import * from theater.controlpoint import * -from .event import * +from userdata.debriefing_parser import * +from game.event import * + +COMMISION_LIMITS_SCALE = 2 +COMMISION_LIMITS_FACTORS = { + CAP: 2, + CAS: 1, + FighterSweep: 3, + AirDefence: 2, +} + +COMMISION_AMOUNTS_SCALE = 2 +COMMISION_AMOUNTS_FACTORS = { + CAP: 0.6, + CAS: 0.3, + FighterSweep: 0.5, + AirDefence: 0.3, +} + + +ENEMY_INTERCEPT_PROBABILITY_BASE = 25 +ENEMY_CAPTURE_PROBABILITY_BASE = 15 + +PLAYER_INTERCEPT_PROBABILITY_BASE = 30 +PLAYER_GROUNDINTERCEPT_PROBABILITY_BASE = 30 + +PLAYER_BUDGET_BASE = 25 +PLAYER_BUDGET_IMPORTANCE_LOG = 2 + class Game: - events = [] # type: typing.List[Event] + budget = 45 + events = None # type: typing.List[Event] def __init__(self, theater: ConflictTheater): + self.events = [] self.theater = theater + self.player = "USA" + self.enemy = "Russia" + + def _roll(self, prob, mult): + return random.randint(0, 100) <= prob * mult def _fill_cap_events(self): - for cp in [x for x in self.theater.controlpoints if x.captured]: - for connected_cp in [x for x in cp.connected_points if not x.captured]: - self.events.append(CaptureEvent(cp, connected_cp)) + for from_cp, to_cp in self.theater.conflicts(True): + self.events.append(CaptureEvent(attacker_name=self.player, + defender_name=self.enemy, + from_cp=from_cp, + to_cp=to_cp)) + + def _generate_enemy_caps(self): + for from_cp, to_cp in self.theater.conflicts(False): + if self._roll(ENEMY_CAPTURE_PROBABILITY_BASE, from_cp.base.strength): + self.events.append(CaptureEvent(attacker_name=self.enemy, + defender_name=self.player, + from_cp=from_cp, + to_cp=to_cp)) + break + + def _generate_interceptions(self): + for from_cp, to_cp in self.theater.conflicts(False): + if self._roll(ENEMY_INTERCEPT_PROBABILITY_BASE, from_cp.base.strength): + self.events.append(InterceptEvent(attacker_name=self.enemy, + defender_name=self.player, + from_cp=from_cp, + to_cp=to_cp)) + break + + for from_cp, to_cp in self.theater.conflicts(True): + if self._roll(PLAYER_INTERCEPT_PROBABILITY_BASE, from_cp.base.strength): + self.events.append(InterceptEvent(attacker_name=self.player, + defender_name=self.enemy, + from_cp=from_cp, + to_cp=to_cp)) + break + + def _generate_groundinterceptions(self): + for from_cp, to_cp in self.theater.conflicts(True): + if self._roll(PLAYER_GROUNDINTERCEPT_PROBABILITY_BASE, from_cp.base.strength): + self.events.append(GroundInterceptEvent(attacker_name=self.player, + defender_name=self.enemy, + from_cp=from_cp, + to_cp=to_cp)) + break + + def _commision_units(self, cp: ControlPoint): + for for_task in [CAP, CAS, FighterSweep, AirDefence]: + limit = COMMISION_LIMITS_FACTORS[for_task] * math.pow(cp.importance, COMMISION_LIMITS_SCALE) + missing_units = limit - cp.base.total_units(for_task) + if missing_units > 0: + awarded_points = COMMISION_AMOUNTS_FACTORS[for_task] * math.pow(cp.importance, COMMISION_AMOUNTS_SCALE) + points_to_spend = cp.base.append_commision_points(for_task, awarded_points) + if points_to_spend > 0: + unit_type = random.choice(db.find_unittype(for_task, self.enemy)) + cp.base.commision_units({unit_type: points_to_spend}) + + def _budget_player(self): + total_importance = sum([x.importance for x in self.theater.player_points()]) + total_strength = sum([x.base.strength for x in self.theater.player_points()]) / len(self.theater.player_points()) + + self.budget += math.ceil(math.log(total_importance * total_strength + 1, PLAYER_BUDGET_IMPORTANCE_LOG) * PLAYER_BUDGET_BASE) + + def initiate_event(self, event: Event): + event.operation.generate() + event.mission.save("build/next_mission.miz") + + def finish_event(self, event: Event, debriefing: Debriefing): + event.commit(debriefing) + if event.is_successfull(debriefing): + self.budget += event.bonus() + + self.events.remove(event) + + def is_player_attack(self, event: Event): + return event.attacker.name == self.player def pass_turn(self): - self.events = [] # type: typing.List[Event] - self._fill_cap_events() + for event in self.events: + event.skip() + + self._budget_player() + for cp in self.theater.enemy_bases(): + self._commision_units(cp) + + self.events = [] # type: typing.List[Event] + self._fill_cap_events() + self._generate_enemy_caps() + self._generate_interceptions() + self._generate_groundinterceptions() diff --git a/game/mission.py b/game/operation.py similarity index 93% rename from game/mission.py rename to game/operation.py index 531e4fd3..c311c1e4 100644 --- a/game/mission.py +++ b/game/operation.py @@ -1,6 +1,7 @@ import typing from globals import * +from userdata.debriefing_parser import * from dcs.mission import * from dcs.unitgroup import * from dcs.vehicles import * @@ -22,6 +23,15 @@ class Operation: self.airgen = AircraftConflictGenerator(self.mission, self.conflict) self.aagen = AAConflictGenerator(self.mission, self.conflict) + def units_of(self, country_name: str) -> typing.Collection[UnitType]: + return [] + + def is_successfull(self, debriefing: Debriefing) -> bool: + return True + + def generate(self): + pass + class CaptureOperation(Operation): def __init__(self, diff --git a/gen/aircraft.py b/gen/aircraft.py index db7d498e..b1fdbe65 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -35,6 +35,7 @@ class AircraftConflictGenerator: def __init__(self, mission: Mission, conflict: Conflict): self.m = mission self.conflict = conflict + self.escort_targets = [] def _group_point(self, point) -> Point: distance = randint( @@ -53,6 +54,8 @@ class AircraftConflictGenerator: airport: Airport = None) -> PlaneGroup: starttype = airport == None and StartType.Warm or StartType.Cold print("generating {} ({}) at {} {}".format(unit, count, at, airport, side)) + assert count > 0 + return self.m.flight_group( country=side, name=name, @@ -66,7 +69,8 @@ class AircraftConflictGenerator: group_size=count) def _generate_escort(self, units: typing.Dict[PlaneType, int], airport: Airport, side: Country, location: Point): - assert len(self.escort_targets) > 0 + if len(self.escort_targets) == 0: + return for type, count in units.items(): group = self._generate_group( @@ -81,7 +85,7 @@ class AircraftConflictGenerator: group.load_task_default_loadout(dcs.task.Escort) heading = group.position.heading_between_point(self.conflict.position) - position = group.position # type: Point + position = group.position # type: Point wayp = group.add_waypoint(position.point_from_heading(heading, WORKAROUND_WAYP_DIST), CAS_ALTITUDE) for group in self.escort_targets: diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 070a9cfb..797f1d83 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -25,6 +25,7 @@ INTERCEPT_DEFENDERS_HEADING = -10, 10 INTERCEPT_ATTACKERS_DISTANCE = 60000 INTERCEPT_DEFENDERS_DISTANCE = 30000 + class Conflict: attackers_side = None # type: Country defenders_side = None # type: Country diff --git a/resources/caumap.gif b/resources/caumap.gif new file mode 100644 index 00000000..f3d388f1 Binary files /dev/null and b/resources/caumap.gif differ diff --git a/shop/db.py b/shop/db.py index fc2771a5..46a4bd18 100644 --- a/shop/db.py +++ b/shop/db.py @@ -13,6 +13,7 @@ PRICES = { # planes Su_25T: 10, + Su_25: 10, A_10A: 15, A_10C: 20, @@ -23,6 +24,10 @@ PRICES = { MiG_15bis: 10, MiG_21Bis: 13, + MiG_29A: 23, + + IL_76MD: 20, + S_3B_Tanker: 20, # armor @@ -40,5 +45,35 @@ PRICES = { UNIT_BY_TASK = { FighterSweep: [Su_27, Su_33, Su_25, F_15C, MiG_15bis, MiG_21Bis, MiG_29A, ], CAS: [Su_25T, A_10A, A_10C, ], - CAP: [Armor.MBT_T_90, Armor.MBT_T_80U, Armor.MBT_T_55, Armor.MBT_M1A2_Abrams, Armor.MBT_M60A3_Patton, Armor.ATGM_M1134_Stryker, Armor.APC_BTR_80, ] + CAP: [Armor.MBT_T_90, Armor.MBT_T_80U, Armor.MBT_T_55, Armor.MBT_M1A2_Abrams, Armor.MBT_M60A3_Patton, Armor.ATGM_M1134_Stryker, Armor.APC_BTR_80, ], + AirDefence: [AirDefence.AAA_ZU_23_on_Ural_375, ], + Transport: [IL_76MD, S_3B_Tanker, ], } + +UNIT_BY_COUNTRY = { + "Russia": [Su_25T, A_10C, Su_27, Su_33, Su_25, MiG_15bis, MiG_21Bis, MiG_29A, AirDefence.AAA_ZU_23_on_Ural_375, Armor.APC_BTR_80, Armor.MBT_T_90, Armor.MBT_T_80U, Armor.MBT_T_55, IL_76MD, ], + "USA": [F_15C, A_10C, Armor.MBT_M1A2_Abrams, Armor.MBT_M60A3_Patton, Armor.ATGM_M1134_Stryker, S_3B_Tanker], +} + + +def unit_task(unit: UnitType) -> Task: + for task, units in UNIT_BY_TASK.items(): + if unit in units: + return task + + assert False + + +def find_unittype(for_task: Task, country_name: str) -> typing.List[UnitType]: + return [x for x in UNIT_BY_TASK[for_task] if x in UNIT_BY_COUNTRY[country_name]] + + +def unit_type_name(unit_type) -> str: + return unit_type.id and unit_type.id or unit_type.name + + +def task_name(task) -> str: + if task == AirDefence: + return "AirDefence" + else: + return task.name diff --git a/theater/base.py b/theater/base.py index cfb2cba9..8a7011a7 100644 --- a/theater/base.py +++ b/theater/base.py @@ -1,6 +1,7 @@ import typing import dcs import math +import itertools from shop import db from theater.controlpoint import ControlPoint @@ -17,10 +18,15 @@ ARMOR_IMPORTANCE_FACTOR = 4 class Base: aircraft = {} # type: typing.Dict[PlaneType, int] armor = {} # type: typing.Dict[Armor, int] - aa = {} # type: typing.Dict[AirDefence, int] + aa = {} # type: typing.Dict[AirDefence, int] + strength = 1 # type: float + commision_points = {} def __init__(self): - pass + self.aircraft = {} + self.armor = {} + self.aa = {} + self.commision_points = {} @property def total_planes(self) -> int: @@ -30,19 +36,40 @@ class Base: 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: - sorted_planes = [key for key in dict.keys() if key in db.UNIT_BY_TASK[for_type]] - sorted_planes.sort(key=lambda x: db.PRICES[x], reverse=True) + assert count > 0 + + 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 plane in sorted_planes: - existing_count = dict[plane] # type: int + 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 - result[plane] = result.get(plane, 0) + result_unit_count + + assert result_unit_count > 0 + result[unit_type] = result.get(unit_type, 0) + result_unit_count return result @@ -65,24 +92,68 @@ class Base: total_scrambled += PLANES_IN_GROUP yield PLANES_IN_GROUP and total_scrambled < total_planes or total_planes - total_scrambled - def commit_scramble(self, scrambled_aircraft: typing.Dict[PlaneType, int]): - for k, c in scrambled_aircraft: - self.aircraft[k] -= c - assert self.aircraft[k] >= 0 - if self.aircraft[k] == 0: - del self.aircraft[k] + 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 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 == FighterSweep: + target_dict = self.aircraft + elif for_task == CAP: + 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]): + for unit_type, count in units_lost.items(): + aircraft_key = next((x for x in self.aircraft.keys() if x.id == unit_type), None) + if aircraft_key: + self.aircraft[aircraft_key] = self.aircraft[aircraft_key] - count + + armor_key = next((x for x in self.armor.keys() if x.name == unit_type), None) + if armor_key: + self.armor[armor_key] = self.armor[armor_key] - count + + aa_key = next((x for x in self.aa.keys() if x.name == unit_type), None) + if aa_key: + self.aa[aa_key] = self.aa[aa_key] - count + + def affect_strength(self, amount): + self.strength += amount + if self.strength > 1: + self.strength = 1 def scramble_cas(self, for_target: ControlPoint) -> typing.Dict[PlaneType, int]: - return self._find_best_planes(CAS, int(for_target.importance * PLANES_IMPORTANCE_FACTOR)) + return self._find_best_planes(CAS, math.ceil(for_target.importance * PLANES_IMPORTANCE_FACTOR * self.strength)) def scramble_sweep(self, for_target: ControlPoint) -> typing.Dict[PlaneType, int]: - return self._find_best_planes(FighterSweep, int(for_target.importance * PLANES_IMPORTANCE_FACTOR)) + return self._find_best_planes(FighterSweep, math.ceil(for_target.importance * PLANES_IMPORTANCE_FACTOR * self.strength)) def scramble_interceptors(self, factor: float) -> typing.Dict[PlaneType, int]: - return self._find_best_planes(FighterSweep, int(self.total_planes * factor)) + return self._find_best_planes(FighterSweep, math.ceil(self.total_planes * factor * self.strength)) + + def scramble_interceptors_count(self, count: int) -> typing.Dict[PlaneType, int]: + assert count > 0 + return self._find_best_planes(FighterSweep, count) def assemble_cap(self, for_target: ControlPoint) -> typing.Dict[Armor, int]: - return self._find_best_armor(CAP, int(for_target.importance * ARMOR_IMPORTANCE_FACTOR)) + return self._find_best_armor(CAP, math.ceil(for_target.importance * ARMOR_IMPORTANCE_FACTOR * self.strength)) def assemble_defense(self, factor: float) -> typing.Dict[Armor, int]: - return self._find_best_armor(CAP, int(self.total_armor * factor)) + return self._find_best_armor(CAP, math.ceil(self.total_armor * factor * self.strength)) diff --git a/theater/caucasus.py b/theater/caucasus.py index cd19baab..08d31105 100644 --- a/theater/caucasus.py +++ b/theater/caucasus.py @@ -4,20 +4,22 @@ from .conflicttheater import * from .base import * class CaucasusTheater(ConflictTheater): - kutaisi = ControlPoint(caucasus.Kutaisi.position, ALL_RADIALS, SIZE_SMALL, IMPORTANCE_LOW) - senaki = ControlPoint(caucasus.Senaki.position, ALL_RADIALS, SIZE_REGULAR, IMPORTANCE_LOW) - kobuleti = ControlPoint(caucasus.Kobuleti.position, COAST_VERTICAL, SIZE_SMALL, IMPORTANCE_LOW) - batumi = ControlPoint(caucasus.Batumi.position, COAST_VERTICAL, SIZE_SMALL, IMPORTANCE_MEDIUM) - sukhumi = ControlPoint(caucasus.Sukhumi.position, COAST_VERTICAL, SIZE_REGULAR, IMPORTANCE_MEDIUM) - gudauta = ControlPoint(caucasus.Gudauta.position, COAST_VERTICAL, SIZE_REGULAR, IMPORTANCE_MEDIUM) - sochi = ControlPoint(caucasus.Sochi.position, COAST_VERTICAL, SIZE_BIG, IMPORTANCE_HIGH) + kutaisi = ControlPoint(caucasus.Kutaisi, ALL_RADIALS, SIZE_SMALL, IMPORTANCE_LOW) + senaki = ControlPoint(caucasus.Senaki, ALL_RADIALS, SIZE_REGULAR, IMPORTANCE_LOW) + kobuleti = ControlPoint(caucasus.Kobuleti, COAST_VERTICAL, SIZE_SMALL, IMPORTANCE_LOW) + batumi = ControlPoint(caucasus.Batumi, COAST_VERTICAL, SIZE_SMALL, IMPORTANCE_MEDIUM) + sukhumi = ControlPoint(caucasus.Sukhumi, COAST_VERTICAL, SIZE_REGULAR, IMPORTANCE_MEDIUM) + gudauta = ControlPoint(caucasus.Gudauta, COAST_VERTICAL, SIZE_REGULAR, IMPORTANCE_MEDIUM) + sochi = ControlPoint(caucasus.Sochi, COAST_VERTICAL, SIZE_BIG, IMPORTANCE_HIGH) def __init__(self): - self.add_controlpoint(self.kutaisi, connected_to=[self.senaki]) - self.add_controlpoint(self.senaki, connected_to=[self.kobuleti, self.sukhumi]) - self.add_controlpoint(self.kobuleti, connected_to=[self.batumi]) - self.add_controlpoint(self.batumi) + self.kutaisi.captured = True - self.add_controlpoint(self.sukhumi, connected_to=[self.gudauta]) - self.add_controlpoint(self.gudauta, connected_to=[self.sochi]) - self.add_controlpoint(self.sochi) + self.add_controlpoint(self.kutaisi, connected_to=[self.senaki]) + self.add_controlpoint(self.senaki, connected_to=[self.kobuleti, self.sukhumi, self.kutaisi]) + self.add_controlpoint(self.kobuleti, connected_to=[self.batumi, self.senaki]) + self.add_controlpoint(self.batumi, connected_to=[self.kobuleti]) + + self.add_controlpoint(self.sukhumi, connected_to=[self.gudauta, self.senaki]) + self.add_controlpoint(self.gudauta, connected_to=[self.sochi, self.sukhumi]) + self.add_controlpoint(self.sochi, connected_to=[self.gudauta]) diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 3bc2be79..b82abda4 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -1,4 +1,6 @@ import typing +import itertools + import dcs from .controlpoint import * @@ -9,8 +11,8 @@ SIZE_BIG = 2000 SIZE_LARGE = 3000 IMPORTANCE_LOW = 1 -IMPORTANCE_MEDIUM = 2 -IMPORTANCE_HIGH = 3 +IMPORTANCE_MEDIUM = 1.2 +IMPORTANCE_HIGH = 1.4 ALL_RADIALS = [0, 45, 90, 135, 180, 225, 270, 315, ] COAST_VERTICAL = [45, 90, 135, ] @@ -20,11 +22,22 @@ COAST_HORIZONTAL = [315, 0, 45, ] class ConflictTheater: controlpoints = [] # type: typing.List[ControlPoint] + def __init__(self): + self.controlpoints = [] + def add_controlpoint(self, point: ControlPoint, connected_to: typing.Collection[ControlPoint] = []): for connected_point in connected_to: point.connect(to=connected_point) self.controlpoints.append(point) - def player_bases(self) -> typing.Collection[ControlPoint]: - return [point for point in self.controlpoints if point.captured and point.base] + def player_points(self) -> typing.Collection[ControlPoint]: + return [point for point in self.controlpoints if point.captured] + + def conflicts(self, from_player=True) -> typing.Collection[typing.Tuple[ControlPoint, ControlPoint]]: + for cp in [x for x in self.controlpoints if x.captured == from_player]: + for connected_point in [x for x in cp.connected_points if x.captured != from_player]: + yield (cp, connected_point) + + def enemy_bases(self) -> typing.Collection[ControlPoint]: + return [point for point in self.controlpoints if not point.captured] diff --git a/theater/controlpoint.py b/theater/controlpoint.py index b4bd709e..e5446497 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -7,26 +7,36 @@ from dcs.country import * from gen.conflictgen import * -class ControlPoint: - connected_points = [] # type: typing.List[ControlPoint] - position = None # type: Point - captured = False - strength = 100 - base: None # type: theater.base.Base - def __init__(self, point: Point, radials: typing.Collection[int], size: int, importance: int): +class ControlPoint: + connected_points = [] # type: typing.List[ControlPoint] + position = None # type: Point + captured = False + base: None # type: theater.base.Base + airport: None # type: Airport + + def __init__(self, airport: Airport, radials: typing.Collection[int], size: int, importance: int): import theater.base - self.position = point + self.name = airport.name + self.position = airport.position + self.airport = airport self.size = size self.importance = importance self.captured = False self.radials = radials + self.connected_points = [] self.base = theater.base.Base() + def __str__(self): + return self.name + def connect(self, to): self.connected_points.append(to) + def is_connected(self, to) -> bool: + return to in self.connected_points + def find_radial(self, heading: int, ignored_radial: int = None): closest_radial = 0 closest_radial_delta = 360 @@ -39,7 +49,7 @@ class ControlPoint: return closest_radial def conflict_attack(self, from_cp, attacker: Country, defender: Country) -> Conflict: - cp = from_cp # type: ControlPoint + cp = from_cp # type: ControlPoint attack_radial = self.find_radial(cp.position.heading_between_point(self.position)) defense_radial = self.find_radial(self.position.heading_between_point(cp.position), ignored_radial=attack_radial) diff --git a/theater/start_generator.py b/theater/start_generator.py new file mode 100644 index 00000000..22cc9779 --- /dev/null +++ b/theater/start_generator.py @@ -0,0 +1,11 @@ +import typing +import random + +import dcs + +from theater.controlpoint import * +from theater.base import * +from theater.conflicttheater import * + +def generate_initial(theater: ConflictTheater): + pass diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/basemenu.py b/ui/basemenu.py new file mode 100644 index 00000000..88e96687 --- /dev/null +++ b/ui/basemenu.py @@ -0,0 +1,62 @@ +from shop import db + +from tkinter import * +from ui.window import * +from ui.eventmenu import * + +from game.game import * + + +class BaseMenu: + def __init__(self, window: Window, parent, game: Game, base: Base): + self.window = window + self.frame = window.right_pane + self.parent = parent + self.game = game + self.base = base + + self.update() + + def go_back(self): + self.parent.update() + + def buy(self, unit_type): + def action(): + price = db.PRICES[unit_type] + if self.game.budget > price: + self.base.commision_units({unit_type: 1}) + self.game.budget -= price + + self.update() + + return action + + def update(self): + self.window.clear_right_pane() + row = 0 + + def purchase_row(unit_type, unit_price): + nonlocal row + + existing_units = self.base.total_units_of_type(unit_type) + Label(self.frame, text=db.unit_type_name(unit_type)).grid(column=0, row=row, sticky=W) + Label(self.frame, text="{}m {}".format(unit_price, existing_units)).grid(column=1, row=row) + Button(self.frame, text="Buy", command=self.buy(unit_type)).grid(column=2, row=row) + row += 1 + + units = { + CAP: db.find_unittype(CAP, self.game.player), + CAS: db.find_unittype(CAS, self.game.player), + FighterSweep: db.find_unittype(FighterSweep, self.game.player), + AirDefence: db.find_unittype(AirDefence, self.game.player), + } + + Label(self.frame, text="Budget: {}m".format(self.game.budget)).grid(column=0, row=row, sticky=W) + Button(self.frame, text="Back", command=self.go_back).grid(column=2, row=row) + row += 1 + + for task_type, units in units.items(): + Label(self.frame, text="{}".format(db.task_name(task_type))).grid(column=0, row=row, columnspan=3); row += 1 + for unit_type in units: + purchase_row(unit_type, db.PRICES[unit_type]) + diff --git a/ui/eventmenu.py b/ui/eventmenu.py new file mode 100644 index 00000000..638d5d78 --- /dev/null +++ b/ui/eventmenu.py @@ -0,0 +1,98 @@ +from tkinter import * +from ui.window import * +from ui.eventresultsmenu import * + +from game.game import * +from game import event + + +class EventMenu: + aircraft_scramble_entries = None # type: typing.Dict[PlaneType, Entry] + armor_scramble_entries = None # type: typing.Dict[Armor, Entry] + + def __init__(self, window: Window, parent, game: Game, event: event.Event): + self.window = window + self.frame = self.window.right_pane + self.parent = parent + + self.event = event + self.game = game + + self.aircraft_scramble_entries = {} + self.armor_scramble_entries = {} + + self.update() + + def start(self): + scrambled_aircraft = {} + scrambled_sweep = {} + scrambled_cas = {} + for unit_type, field in self.aircraft_scramble_entries.items(): + value = field.get() + if value and int(value) > 0: + amount = int(value) + task = db.unit_task(unit_type) + + scrambled_aircraft[unit_type] = amount + if task == CAS: + scrambled_cas[unit_type] = amount + elif task == FighterSweep: + scrambled_sweep[unit_type] = amount + + scrambled_armor = {} + for unit_type, field in self.armor_scramble_entries.items(): + value = field.get() + if value and int(value) > 0: + scrambled_armor[unit_type] = int(value) + + if type(self.event) is CaptureEvent: + e = self.event # type: CaptureEvent + if self.game.is_player_attack(self.event): + e.player_attacking(cas=scrambled_cas, + escort=scrambled_sweep, + armor=scrambled_armor) + else: + e.player_defending(interceptors=scrambled_aircraft) + elif type(self.event) is InterceptEvent: + e = self.event # type: InterceptEvent + if self.game.is_player_attack(self.event): + e.player_attacking(interceptors=scrambled_aircraft) + else: + e.player_defending(escort=scrambled_aircraft) + elif type(self.event) is GroundInterceptEvent: + e = self.event # type: GroundInterceptEvent + e.player_attacking(e.to_cp.position.random_point_within(30000), strikegroup=scrambled_aircraft) + + self.game.initiate_event(self.event) + EventResultsMenu(self.window, self.parent, self.game, self.event) + + def update(self): + self.window.clear_right_pane() + row = 0 + + def label(text): + nonlocal row + Label(self.frame, text=text).grid(column=0, row=0) + + row += 1 + + def scrable_row(unit_type, unit_count): + nonlocal row + Label(self.frame, text="{} ({})".format(unit_type.id and unit_type.id or unit_type.name, unit_count)).grid(column=0, row=row) + e = Entry(self.frame) + e.grid(column=1, row=row) + + self.aircraft_scramble_entries[unit_type] = e + row += 1 + + base = None # type: Base + if self.event.attacker.name == self.game.player: + base = self.event.from_cp.base + else: + base = self.event.to_cp.base + + label("Aircraft") + for unit, count in base.aircraft.items(): + scrable_row(unit, count) + + Button(self.frame, text="Commit", command=self.start).grid(column=0, row=row) diff --git a/ui/eventresultsmenu.py b/ui/eventresultsmenu.py new file mode 100644 index 00000000..95e6785c --- /dev/null +++ b/ui/eventresultsmenu.py @@ -0,0 +1,61 @@ +import math + +from tkinter import * +from ui.window import * + +from userdata.debriefing_parser import * +from game.game import * +from game import event + + +class EventResultsMenu: + def __init__(self, window: Window, parent, game: Game, event: Event): + self.window = window + self.frame = window.right_pane + self.parent = parent + + self.game = game + self.event = event + + self.update() + + def simulate_result(self, player_factor: float, enemy_factor: float, result: bool): + def action(): + debriefing = Debriefing() + + def count_planes(groups: typing.List[FlyingGroup], mult: float) -> typing.Dict[UnitType, int]: + result = {} + for group in groups: + for unit in group.units: + result[unit.type] = result.get(unit.type, 0) + 1 * mult + + return {x: math.floor(y) for x, y in result.items()} + + player_planes = self.event.operation.mission.country(self.game.player).plane_group + enemy_planes = self.event.operation.mission.country(self.game.enemy).plane_group + + player_losses = count_planes(player_planes, player_factor) + enemy_losses = count_planes(enemy_planes, enemy_factor) + + debriefing.destroyed_units = { + self.game.player: player_losses, + self.game.enemy: enemy_losses, + } + + self.game.finish_event(self.event, debriefing) + self.game.pass_turn() + self.parent.update() + + return action + + def update(self): + self.window.clear_right_pane() + + Button(self.frame, text="no losses, succ", command=self.simulate_result(0, 1, True)).grid(row=0, column=0) + Button(self.frame, text="no losses, fail", command=self.simulate_result(0, 1, False)).grid(row=0, column=1) + + Button(self.frame, text="half losses, succ", command=self.simulate_result(0.5, 0.5, True)).grid(row=1, column=0) + Button(self.frame, text="half losses, fail", command=self.simulate_result(0.5, 0.5, False)).grid(row=1, column=1) + + Button(self.frame, text="full losses, succ", command=self.simulate_result(1, 0, True)).grid(row=2, column=0) + Button(self.frame, text="full losses, fail", command=self.simulate_result(1, 0, False)).grid(row=2, column=1) diff --git a/ui/mainmenu.py b/ui/mainmenu.py new file mode 100644 index 00000000..708e5ee8 --- /dev/null +++ b/ui/mainmenu.py @@ -0,0 +1,78 @@ +from tkinter import * +from tkinter.ttk import * + +from ui.window import * +from ui.eventmenu import * +from ui.basemenu import * + +from game.game import * + +class MainMenu: + def __init__(self, game: Game, window: Window): + self.image = PhotoImage(file="resources/caumap.gif") + self.game = game + self.window = window + + map = Label(window.left_pane, image=self.image) + map.grid(column=0, row=0) + + self.frame = self.window.right_pane + self.frame.grid_columnconfigure(0, weight=1) + self.update() + + def pass_turn(self): + self.game.pass_turn() + self.update() + + def start_event(self, event) -> typing.Callable: + return lambda: EventMenu(self.window, self, self.game, event) + + def go_cp(self, cp: ControlPoint) -> typing.Callable: + return lambda: BaseMenu(self.window, self, self.game, cp.base) + + def update(self): + self.window.clear_right_pane() + + row = 1 + + def label(text): + nonlocal row + Label(self.frame, text=text).grid(column=0, row=row, sticky=NW) + row += 1 + + def event_button(event, text): + nonlocal row + Button(self.frame, text=text, command=self.start_event(event)).grid(column=0, row=row, sticky=N) + row += 1 + + def cp_button(cp): + nonlocal row + title = "{}{}{}{}".format( + cp.name, + "^" * cp.base.total_planes, + "." * cp.base.total_armor, + "*" * cp.base.total_aa) + Button(self.frame, text=title, command=self.go_cp(cp)).grid(column=0, row=row, sticky=NW) + row += 1 + + Button(self.frame, text="Pass turn", command=self.pass_turn).grid(column=0, row=row, sticky=N); row += 1 + label("Budget: {}m".format(self.game.budget)) + + for event in self.game.events: + event_button(event, "{} {}".format(event.attacker.name != self.game.player and "!" or " ", event)) + + Separator(self.frame, orient='horizontal').grid(column=0, row=row, sticky=EW); row += 1 + for cp in self.game.theater.player_points(): + cp_button(cp) + + Separator(self.frame, orient='horizontal').grid(column=0, row=row, sticky=EW); row += 1 + for cp in self.game.theater.enemy_bases(): + title = "[{}] {}{}{}{}".format( + int(cp.base.strength * 10), + cp.name, + "^" * cp.base.total_planes, + "." * cp.base.total_armor, + "*" * cp.base.total_aa) + Label(self.frame, text=title).grid(column=0, row=row, sticky=NE) + row += 1 + diff --git a/ui/window.py b/ui/window.py new file mode 100644 index 00000000..8d0e64b9 --- /dev/null +++ b/ui/window.py @@ -0,0 +1,41 @@ +from tkinter import * + + +class Window: + image = None + left_pane = None # type: Frame + right_pane = None # type: Frame + + def __init__(self): + self.tk = Tk() + self.tk.grid_columnconfigure(0, weight=1) + self.tk.grid_rowconfigure(0, weight=1) + + self.frame = Frame(self.tk) + self.frame.grid(column=0, row=0, sticky=NSEW) + self.frame.grid_columnconfigure(0, minsize=300) + self.frame.grid_columnconfigure(1, minsize=300) + + self.frame.grid_columnconfigure(0, weight=0) + self.frame.grid_columnconfigure(1, weight=1) + self.frame.grid_rowconfigure(0, weight=1) + + self.left_pane = Frame(self.frame) + self.left_pane.grid(column=0, row=0, sticky=NSEW) + self.right_pane = Frame(self.frame) + self.right_pane.grid(column=1, row=0, sticky=NSEW) + + self.tk.focus() + + def clear_right_pane(self): + for x in self.right_pane.winfo_children(): + x.grid_remove() + + def clear(self): + for x in self.left_pane.winfo_children(): + x.grid_remove() + for x in self.right_pane.winfo_children(): + x.grid_remove() + + def run(self): + self.tk.mainloop() diff --git a/userdata/debriefing_parser.py b/userdata/debriefing_parser.py new file mode 100644 index 00000000..3d80aa6a --- /dev/null +++ b/userdata/debriefing_parser.py @@ -0,0 +1,12 @@ +import typing +import json + + +class Debriefing: + def __init__(self): + self.destroyed_units = {} # type: typing.Dict[str, typing.Dict[str, int]] + + def parse(self, path: str): + with open(path, "r") as f: + events = json.load(f) +