Browse Source

add fall2022

olinox14 3 years ago
parent
commit
68bc64ff74

+ 1 - 1
.idea/misc.xml

@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (clonedb)" project-jdk-type="Python SDK" />
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
 </project>

+ 242 - 107
cultist_war/main.py

@@ -131,15 +131,22 @@ PLAYERS_ORDER = sorted([ME, OPPONENT], key=lambda p: p.id)
 
 
 class Threat(BaseClass):
-    def __init__(self, unit, damages, target, line):
+    def __init__(self, unit, damages, pos, line):
         self.unit = unit
         self.damages = damages
-        self.target = target
+        self.pos = pos
         self.line = line if line else []
-        self.fatal = damages >= target.hp
+
+        self.target = None
+        self.fatal = False
+        self.neutral = False
 
     def __repr__(self):
-        return f"<Threat (unit: {self.unit.id}, damages={self.damages}, target={self.target.id}, fatal={self.fatal}, line={self.line})>"
+        return f"<Threat (unit: {self.unit.id}, damages={self.damages}, pos={self.pos}, target={self.target.id if self.target else None}, fatal={self.fatal}, line={self.line})>"
+
+    def fatal_for(self, unit):
+        return self.damages >= unit.hp
+
 
 class Unit(BaseClass):
     TYPE_CULTIST = 0
@@ -149,18 +156,20 @@ class Unit(BaseClass):
     OWNER_1 = 1
     NO_OWNER = 2
 
+    MAX_HP = 10
     SHOOTING_RANGE = 6
     SHOOTING_MAX_DAMAGE = 7
 
     def __init__(self, id_):
         self.id = id_
-        self.hp = 10
+        self.hp = Unit.MAX_HP
         self.x = None
         self.y = None
         self.owner = None
 
-        self.threats = []
+        self.targets = []
         self.threaten_by = []
+        self.has_been_shooted = False
 
     @property
     def pos(self):
@@ -245,8 +254,10 @@ class Grid(BaseClass):
 
         self.index = {}
         self.units = {}
-        self.threat = {}
+        self.threats_on = {}
+        self.max_threat = {}
         self.conversion_path = ConversionPath()
+        self.ideal_conversion_path = ConversionPath()
 
         self.cult_leader = None
         self.opponent_cult_leader = None
@@ -255,6 +266,7 @@ class Grid(BaseClass):
         self.opponent_units = []
         self.opponent_cultists = []
         self.neutrals = []
+        self.previous_hps = {}
 
     def pre_compute(self):
         self.cells = [(x, y) for x in range(self.width) for y in range(self.height)]
@@ -264,15 +276,31 @@ class Grid(BaseClass):
                                        0 <= xn < self.width and 0 <= yn < self.height]
 
     def reinit_round(self):
+        self.previous_hps = {u.id: u.hp for u in self.owned_units}
         self.units = {}
+        self.threats_on = {pos: [] for pos in self.cells}
+        self.max_threat = {pos: 0 for pos in self.cells}
+        self.conversion_path = ConversionPath()
+        self.ideal_conversion_path = ConversionPath()
 
     def update_unit(self, id_, type_, hp, x, y, owner):
+
+        # quand le bresenham foire, histoire de "corriger le tir", on retient le coup pour en tenir compte
+        has_been_shooted = False
+        if id_ in self.previous_hps:
+            if hp < self.previous_hps[id_]:
+                has_been_shooted = True
+
         self.units[id_] = Unit(id_) if type_ != Unit.TYPE_CULT_LEADER else CultLeader(id_)
         unit = self.units[id_]
         unit.hp = hp
         unit.x = x
         unit.y = y
         unit.owner = owner
+        unit.targets = []
+        unit.threaten_by = []
+        unit.has_been_shooted = has_been_shooted
+
 
     def update_index(self):
         self.index = {}
@@ -302,78 +330,83 @@ class Grid(BaseClass):
             else:
                 self.neutrals.append(unit)
 
-    def update_threat_map(self):
-        self.threat = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
+    def update_threat(self):
 
-        sources = self.opponent_cultists
+        sources = self.allied_cultists + self.opponent_cultists
 
-        # On ajoute les neutres voisins du leader ennemi aux sources de menace possible
         if self.opponent_cult_leader:
-            for pos in self.neighbors(*self.opponent_cult_leader.pos):
-                if pos in self.index and self.index[pos].neutral:
-                    sources.append(self.index[pos])
+            for n in self.neighbors(*self.opponent_cult_leader.pos):
+                if n in self.index and self.index[n].neutral:
+                    sources.append(self.index[n])
 
         for u in sources:
