olinox14 3 سال پیش
والد
کامیت
68bc64ff74

+ 1 - 1
.idea/misc.xml

@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
 <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>
 </project>

+ 242 - 107
cultist_war/main.py

@@ -131,15 +131,22 @@ PLAYERS_ORDER = sorted([ME, OPPONENT], key=lambda p: p.id)
 
 
 
 
 class Threat(BaseClass):
 class Threat(BaseClass):
-    def __init__(self, unit, damages, target, line):
+    def __init__(self, unit, damages, pos, line):
         self.unit = unit
         self.unit = unit
         self.damages = damages
         self.damages = damages
-        self.target = target
+        self.pos = pos
         self.line = line if line else []
         self.line = line if line else []
-        self.fatal = damages >= target.hp
+
+        self.target = None
+        self.fatal = False
+        self.neutral = False
 
 
     def __repr__(self):
     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):
 class Unit(BaseClass):
     TYPE_CULTIST = 0
     TYPE_CULTIST = 0
@@ -149,18 +156,20 @@ class Unit(BaseClass):
     OWNER_1 = 1
     OWNER_1 = 1
     NO_OWNER = 2
     NO_OWNER = 2
 
 
+    MAX_HP = 10
     SHOOTING_RANGE = 6
     SHOOTING_RANGE = 6
     SHOOTING_MAX_DAMAGE = 7
     SHOOTING_MAX_DAMAGE = 7
 
 
     def __init__(self, id_):
     def __init__(self, id_):
         self.id = id_
         self.id = id_
-        self.hp = 10
+        self.hp = Unit.MAX_HP
         self.x = None
         self.x = None
         self.y = None
         self.y = None
         self.owner = None
         self.owner = None
 
 
-        self.threats = []
+        self.targets = []
         self.threaten_by = []
         self.threaten_by = []
+        self.has_been_shooted = False
 
 
     @property
     @property
     def pos(self):
     def pos(self):
@@ -245,8 +254,10 @@ class Grid(BaseClass):
 
 
         self.index = {}
         self.index = {}
         self.units = {}
         self.units = {}
-        self.threat = {}
+        self.threats_on = {}
+        self.max_threat = {}
         self.conversion_path = ConversionPath()
         self.conversion_path = ConversionPath()
+        self.ideal_conversion_path = ConversionPath()
 
 
         self.cult_leader = None
         self.cult_leader = None
         self.opponent_cult_leader = None
         self.opponent_cult_leader = None
@@ -255,6 +266,7 @@ class Grid(BaseClass):
         self.opponent_units = []
         self.opponent_units = []
         self.opponent_cultists = []
         self.opponent_cultists = []
         self.neutrals = []
         self.neutrals = []
+        self.previous_hps = {}
 
 
     def pre_compute(self):
     def pre_compute(self):
         self.cells = [(x, y) for x in range(self.width) for y in range(self.height)]
         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]
                                        0 <= xn < self.width and 0 <= yn < self.height]
 
 
     def reinit_round(self):
     def reinit_round(self):
+        self.previous_hps = {u.id: u.hp for u in self.owned_units}
         self.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):
     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_)
         self.units[id_] = Unit(id_) if type_ != Unit.TYPE_CULT_LEADER else CultLeader(id_)
         unit = self.units[id_]
         unit = self.units[id_]
         unit.hp = hp
         unit.hp = hp
         unit.x = x
         unit.x = x
         unit.y = y
         unit.y = y
         unit.owner = owner
         unit.owner = owner
+        unit.targets = []
+        unit.threaten_by = []
+        unit.has_been_shooted = has_been_shooted
+
 
 
     def update_index(self):
     def update_index(self):
         self.index = {}
         self.index = {}
@@ -302,78 +330,83 @@ class Grid(BaseClass):
             else:
             else:
                 self.neutrals.append(unit)
                 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:
         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:
         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
                     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):
     def compute_conversion_path(self):
         conversion_path = ConversionPath()
         conversion_path = ConversionPath()
+        ideal_conversion_path = ConversionPath()
 
 
         if self.cult_leader and self.neutrals:
         if self.cult_leader and self.neutrals:
             conversion_path = self.get_conversion_path(
             conversion_path = self.get_conversion_path(
                 self.cult_leader,
                 self.cult_leader,
                 key=(lambda pos: pos in self.index and self.index[pos].neutral),
                 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))
                 limit=min(4, len(self.neutrals))
             )
             )
             log(f"conversion : {conversion_path}")
             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.conversion_path = conversion_path
+        self.ideal_conversion_path = ideal_conversion_path
 
 
     def update(self):
     def update(self):
