浏览代码

various fixes

olinox14 3 年之前
父节点
当前提交
5c4117e80b
共有 1 个文件被更改,包括 167 次插入89 次删除
  1. 167 89
      cultist_war/main.py

+ 167 - 89
cultist_war/main.py

@@ -130,6 +130,17 @@ PLAYERS_INDEX = {p.id: p for p in [ME, OPPONENT]}
 PLAYERS_ORDER = sorted([ME, OPPONENT], key=lambda p: p.id)
 
 
+class Threat(BaseClass):
+    def __init__(self, unit, damages, target, line):
+        self.unit = unit
+        self.damages = damages
+        self.target = target
+        self.line = line if line else []
+        self.fatal = damages >= target.hp
+
+    def __repr__(self):
+        return f"<Threat (unit: {self.unit.id}, damages={self.damages}, target={self.target.id}, fatal={self.fatal}, line={self.line})>"
+
 class Unit(BaseClass):
     TYPE_CULTIST = 0
     TYPE_CULT_LEADER = 1
@@ -148,6 +159,9 @@ class Unit(BaseClass):
         self.y = None
         self.owner = None
 
+        self.threats = []
+        self.threaten_by = []
+
     @property
     def pos(self):
         return self.x, self.y
@@ -314,6 +328,34 @@ class Grid(BaseClass):
                 if threat > self.threat[(x, y)]:
                     self.threat[(x, y)] = threat
 
+        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
+
+                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)
+
+        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
+
+                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)
+
     def compute_conversion_path(self):
         conversion_path = ConversionPath()
 
@@ -331,6 +373,7 @@ class Grid(BaseClass):
         self.update_index()
         self.update_threat_map()
         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])
@@ -338,10 +381,19 @@ class Grid(BaseClass):
     def build_actions(self):
         actions = Queue()
 
+        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}")
+            if u.threaten_by:
+                log(f"Threaten : {u.id} - {u.threaten_by}")
+
         # Leader take cover
         k0_protect_cult_leader = 30
         k_protect_threat_level = -5
-        k_cover_threat = 10
+        k_protect_worth_risk = 3
         k_cover_interest = -10
 
         if self.cult_leader:
@@ -351,19 +403,19 @@ class Grid(BaseClass):
                 covers = [n for n in self.neighbors(*self.cult_leader.pos) if self.can_move_on(self.cult_leader, n)]
 
                 for pos in covers:
-                    action = ActionMove(self.cult_leader, pos, f'take cover (t: {current_threat})')
-
                     interest = bool(self.conversion_path and self.conversion_path.steps and pos in [s.pos for s in
-                                                                                               self.conversion_path.steps])
+                                                                                                    self.conversion_path.steps])
 
                     priority = k0_protect_cult_leader
-                    priority += k_protect_threat_level * current_threat
-                    priority += k_cover_threat * self.threat[pos]
+                    priority += k_protect_threat_level * (current_threat - self.threat[pos])
                     priority += k_cover_interest * interest
+                    priority += k_protect_worth_risk  * (self.cult_leader.hp - (current_threat + 1))
 
+                    action = ActionMove(self.cult_leader, pos, f'take cover (current: {current_threat}, new: {self.threat[pos]})')
                     actions.put(priority, action)
 
         # Convert
+        k0_convert = 15
         k_convert_number = -10
         k_convert_distance = 10
         k_convert_danger = 20
@@ -373,7 +425,7 @@ class Grid(BaseClass):
             if path:
                 targets = [self.index[c] for c in path[-1].candidates]
 
-                priority = 0
+                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])
@@ -386,104 +438,87 @@ class Grid(BaseClass):
                 actions.put(priority, action)
 
         # Shoot opponent units
-        k_shoot_opponent_cultist = 8
-        k_shoot_opponent_leader = 4
-        k_shoot_movement_needed = 15
-        k0_shoot_in_danger = -10
-        k_shoot_sacrifice = 50
+        k0_shoot = 0
+        k_shoot_opponent_cultist = 6
+        k_shoot_opponent_leader = 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:
-            targets = {}
-            for u in self.opponent_units:
-                line_of_sight = self.line_of_sight(a.pos, u.pos)
-                if not len(line_of_sight):
-                    continue
-
-                if len(line_of_sight) <= u.SHOOTING_RANGE:
-                    targets[u] = line_of_sight
-                else:
-                    # la cible est hors de portée, la poursuivre si plus faible et sans soutien?
-                    # TODO: implémenter
-                    pass
-
-            if not targets:
+            if not a.threats:
                 continue
 