-            shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
-            for x, y in shooting_zone:
-                dist = shooting_zone[(x, y)]
+            threat_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
+            for pos in threat_zone:
+                if pos in self.obstacles:
+                    continue
 
-                if not self.line_of_sight(u.pos, (x, y)):
+                line_of_sight = self.line_of_sight(u.pos, pos)
+                if not line_of_sight:
                     continue
 
-                threat = Unit.SHOOTING_RANGE + 1 - dist
-                if u.neutral:
-                    threat //= 2
+                dist = self.manhattan(u.pos, pos)
 
-                if threat > self.threat[(x, y)]:
-                    self.threat[(x, y)] = threat
+                damages = u.SHOOTING_MAX_DAMAGE - dist
+                threat = Threat(u, damages, pos, line_of_sight)
 
-        for u in self.opponent_units:
-            u.threat = []
-            for a in self.owned_units:
-                line_of_sight = self.line_of_sight(u.pos, a.pos)
-                if not line_of_sight:
-                    continue
+                self.threats_on[pos].append(threat)
 
-                dist = self.manhattan(u.pos, a.pos)
-                if dist <= u.SHOOTING_RANGE:
-                    damages = u.SHOOTING_MAX_DAMAGE - dist
-                    threat = Threat(u, damages, a, line_of_sight)
-                    u.threats.append(threat)
-                    a.threaten_by.append(threat)
+                if not u.owned and self.max_threat[pos] < damages:
+                    self.max_threat[pos] = damages
 
-        for a in self.owned_units:
-            a.threat = []
-            for u in self.opponent_units:
-                line_of_sight = self.line_of_sight(a.pos, u.pos)
-                if not line_of_sight:
-                    continue
+                if pos in self.index:
+                    target = self.index[pos]
+                    if target.owner == u.owner:
+                        continue
+
+                    threat.target = target
+                    threat.fatal = damages >= target.hp
+                    threat.neutral = u.neutral
 
-                dist = self.manhattan(u.pos, a.pos)
-                if dist <= u.SHOOTING_RANGE:
-                    damages = u.SHOOTING_MAX_DAMAGE - dist
-                    threat = Threat(a, damages, u, line_of_sight)
-                    a.threats.append(threat)
-                    u.threaten_by.append(threat)
+                    u.targets.append(threat)
+                    target.threaten_by.append(threat)
+
+        # fallback pour compenser le bresenham inégal
+        for a in self.owned_units:
+            for t in a.threaten_by:
+                if not t in a.targets:
+                    threat = Threat(a, t.damages, a.pos, t.line[::-1])
+                    threat.target = t.unit
+                    threat.neutral = False
+                    a.targets.append(threat)
 
     def compute_conversion_path(self):
         conversion_path = ConversionPath()
+        ideal_conversion_path = ConversionPath()
 
         if self.cult_leader and self.neutrals:
             conversion_path = self.get_conversion_path(
                 self.cult_leader,
                 key=(lambda pos: pos in self.index and self.index[pos].neutral),
+                moving_key=lambda pos: self.can_move_on(self.cult_leader, pos),
                 limit=min(4, len(self.neutrals))
             )
             log(f"conversion : {conversion_path}")
+
+            ideal_conversion_path = self.get_conversion_path(
+                self.cult_leader,
+                key=(lambda pos: pos in self.index and self.index[pos].neutral),
+                moving_key=lambda pos: self.can_move_on(self.cult_leader, pos) or (pos in self.index and self.index[pos].owned),
+                limit=min(4, len(self.neutrals))
+            )
+            log(f"ideal conversion : {ideal_conversion_path}")
         self.conversion_path = conversion_path
+        self.ideal_conversion_path = ideal_conversion_path
 
     def update(self):
-        log('update indexes')
         self.update_index()
-        self.update_threat_map()
+        self.update_threat()
         self.compute_conversion_path()
-        log('indexes updated')
 
         # log(self.obstacles + [u.pos for u in self.allied_cultists])
         # log([n.pos for n in self.neutrals])
@@ -384,22 +417,33 @@ class Grid(BaseClass):
         advantage = len(self.owned_units) > len(self.opponent_units)
         sure_advantage = len(self.owned_units) > (len(self.opponent_units) + len(self.neutrals))
 
-        for u in self.allied_cultists:
-            if u.threats:
-                log(f"Threats : {u.id} - {u.threats}")
+        for u in self.owned_units:
             if u.threaten_by:
-                log(f"Threaten : {u.id} - {u.threaten_by}")
+                log(f"Threats : {u.targets}")
+                log(f"Threats : {u.threaten_by}")
+            elif u.has_been_shooted:
+                log(f"<!!!> {u.id} has been shooted by unknown threat!")
 
         # Leader take cover