-        log('update indexes')
         self.update_index()
         self.update_index()
-        self.update_threat_map()
+        self.update_threat()
         self.compute_conversion_path()
         self.compute_conversion_path()
-        log('indexes updated')
 
 
         # log(self.obstacles + [u.pos for u in self.allied_cultists])
         # log(self.obstacles + [u.pos for u in self.allied_cultists])
         # log([n.pos for n in self.neutrals])
         # log([n.pos for n in self.neutrals])
@@ -384,22 +417,33 @@ class Grid(BaseClass):
         advantage = len(self.owned_units) > len(self.opponent_units)
         advantage = len(self.owned_units) > len(self.opponent_units)
         sure_advantage = len(self.owned_units) > (len(self.opponent_units) + len(self.neutrals))
         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:
             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
         # Leader take cover
-        k0_protect_cult_leader = 30
+        k0_protect_cult_leader = 25
         k_protect_threat_level = -5
         k_protect_threat_level = -5
         k_protect_worth_risk = 3
         k_protect_worth_risk = 3
         k_cover_interest = -10
         k_cover_interest = -10
 
 
         if self.cult_leader:
         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)]
                 covers = [n for n in self.neighbors(*self.cult_leader.pos) if self.can_move_on(self.cult_leader, n)]
 
 
                 for pos in covers:
                 for pos in covers:
@@ -407,105 +451,164 @@ class Grid(BaseClass):
                                                                                                     self.conversion_path.steps])
                                                                                                     self.conversion_path.steps])
 
 
                     priority = k0_protect_cult_leader
                     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_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)
                     actions.put(priority, action)
 
 
         # Convert
         # Convert
         k0_convert = 15
         k0_convert = 15
         k_convert_number = -10
         k_convert_number = -10
-        k_convert_distance = 10
         k_convert_danger = 20
         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 = 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,
                     action = ActionMove(self.cult_leader, path[1].pos,
                                         f'go convert {",".join([str(t.id) for t in targets])}')
                                         f'go convert {",".join([str(t.id) for t in targets])}')
-                actions.put(priority, action)
+                    actions.put(priority, action)
 
 
         # Shoot opponent units
         # Shoot opponent units
         k0_shoot = 0
         k0_shoot = 0
         k_shoot_opponent_cultist = 6
         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_kill_before_he_kills = -5
         k_shoot_sacrifice = 30
         k_shoot_sacrifice = 30
         k0_runaway = 10
         k0_runaway = 10
         k_runaway_threat = 10
         k_runaway_threat = 10
 
 
         for a in self.allied_cultists:
         for a in self.allied_cultists:
-            if not a.threats:
+            if not a.targets:
                 continue
                 continue
 
 
-            for threat in a.threats:
+            for threat in a.targets:
                 priority = k0_shoot
                 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?
                 # 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)
                     # 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)]
                     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:
                         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!')
                                 action = ActionMove(a, cover, f'run away!')
                                 priority += k0_runaway
                                 priority += k0_runaway
-                                priority += k_runaway_threat * self.threat[cover]
+                                priority += k_runaway_threat * self.max_threat[cover]
                                 actions.put(priority, action)
                                 actions.put(priority, action)
                                 continue
                                 continue
 
 
                     log(f'<!> {a.id} seems lost')
                     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
                     # l'unité peut tuer la cible, mais sera probablement tuée par une autre unité ennemie
                     priority += k_shoot_sacrifice
                     priority += k_shoot_sacrifice
 
 
                 if not threat.fatal:
                 if not threat.fatal:
                     k = k_shoot_opponent_leader if threat.target is self.opponent_cult_leader else k_shoot_opponent_cultist
                     k = k_shoot_opponent_leader if threat.target is self.opponent_cult_leader else k_shoot_opponent_cultist
                     priority += k * len(threat.line)
                     priority += k * len(threat.line)
+                else:
+                    priority += k_shoot_fatal
 
 
                 # peut tuer un adversaire avant qu'il ne tue un allié
                 # 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)
                 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
         # Position: go to front line
         k0_position = 50
         k0_position = 50
         k_position_advantage = 40
         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...
         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:
         for a in self.allied_cultists:
             if a.hp < position_hp_min:
             if a.hp < position_hp_min:
                 continue
                 continue
-            if self.threat[a.pos]:
+
+            if self.max_threat[a.pos]:
                 # unit already under threat, let shoot action manage this
                 # unit already under threat, let shoot action manage this
                 continue
                 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
                 # unit already on frontline
                 continue
                 continue
 
 
             # l'unité semble en sécurité, go to front-line
             # 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)
                                               limit=1)