-            # Il est probable que l'unité se fera tirer dessus par l'ennemi le plus proche: quel résultat?
-            dist_to_nearest_target = min([len(line) for line in targets.values()])
-            damages_received = Unit.SHOOTING_MAX_DAMAGE - dist_to_nearest_target
-            unit_in_danger = damages_received >= a.hp or (len(targets) > 1 and (damages_received + 3) >= a.hp)
+            for threat in a.threats:
+                priority = k0_shoot
 
-            for u, line in targets.items():
-                dist = len(line)
-                damages = Unit.SHOOTING_MAX_DAMAGE - dist
-                target_die = damages >= u.hp
+                # 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]
 
-                priority = 0
+                if can_be_killed_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 unit_in_danger and not target_die:
-                    # run away!
-                    covers = [n for n in self.neighbors(*a.pos)
-                              if self.threat[n] < self.threat[a.pos] and self.can_move_on(a, n)]
+                    if covers and not worth_sacrificing:
+                        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):
+                                action = ActionMove(a, cover, f'run away!')
+                                priority += k0_runaway
+                                priority += k_runaway_threat * self.threat[cover]
+                                actions.put(priority, action)
+                                continue
 
-                    for pos in covers:
-                        action = ActionMove(a, pos, f'run away!')
-
-                        priority += k0_runaway
-                        priority += k_runaway_threat * self.threat[pos]
-
-                        actions.put(priority, action)
-                    continue
+                    log(f'<!> {a.id} seems lost')
 
-                elif unit_in_danger and target_die and len(targets) > 1:
-                    # bof, un échange, pas sûr que ça puisse être intéressant...
+                elif any(t for t in can_be_killed_by if t.unit != threat.target) and threat.fatal:
+                    # l'unité peut tuer la cible, mais sera probablement tuée par une autre unité ennemie
                     priority += k_shoot_sacrifice
 
-                k = k_shoot_opponent_leader if u is self.opponent_cult_leader else k_shoot_opponent_cultist
-                priority += k * dist
-                priority += k0_shoot_in_danger * unit_in_danger
+                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)
+
+                # 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)
 
-                action = ActionShoot(a, u, f'shoot from {a.pos} ({line})')
+                action = ActionShoot(a, threat.target, f'shoot from {a.pos} (damages: {threat.damages}, fatal: {threat.fatal}, line: {threat.line})')
 
                 actions.put(priority, action)
 
-        # Position
-        k0_position = 30
-        k_advantage = 40
+        # 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...
-        hp_min = 5
-
-        advantage = len(self.owned_units) > len(self.opponent_units) + len(self.neutrals)
+        position_hp_min = 5
 
         for a in self.allied_cultists:
-
-            if a.hp < hp_min:
+            if a.hp < position_hp_min:
                 continue
-
             if self.threat[a.pos]:
-                # l'unité est déjà dans une zone à risque
-                # TODO: envisager un retrait
+                # unit already under threat, let shoot action manage this
+                continue
+            if any(self.threat[n] for n in self.neighbors(*a.pos)):
+                # unit already on frontline
                 continue
 
-            else:
-                # 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, limit=1)
-                if not nearest_frontline:
-                    log(f"<!> {a.id} can not join nearest 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,
+                                              limit=1)
+            if not nearest_frontline:
+                log(f"<!> {a.id} can not join nearest frontline")
+                continue
 
-                path, target = nearest_frontline[0]
-                if path:
-                    # log(f"{a.id} - {path} - {target}")
-                    # already in place
-                    priority = k0_position
-                    priority += k_position_distance * len(path)
-                    priority += k_advantage * advantage
+            path, target = nearest_frontline[0]
+            if path:
+                # log(f"{a.id} - {path} - {target}")
+                priority = k0_position
+                priority += k_position_distance * len(path)
+                priority += k_position_advantage * advantage
 
-                    action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
-                    actions.put(priority, action)
+                action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
+                actions.put(priority, action)
 
         # Shoot neutral units:
         k_shoot_dangerous_neutral_threat = 2