-        k0_protect_cult_leader = 30
+        k0_protect_cult_leader = 25
         k_protect_threat_level = -5
         k_protect_worth_risk = 3
         k_cover_interest = -10
 
         if self.cult_leader:
-            current_threat = self.threat[self.cult_leader.pos]
+            threat_level = self.max_threat[self.cult_leader.pos]
+
+            # pas de menace apparente, mais le leader a reçu un tir depuis le tour précédent (pbm de bresenham)
+            if not threat_level and self.cult_leader.has_been_shooted:
+                log("<!!> leader seems not threaten but has been hurted!")
+                threat_level = 10
+
+            # une unité voisine peut être convertie, on laisse ça à l'action convert
+            threats = [t
+                       for t in self.cult_leader.threaten_by
+                       if t.unit not in self.neighbors(*self.cult_leader.pos)]
 
-            if current_threat:
+            if threats or self.cult_leader.has_been_shooted:
                 covers = [n for n in self.neighbors(*self.cult_leader.pos) if self.can_move_on(self.cult_leader, n)]
 
                 for pos in covers:
@@ -407,105 +451,164 @@ class Grid(BaseClass):
                                                                                                     self.conversion_path.steps])
 
                     priority = k0_protect_cult_leader
-                    priority += k_protect_threat_level * (current_threat - self.threat[pos])
+                    priority += k_protect_threat_level * (threat_level - self.max_threat[pos])
                     priority += k_cover_interest * interest
-                    priority += k_protect_worth_risk  * (self.cult_leader.hp - (current_threat + 1))
+                    priority += k_protect_worth_risk * (self.cult_leader.hp - (threat_level + 1))
 
-                    action = ActionMove(self.cult_leader, pos, f'take cover (current: {current_threat}, new: {self.threat[pos]})')
+                    action = ActionMove(self.cult_leader, pos,
+                                        f'take cover (current: {threat_level}, new: {self.max_threat[pos]})')
                     actions.put(priority, action)
 
         # Convert
         k0_convert = 15
         k_convert_number = -10
-        k_convert_distance = 10
         k_convert_danger = 20
+        k_convert_threat_on = 3
+        k_convert_hurted = 1
+        k_convert_neighbor_threat = -15
+        k_convert_neighbor_opponent = -20
+        k_convert_distance = 10
 
-        if self.cult_leader and self.conversion_path:
-            path = self.conversion_path.next_candidate()
-            if path:
-                targets = [self.index[c] for c in path[-1].candidates]
-
+        if self.cult_leader:
+            immediate_targets = [
+                self.index[n] for n in self.neighbors(*self.cult_leader.pos)
+                if n in self.index and self.index[n] not in self.owned_units and self.index[n] is not self.opponent_cult_leader
+            ]
+            for target in immediate_targets:
                 priority = k0_convert
-                priority += k_convert_number * len(targets)
-                priority += k_convert_distance * len(path)
-                priority += k_convert_danger * sum([self.threat[s.pos] for s in path])
+                priority += k_convert_neighbor_threat * (target.neutral and target in [t.unit for t in self.cult_leader.threaten_by])
+                priority += k_convert_neighbor_opponent * target.opponent
+                priority += k_convert_threat_on * max([t.damages for t in self.threats_on[target.pos] if t.unit.opponent], default=0)
+
+                action = ActionConvert(self.cult_leader, target)
+                actions.put(priority, action)
+
+            # Go Convert
+            if not immediate_targets and self.conversion_path:
+                path = self.conversion_path.next_candidate()
+                if path:
+                    targets = [self.index[c] for c in path[-1].candidates]
+
+                    priority = k0_convert
+                    priority += k_convert_number * len(targets)
+                    priority += k_convert_distance * len(path)
+                    priority += k_convert_danger * sum([self.max_threat[s.pos] for s in path])
+
+                    for target in targets:
+                        priority += k_convert_hurted * (Unit.MAX_HP - target.hp)
+                        priority += k_convert_threat_on * max([t.damages for t in self.threats_on[target.pos] if t.unit.opponent], default=0)
 
-                if len(path) == 1:
-                    action = ActionConvert(self.cult_leader, targets[0])
-                else:
                     action = ActionMove(self.cult_leader, path[1].pos,
                                         f'go convert {",".join([str(t.id) for t in targets])}')
-                actions.put(priority, action)
+                    actions.put(priority, action)
 
         # Shoot opponent units
         k0_shoot = 0
         k_shoot_opponent_cultist = 6