+            # log(f"{a.id} - {nearest_frontline}")
             if not nearest_frontline:
             if not nearest_frontline:
                 log(f"<!> {a.id} can not join nearest frontline")
                 log(f"<!> {a.id} can not join nearest frontline")
                 continue
                 continue
@@ -520,8 +623,43 @@ class Grid(BaseClass):
                 action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
                 action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
                 actions.put(priority, action)
                 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:
         # Shoot neutral units:
-        k_shoot_dangerous_neutral_threat = 2
+        k_shoot_dangerous_neutral_threat = 3
         k_shoot_dangerous_neutral_distance = 8
         k_shoot_dangerous_neutral_distance = 8
         dangerous_neutral_distance_limit = 3
         dangerous_neutral_distance_limit = 3
 
 
@@ -544,8 +682,8 @@ class Grid(BaseClass):
                         target_pos] < len(path):
                         target_pos] < len(path):
                         threaten_neutrals_from_leader[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 = {}
             lost_causes = {}
             for target, dist in neutrals_threaten.items():
             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_distance * shooting_distance
                         priority += k_shoot_dangerous_neutral_threat * dist
                         priority += k_shoot_dangerous_neutral_threat * dist
-                        # log(self.line_of_sight(a.pos, u.pos))
                         actions.put(priority, action)
                         actions.put(priority, action)
 
 
         # Last hope: take risks
         # Last hope: take risks
@@ -588,7 +725,7 @@ class Grid(BaseClass):
                     continue
                     continue
 
 
                 for n in self.neighbors(*a.pos):
                 for n in self.neighbors(*a.pos):
-                    if not self.threat[n]:
+                    if not self.max_threat[n]:
                         continue
                         continue
 
 
                     ongoing_fights = []
                     ongoing_fights = []
@@ -610,11 +747,11 @@ class Grid(BaseClass):
                     action = ActionMove(a, n, f"last hope, moving")
                     action = ActionMove(a, n, f"last hope, moving")
 
 
                     priority = k0_last_chance
                     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)
                     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: 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: 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
         # 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):
     def moving_cost(self, unit, pos):
         if not self.can_move_on(unit, pos):
         if not self.can_move_on(unit, pos):
             return -1
             return -1
-        return 1 + self.threat[pos]
+        return 1 + self.max_threat[pos]
 
 
     @staticmethod
     @staticmethod
     def manhattan(from_, to_):
     def manhattan(from_, to_):
@@ -651,14 +788,12 @@ class Grid(BaseClass):
 
 
     def zone(self, pos, radius):
     def zone(self, pos, radius):
         x0, y0 = pos
         x0, y0 = pos
-        zone = {}
-
+        zone = []
         for x in range(max(x0 - radius, 0), min(x0 + radius, self.width)):
         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)):
             for y in range(max(y0 - radius, 0), min(y0 + radius, self.height)):
                 dist = self.manhattan(pos, (x, y))
                 dist = self.manhattan(pos, (x, y))
                 if dist <= radius:
                 if dist <= radius:
-                    zone[(x, y)] = dist
-
+                    zone.append((x, y))
         return zone
         return zone
 
 
     @classmethod
     @classmethod
@@ -800,7 +935,7 @@ class Grid(BaseClass):
                 nodes.append(node)
                 nodes.append(node)
         return paths
         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
         """ essaies de trouver le meilleur chemin pour relier des cases dont au moins une voisine valide
             la condition 'key' (dans la limite de 'limit')"""
             la condition 'key' (dans la limite de 'limit')"""
         nodes = Queue()
         nodes = Queue()
@@ -829,10 +964,10 @@ class Grid(BaseClass):
             current = nodes.get()
             current = nodes.get()
 
 
             for pos in self.neighbors(*current):
             for pos in self.neighbors(*current):
-                if not self.can_move_on(unit, pos):
+                if not moving_key(pos):
                     continue
                     continue
 
 
-                moving_cost = 1 + self.threat[pos]
+                moving_cost = 1 + self.max_threat[pos]
 
 
                 matches = []
                 matches = []
                 for n in self.neighbors(*pos):
                 for n in self.neighbors(*pos):
@@ -870,8 +1005,8 @@ class Grid(BaseClass):
         return ConversionPath.make_from_discovery_node(best_node)
         return ConversionPath.make_from_discovery_node(best_node)
 
 
     def _repr_cell(self, pos):
     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]
         # return self.heat_map[pos]
 
 
         if pos in self.obstacles:
         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>