@@ -492,7 +527,8 @@ class Grid(BaseClass):
 
         if self.opponent_cult_leader:
 
-            discovered_by_opponent = self.discover(self.opponent_cult_leader, key=lambda x: x in self.index and self.index[x].neutral, limit=3)
+            discovered_by_opponent = self.discover(self.opponent_cult_leader,
+                                                   key=lambda x: x in self.index and self.index[x].neutral, limit=3)
             neutrals_threaten = {}
             for path, target_pos in discovered_by_opponent:
                 if len(path) > dangerous_neutral_distance_limit:
@@ -504,7 +540,8 @@ class Grid(BaseClass):
             if self.cult_leader:
                 discovered_by_leader = self.discover(self.cult_leader, key=lambda x: x in neutrals_threaten, limit=3)
                 for path, target_pos in discovered_by_leader:
-                    if target_pos not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target_pos] < len(path):
+                    if target_pos not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[
+                        target_pos] < len(path):
                         threaten_neutrals_from_leader[target_pos] = len(path)
 
             log(f"Nearest from opp. leader: {neutrals_threaten}")
@@ -512,9 +549,11 @@ class Grid(BaseClass):
 
             lost_causes = {}
             for target, dist in neutrals_threaten.items():
-                if target not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target] <= neutrals_threaten[target]:
-                    lost_causes[target] = (dist - threaten_neutrals_from_leader.get(target, 0))  # la distance retenue est la différence entre la distance entre la cible
-                                                                                 # et le leader ennemi, et celle entre la cible et le leader allié
+                if target not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target] <= \
+                        neutrals_threaten[target]:
+                    lost_causes[target] = (dist - threaten_neutrals_from_leader.get(target,
+                                                                                    0))  # la distance retenue est la différence entre la distance entre la cible
+                    # et le leader ennemi, et celle entre la cible et le leader allié
 
             for a in self.allied_cultists:
                 for pos, dist in lost_causes.items():
@@ -538,10 +577,49 @@ class Grid(BaseClass):
                         # log(self.line_of_sight(a.pos, u.pos))
                         actions.put(priority, action)
 
-        # TODO: action 'take cover' pour les unités aussi
-        # TODO: action 'peace-keeping': tirer sur les neutres qu'on ne pourra pas convertir avant l'ennemi
-        # TODO: action 'intercept': une unité se place entre un tireur ennemi et le leader; en dernier recours
-        # TODO: action 'do nothing' : parfois, c'est la meilleure chose à faire
+        # Last hope: take risks
+        if not advantage and not actions:
+            k0_last_chance = 80
+            k_last_chance_danger = 5
+            last_chance_hp_min = 5
+
+            for a in self.allied_cultists:
+                if a.hp < last_chance_hp_min:
+                    continue
+
+                for n in self.neighbors(*a.pos):
+                    if not self.threat[n]:
+                        continue
+
+                    ongoing_fights = []
+                    for u in self.opponent_cultists:
+                        line_of_sight = self.line_of_sight(u.pos, n)
+                        if not line_of_sight:
+                            continue
+
+                        dist = self.manhattan(u.pos, n)
+                        if dist <= u.SHOOTING_RANGE:
+                            damages = u.SHOOTING_MAX_DAMAGE - dist
+
+                            ongoing_fights.append((u, damages))
+
+                    if any(d >= a.hp for _, d in ongoing_fights):
+                        # c'est du suicide: sortir revient à se prendre un coup fatal
+                        continue
+
+                    action = ActionMove(a, n, f"last hope, moving")
+
+                    priority = k0_last_chance
+                    priority += k_last_chance_danger * max(d for _, d in ongoing_fights)
+                    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: 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
+        # TODO: toujours le pbm des tirs supposés possibles mais non, ça coince quand le tir est en diagonale entre deux obstables; et autres pbm avec cet algo :(
+        # TODO: abandonner l'idée d'aller convertir si la menace est mortelle...
 
         return actions
 
@@ -754,7 +832,7 @@ class Grid(BaseClass):
                 if not self.can_move_on(unit, pos):
                     continue
 
-                moving_cost = 1
+                moving_cost = 1 + self.threat[pos]
 
                 matches = []
                 for n in self.neighbors(*pos):