-        k_shoot_opponent_leader = 5
+        k_shoot_opponent_leader = 6 if self.neutrals else 10
+        k_shoot_fatal = -5
         k_shoot_kill_before_he_kills = -5
         k_shoot_sacrifice = 30
         k0_runaway = 10
         k_runaway_threat = 10
 
         for a in self.allied_cultists:
-            if not a.threats:
+            if not a.targets:
                 continue
 
-            for threat in a.threats:
+            for threat in a.targets:
                 priority = k0_shoot
 
+                if threat.target.neutral:
+                    continue
+
                 # Il est probable que l'unité se fera tirer dessus par l'ennemi le plus proche: quel résultat?
-                can_be_killed_by = [t for t in a.threaten_by if t.fatal]
+                deadly_threaten_by = [t for t in a.threaten_by if t.fatal]
+                if not a.threaten_by and a.has_been_shooted:
+                    log(f"<!!> unit {a.id} seems not threaten but has been hurted!")
+                    deadly_threaten_by.append(Threat(self.opponent_cult_leader, 10, a.pos, []))
+
+                if not a.threaten_by and a.has_been_shooted:
+                    deadly_threaten_by.append(Threat(self.opponent_cult_leader, 10, a.pos, []))
 
-                if can_be_killed_by and not threat.fatal:
+                if deadly_threaten_by and not threat.fatal:
                     # run away! (si possible)
-                    worth_sacrificing = any(t.target is self.cult_leader or (t.fatal and t.target is not a) for t in threat.target.threats)
                     covers = [n for n in self.neighbors(*a.pos) if self.can_move_on(a, n)]
 
-                    if covers and not worth_sacrificing:
+                    if covers:
                         for cover in covers:
-                            if self.threat[cover] < a.hp and not any(self.line_of_sight(u.unit.pos, cover) for u in can_be_killed_by):
+                            threats_on_cover = [t for t in self.threats_on[cover] if not t.unit.owned]
+                            if not any(t.fatal_for(a) for t in threats_on_cover):
                                 action = ActionMove(a, cover, f'run away!')
                                 priority += k0_runaway
-                                priority += k_runaway_threat * self.threat[cover]
+                                priority += k_runaway_threat * self.max_threat[cover]
                                 actions.put(priority, action)
                                 continue
 
                     log(f'<!> {a.id} seems lost')
 
-                elif any(t for t in can_be_killed_by if t.unit != threat.target) and threat.fatal:
+                elif threat.fatal and deadly_threaten_by:
                     # l'unité peut tuer la cible, mais sera probablement tuée par une autre unité ennemie
                     priority += k_shoot_sacrifice
 
                 if not threat.fatal:
                     k = k_shoot_opponent_leader if threat.target is self.opponent_cult_leader else k_shoot_opponent_cultist
                     priority += k * len(threat.line)
+                else:
+                    priority += k_shoot_fatal
 
                 # peut tuer un adversaire avant qu'il ne tue un allié
-                priority += k_shoot_kill_before_he_kills * any(t.fatal and t.target is not a for t in threat.target.threats)
+                priority += k_shoot_kill_before_he_kills * any(t.fatal and t.target is not a for t in threat.target.targets)
 
-                action = ActionShoot(a, threat.target, f'shoot from {a.pos} (damages: {threat.damages}, fatal: {threat.fatal}, line: {threat.line})')
+                action = ActionShoot(a, threat.target,
+                                     f'shoot from {a.pos} (damages: {threat.damages}, pos: {threat.pos}, target: {threat.target}, fatal: {threat.fatal}, line: {threat.line})')
 
                 actions.put(priority, action)
 
+        # Intercept
+        k0_intercept = 20
+        position_hp_min = 5
+        k_intercept_diff = 3
+
+        for a in self.allied_cultists:
+            if a.hp < position_hp_min:
+                continue
+
+            for o in (u for u in self.owned_units if u is not a):
+                neighbors = self.neighbors(*a.pos)
+
+                for threat in o.threaten_by:
+                    if threat.fatal and not threat.fatal_for(a):
+                        for n in neighbors:
+                            if n in threat.line:
+                                action = ActionMove(a, n, f'intercept {threat}')
+
+                                priority = k0_intercept
+                                priority = k_intercept_diff * (a.hp - o.hp)
+
+                                actions.put(priority, action)
+
         # Position: go to front line
         k0_position = 50
         k_position_advantage = 40
         k_position_distance = 3  # on veut privilégier les plus près, mais pas non plus décourager ceux de l'arrière...
-        position_hp_min = 5
 
         for a in self.allied_cultists:
             if a.hp < position_hp_min:
                 continue
