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)
+