-            if self.threat[a.pos]:
+
+            if self.max_threat[a.pos]:
                 # unit already under threat, let shoot action manage this
                 continue
-            if any(self.threat[n] for n in self.neighbors(*a.pos)):
+
+            if any(self.max_threat[n] for n in self.neighbors(*a.pos)):
                 # unit already on frontline
                 continue
 
             # l'unité semble en sécurité, go to front-line
-            nearest_frontline = self.discover(a, key=lambda x: self.can_move_on(a, x) and self.threat[x] == 1,
+            nearest_frontline = self.discover(a, key=lambda x: self.can_move_on(a, x) and self.max_threat[x] >= 1,
                                               limit=1)
+            # log(f"{a.id} - {nearest_frontline}")
             if not nearest_frontline:
                 log(f"<!> {a.id} can not join nearest frontline")
                 continue
@@ -520,8 +623,43 @@ class Grid(BaseClass):
                 action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
                 actions.put(priority, action)
 
+        # Move out
+        k0_move_out = 20
+        k_move_out_gain = -3
+        k_move_out_still_in_path_but_later = 5
+        k_move_out_default_gain = 4
+
+        if self.cult_leader:
+            actual_path = [s.pos for s in (self.conversion_path.next_candidate() or [])]
+            ideal_path = [s.pos for s in (self.ideal_conversion_path.next_candidate() or [])]
+
+            if ideal_path:
+                for a in self.allied_cultists:
+                    if a.pos in ideal_path:
+                        gain = len(actual_path) - len(ideal_path) if actual_path else k_move_out_default_gain
+
+                        if gain > 0:
+                            for n in self.neighbors(*a.pos):
+                                if not self.can_move_on(a, n):
+                                    continue
+
+                                priority = k0_move_out
+                                still_in_the_way = (n in ideal_path)
+
+                                if still_in_the_way:
+                                    if ideal_path.index(n) > ideal_path.index(a.pos):
+                                        priority += k_move_out_still_in_path_but_later
+                                    else:
+                                        continue
+
+                                priority += k_move_out_gain * gain
+
+                                action = ActionMove(a, n, f'move out of the way (gain: {gain}, in the way: {still_in_the_way})')
+                                actions.put(priority, action)
+
+
         # Shoot neutral units:
-        k_shoot_dangerous_neutral_threat = 2
+        k_shoot_dangerous_neutral_threat = 3
         k_shoot_dangerous_neutral_distance = 8
         dangerous_neutral_distance_limit = 3
 
@@ -544,8 +682,8 @@ class Grid(BaseClass):
                         target_pos] < len(path):
                         threaten_neutrals_from_leader[target_pos] = len(path)
 
-            log(f"Nearest from opp. leader: {neutrals_threaten}")
-            log(f"Nearest from own  leader: {threaten_neutrals_from_leader}")
+            # log(f"Nearest from opp. leader: {neutrals_threaten}")
+            # log(f"Nearest from own  leader: {threaten_neutrals_from_leader}")
 
             lost_causes = {}
             for target, dist in neutrals_threaten.items():
@@ -574,7 +712,6 @@ class Grid(BaseClass):
 
                         priority = k_shoot_dangerous_neutral_distance * shooting_distance
                         priority += k_shoot_dangerous_neutral_threat * dist
-                        # log(self.line_of_sight(a.pos, u.pos))
                         actions.put(priority, action)
 
         # Last hope: take risks
@@ -588,7 +725,7 @@ class Grid(BaseClass):
                     continue
 
                 for n in self.neighbors(*a.pos):
-                    if not self.threat[n]:
+                    if not self.max_threat[n]:
                         continue
 
                     ongoing_fights = []
@@ -610,11 +747,11 @@ class Grid(BaseClass):
                     action = ActionMove(a, n, f"last hope, moving")
 
                     priority = k0_last_chance
-                    priority += k_last_chance_danger * max(d for _, d in ongoing_fights)
+                    priority += k_last_chance_danger * max([d for _, d in ongoing_fights], default=0)
                     actions.put(priority, action)
 
-        # TODO: si les deux leaders sont de chaque côté d'un neutre, même si notre leader pourrait convertir le premier, la menace l'emporte et il se barre... repenser le mécanisme de 'take cover'
-        # TODO: ne pas aller convertir les neutres qui sont sous le feu ennemi, ou au moins baisser leur priorité (surtout s'ils sont la seule chose entre le leader et les ennemis)
+        # TODO: prévoir une action intercept, où une unité alliée se place sur le chemin d'une menace fatale, si l'unité a suffisamment de pv pour affronter la menace sans mourir
+        # TODO: ne pas aller convertir les neutres qui sont sous le feu ennemi, ou au moins baisser leur priorité (surtout s'ils sont la seule chose entre le leader et les ennemis);
         # TODO: augmenter la prise de risque pour la conversion, parfois ça peut changer le résultat si le leader a tous ses pvs
         # TODO: cf Capture d’écran 2022-09-24 014757.png -> même si pas de ligne entre deux pos, la position peut se retrouver sur une ligne de mire avec une autre cible... snif
         # TODO: l'algo actuel va privilégier un tir sur un neutre lointain voisin du leader ennemi, plutôt que sur un ennemi en train de nous tirer dessus
@@ -638,7 +775,7 @@ class Grid(BaseClass):
     def moving_cost(self, unit, pos):
         if not self.can_move_on(unit, pos):
             return -1
-        return 1 + self.threat[pos]
+        return 1 + self.max_threat[pos]
 
     @staticmethod
     def manhattan(from_, to_):
@@ -651,14 +788,12 @@ class Grid(BaseClass):
 
     def zone(self, pos, radius):
         x0, y0 = pos
-        zone = {}
-
+        zone = []
         for x in range(max(x0 - radius, 0), min(x0 + radius, self.width)):
             for y in range(max(y0 - radius, 0), min(y0 + radius, self.height)):
                 dist = self.manhattan(pos, (x, y))
                 if dist <= radius:
-                    zone[(x, y)] = dist
-
+                    zone.append((x, y))
         return zone
 
     @classmethod
@@ -800,7 +935,7 @@ class Grid(BaseClass):
                 nodes.append(node)
         return paths
 
-    def get_conversion_path(self, unit, key, limit=5):
+    def get_conversion_path(self, unit, key, moving_key, limit=5):
         """ essaies de trouver le meilleur chemin pour relier des cases dont au moins une voisine valide
             la condition 'key' (dans la limite de 'limit')"""
         nodes = Queue()
@@ -829,10 +964,10 @@ class Grid(BaseClass):
             current = nodes.get()
 
             for pos in self.neighbors(*current):
-                if not self.can_move_on(unit, pos):
+                if not moving_key(pos):
                     continue
 
-                moving_cost = 1 + self.threat[pos]
+                moving_cost = 1 + self.max_threat[pos]
 
                 matches = []
                 for n in self.neighbors(*pos):
@@ -870,8 +1005,8 @@ class Grid(BaseClass):
         return ConversionPath.make_from_discovery_node(best_node)
 
     def _repr_cell(self, pos):
-        return f"{self.threat[pos]}"
-        # return f"{self.control[pos]}/{self.threat[pos]}"
+        return f"{self.max_threat[pos]}"
+        # return f"{self.control[pos]}/{self.max_threat[pos]}"
         # return self.heat_map[pos]
 
         if pos in self.obstacles:

+ 3 - 0
fall2022/.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 8 - 0
fall2022/.idea/fall2022.iml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 20 - 0
fall2022/.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,20 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="ignoredErrors">
+        <list>
+          <option value="N802" />
+        </list>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredIdentifiers">
+        <list>
+          <option value="vlc.Meta.Title" />
+          <option value="vlc.Meta.*" />
+        </list>
+      </option>
+    </inspection_tool>
+  </profile>
+</component>

+ 6 - 0
fall2022/.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 4 - 0
fall2022/.idea/misc.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
+</project>

+ 8 - 0
fall2022/.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/fall2022.iml" filepath="$PROJECT_DIR$/.idea/fall2022.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
fall2022/.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
+  </component>
+</project>

+ 346 - 0
fall2022/fall2022.py

@@ -0,0 +1,346 @@
+import heapq
+import sys
+import time
+
+debug = True
+
+t0 = time.time()
+
+
+def log(*msg):
+    if debug:
+        print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr, flush=True)
+
+
+class BaseClass:
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+
+class Queue(BaseClass):
+    def __init__(self):
+        self.items = []
+
+    def __bool__(self):
+        return bool(self.items)
+
+    def __repr__(self):
+        return str(self.items)
+
+    def values(self):
+        return (v for _, v in self.items)
+
+    def put(self, priority, item):
+        while priority in [p for p, _ in self.items]:
+            priority += 1
+        heapq.heappush(self.items, (priority, item))
+
+    def get(self):
+        return heapq.heappop(self.items)[1]
+
+    def get_items(self):
+        return heapq.heappop(self.items)
+
+
+class Player(BaseClass):
+    ME = 1
+    OPPONENT = 0
+    NONE = -1
+
+    def __init__(self, id_):
+        self.id = id_
+        self.matter = 0
+
+
+class Cell(BaseClass):
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+
+        self.amount = 0
+        self.owner = Player.NONE
+        self.units = 0
+        self.recycler = False
+
+        self.can_build = False
+        self.can_spawn = False
+        self.in_range_of_recycler = False
+
+    @property
+    def pos(self):
+        return self.x, self.y
+
+    @property
+    def owned(self):
+        return self.owner == Player.ME
+
+    @property
+    def is_opponents(self):
+        return self.owner == Player.OPPONENT
+
+    def is_movable(self):
+        return self.amount > 0 and not self.recycler
+
+    def unmovable_next_round(self):
+        return self.amount == 1 and self.in_range_of_recycler
+
+    def update(self, scrap_amount, owner, units, recycler, can_build, can_spawn, in_range_of_recycler):
+        self.amount = scrap_amount
+        self.owner = owner
+        self.units = units
+        self.recycler = recycler
+        self.can_build = can_build
+        self.can_spawn = can_spawn
+        self.in_range_of_recycler = in_range_of_recycler
+
+
+class RobotGroup(BaseClass):
+    COST = 10
+
+    def __init__(self, x, y, owner, amount):
+        self.x = x
+        self.y = y
+        self.owner = owner
+        self.amount = amount
+
+    @property
+    def pos(self):
+        return self.x, self.y
+
+
+class Recycler(BaseClass):
+    COST = 10
+
+    def __init__(self, x, y, owner, area):
+        self.x = x
+        self.y = y
+        self.owner = owner
+        self.area = area
+
+    @property
+    def total_available_amount(self):
+        return sum(cell.amount for cell in self.area)
+
+    @property
+    def immediately_available_amount(self):
+        return sum((cell.amount > 0) for cell in self.area)
+
+    def lifetime(self):
+        return min(cell.amount for cell in self.area)
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: ({self.x}, {self.y}), {self.owner}, " \
+               f"{self.immediately_available_amount}, {self.total_available_amount}, {self.lifetime()}>"
+
+
+class Action(BaseClass):
+    pass
+
+
+class Wait(Action):
+    @staticmethod
+    def do():
+        return "WAIT"
+
+
+class Move(Action):
+    def __init__(self, from_, to, amount):
+        self.from_ = from_
+        self.to = to
+        self.amount = amount
+
+    def do(self):
+        x0, y0 = self.from_
+        x1, y1 = self.to
+        return f'MOVE {self.amount} {x0} {y0} {x1} {y1}'
+
+
+class Build(Action):
+    COST = 10
+
+    def __init__(self, pos):
+        self.pos = pos
+
+    def do(self):
+        x, y = self.pos
+        return f'BUILD {x} {y}'
+
+
+class Spawn(Action):
+    COST = 10
+
+    def __init__(self, pos, amount):
+        self.pos = pos
+        self.amount = amount
+
+    def do(self):
+        x, y = self.pos
+        return f'SPAWN {self.amount} {x} {y}'
+
+
+class Grid(BaseClass):
+    def __init__(self, width, height, me, opponent):
+        self.width = width
+        self.height = height
+        self.cells = {(x, y): Cell(x, y) for x in range(width) for y in range(height)}
+
+        self.round = 0
+
+        self.me = me
+        self.opponent = opponent
+
+        self.units = {}
+        self.recyclers = {}
+
+    @property
+    def grid(self):
+        return [[self.cells[(x, y)] for x in range(self.width)] for y in range(self.height)]
+
+    def print(self, key=None):
+        if key is None:
+            key = lambda x: x
+        log("\n" + "\n".join(["".join([f"{key(c)}|" for c in row]) for row in self.grid]))
+
+    @staticmethod
+    def manhattan(from_, to_):
+        xa, ya = from_
+        xb, yb = to_
+        return abs(xa - xb) + abs(ya - yb)
+
+    def neighbors(self, x, y):
+        return [
+            (x, y)
+            for x, y in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
+            if 0 <= x < self.width and 0 <= y < self.height
+        ]
+
+    @staticmethod
+    def create():
+        me = Player(Player.ME)
+        opponent = Player(Player.OPPONENT)
+        w, h = [int(i) for i in input().split()]
+        return Grid(w, h, me, opponent)
+
+    def update(self):
+        my_matter, opp_matter = [int(i) for i in input().split()]
+
+        self.me.matter = my_matter
+        self.opponent.matter = opp_matter
+
+        for y in range(self.height):
+            for x in range(self.width):
+                scrap_amount, owner, units, recycler, can_build, can_spawn, in_range_of_recycler = [int(k) for k in
+                                                                                                    input().split()]
+                self.cells[(x, y)].update(scrap_amount, owner, units, recycler, can_build, can_spawn,
+                                          in_range_of_recycler)
+
+        # update robots
+        self.units = {}
+        for cell in self.cells.values():
+            if cell.units:
+                self.units[cell.pos] = RobotGroup(cell.x, cell.y, cell.owner, cell.units)
+
+        # update recyclers
+        self.recyclers = {}
+        seen = set()
+        for cell in self.cells.values():
+            if cell.recycler:
+                area = [self.cells[pos] for pos in self.neighbors(*cell.pos) if cell.pos not in seen]
+                seen |= set(self.neighbors(*cell.pos))
+                self.recyclers[cell.pos] = Recycler(cell.x, cell.y, cell.owner, area)
+
+    def owned_units(self):
+        return [r for r in self.units.values() if r.owner == Player.ME]
+
+    def opponent_units(self):
+        return [r for r in self.units.values() if r.owner == Player.OPPONENT]
+
+    def owned_recyclers(self):
+        return [r for r in self.recyclers.values() if r.owner == Player.ME]
+
+    def opponent_recyclers(self):
+        return [r for r in self.recyclers.values() if r.owner == Player.OPPONENT]
+
+    def act(self):
+
+        build_actions = Queue()
+
+        # List available build actions
+        places = [c for c in self.cells.values() if c.owned and c not in self.units]
+        k0 = 100
+        k_available_amount = -1
+        k_already_exploited = 30
+
+        for place in places:
+            action = Build(place.pos)
+            k = k0
+
+            area = [place.pos] + self.neighbors(*place.pos)
+            for pos in area:
+                k += k_available_amount * self.cells[pos].amount
+                k += k_already_exploited * self.cells[pos].in_range_of_recycler
+
+            build_actions.put(k, action)
+
+        # List available spawn actions
+        places = [c for c in self.cells.values() if c.owned]
+        k0 = 60
+        for place in places:
+            action = Spawn(place.pos, 1)
+            k = k0
+            build_actions.put(k, action)
+
+        # List available move actions
+        move_actions_per_unit = {}
+        # targets = Queue()
+        k0 = 100
+        k_blitzkrieg = -2
+        k_occupy = -5
+        k_destroy = -2
+
+        for u in self.owned_units():
+            move_actions_per_unit[u.pos] = Queue()
+
+            nearest_enemy_cell = min(
+                (c for c in self.cells.values() if c.is_opponents),
+                key=lambda c: self.manhattan(c.pos, u.pos)
+            )
+            nearest_enemy_cell_dist = self.manhattan(nearest_enemy_cell.pos, u.pos)
+
+            for pos in self.neighbors(*u.pos):
+                place = self.cells[pos]
+                action = Move(u.pos, place.pos, u.amount)
+
+                if not place.owned and place.is_movable():
+                    k = k0
+                    k += k_blitzkrieg * (nearest_enemy_cell_dist - self.manhattan(place.pos, nearest_enemy_cell.pos))
+                    if place.is_opponents:
+                        k += k_occupy
+                        if place.pos in self.units:
+                            k += k_destroy * self.units[place.pos].amount
+                    move_actions_per_unit[u.pos].put(k, action)
+
+        actions = []
+        expanse = 0
+        while build_actions and expanse <= self.me.matter:
+            action = build_actions.get()
+            actions.append(action)
+            expanse += action.COST
+
+        for move_actions in move_actions_per_unit.values():
+            if not move_actions:
+                continue
+            action = move_actions.get()
+            actions.append(action)
+
+        print(";".join([a.do() for a in actions]))
+
+
+grid = Grid.create()
+
+while True:
+    grid.update()
+
+    # grid.print(lambda x: f"{x.amount:2d}")
+
+    grid.act()

+ 3 - 0
spring2022/.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 20 - 0
spring2022/.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,20 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="ignoredErrors">
+        <list>
+          <option value="N802" />
+        </list>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredIdentifiers">
+        <list>
+          <option value="vlc.Meta.Title" />
+          <option value="vlc.Meta.*" />
+        </list>
+      </option>
+    </inspection_tool>
+  </profile>
+</component>

+ 6 - 0
spring2022/.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 4 - 0
spring2022/.idea/misc.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
+</project>

+ 8 - 0
spring2022/.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/spring2022.iml" filepath="$PROJECT_DIR$/.idea/spring2022.iml" />
+    </modules>
+  </component>
+</project>

+ 12 - 0
spring2022/.idea/spring2022.iml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+  <component name="PyDocumentationSettings">
+    <option name="format" value="PLAIN" />
+    <option name="myDocStringFormat" value="Plain" />
+  </component>
+</module>

+ 6 - 0
spring2022/.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
+  </component>
+</project>