浏览代码

new chained pathfinder apparently operational

olinox14 3 年之前
父节点
当前提交
aae4ddd38e
共有 2 个文件被更改,包括 208 次插入82 次删除
  1. 133 66
      cultist_war/main.py
  2. 75 16
      cultist_war/pathfinder.py

+ 133 - 66
cultist_war/main.py

@@ -40,15 +40,15 @@ class PathNode(tuple):
 
 
 class DiscoveryNode(tuple):
-    def __new__(cls, x, y, ancestors=None, matches=None):
+    def __new__(cls, x, y, cost=0, ancestors=None, matches=None):
         n = tuple.__new__(cls, (x, y))
+        n.cost = cost
         n.ancestors = ancestors if ancestors is not None else []
-        n.cost = 0
         n.matches = matches if matches is not None else []
         return n
 
     def __repr__(self):
-        return f"<{self[0]}, {self[1]}, c:{self.cost}>"
+        return f"<{self[0]}, {self[1]}>"
 
 
 class Queue(BaseClass):
@@ -76,6 +76,48 @@ class Queue(BaseClass):
         return heapq.heappop(self.items)
 
 
+class ConversionStep:
+    def __init__(self):
+        self.pos = None
+        self.candidates = []
+
+    def __repr__(self):
+        return f"<{self.pos}, c:{self.candidates}>"
+
+
+class ConversionPath:
+    def __init__(self):
+        self.steps = []
+
+    def __repr__(self):
+        return f"<{self.steps}>"
+
+    @classmethod
+    def make_from_discovery_node(cls, node):
+        nodes = node.ancestors + [node]
+        path = cls()
+        found = []
+
+        for node in nodes:
+            step = ConversionStep()
+            step.pos = tuple(node)
+            for m in node.matches:
+                if m in found:
+                    continue
+                step.candidates.append(m)
+                found.append(m)
+            path.steps.append(step)
+        return path
+
+    def next_candidate(self):
+        path = []
+        for step in self.steps:
+            path.append(step)
+            if step.candidates:
+                return path
+        return None
+
+
 class Player(BaseClass):
     def __init__(self, id_):
         self.id = id_
@@ -187,7 +229,7 @@ class Grid(BaseClass):
 
         self.index = {}
         self.units = {}
-        self.round = 0
+        self.round = 1
         self.threat = {}
         self.control = {}
         self.heat_map = {}
@@ -301,14 +343,15 @@ class Grid(BaseClass):
     def list_actions(self):
         actions = Queue()
 
-        k_convert_neutrals = 10
+        k_convert_number = -10
+        k_convert_distance = 10
         k_convert_danger = 30
         k_shoot_opponent_cultist = 10
         k_shoot_opponent_cult_leader = 5
         k0_protect_cult_leader = 30
         k_protect_threat_level = -5
         k_cover_threat = 10
-        k_cover_interest = -3
+        k_cover_interest = -10
         k0_position = 50
         k_position_distance = 10
         k_position_heat = -2
@@ -318,31 +361,34 @@ class Grid(BaseClass):
         cult_leader = self.cult_leader()
         opponent_cult_leader = self.opponent_cult_leader()
 
-        log(self.obstacles)
+        log(self.obstacles + [u.pos for u in self.allied_cultists()])
         log([n.pos for n in self.neutrals()])
 
         # compute conversion paths
-        conversion_paths = []
+        conversion_path = []
         if cult_leader and self.neutrals():
-            conversion_paths = self.discover(
-                cult_leader.pos,
+            conversion_path = self.get_conversion_path(
+                cult_leader,
                 key=(lambda pos: pos in self.index and self.index[pos].neutral),
-                limit=min(5, len(self.neutrals()))
+                limit=min(4, len(self.neutrals()))
             )
+            log(conversion_path)
 
         # Conversion des neutres
-        if cult_leader:
-            for path, target_pos in conversion_paths:
-                target = self.index[target_pos]
+        if cult_leader and conversion_path:
+            path = conversion_path.next_candidate()
+            if path:
+                targets = [self.index[c] for c in path[-1].candidates]
 
                 priority = 0
-                priority += k_convert_neutrals * len(path)
-                priority += k_convert_danger * sum([self.threat[pos] for pos in path])
+                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])
 
-                if target_pos in self.neighbors(*cult_leader.pos):
-                    action = ActionConvert(cult_leader, target)
+                if len(path) == 1:
+                    action = ActionConvert(cult_leader, targets[0])
                 else:
-                    action = ActionMove(cult_leader, path[0], f'go convert {target.id}')
+                    action = ActionMove(cult_leader, path[1].pos, f'go convert {",".join([str(t.id) for t in targets])}')
                 actions.put(priority, action)
 
         # Attaque d'unités ennemies
@@ -368,20 +414,19 @@ class Grid(BaseClass):
             # on garde les trois points les plus chauds
             hot_spots = sorted(self.heat_map.items(), key=lambda p: p[1], reverse=True)[:3]
 
-            results = self.discover(a.pos, key=lambda x: x in [s[0] for s in hot_spots])
-
-            for path, target_pos in results:
-                if not path:
-                    break
+            # results = self.discover(a.pos, key=lambda x: x in [s[0] for s in hot_spots])
 
-                heat = self.heat_map[target_pos]
+            # for path, target_pos in results:
+            #     if not path:
+            #         break
 
+            for spot_pos, heat in hot_spots:
                 priority = k0_position
                 priority += k_position_heat * heat
-                priority += k_position_distance * len(path)
-                priority += k_position_danger * sum(self.threat[p] for p in path)
+                priority += k_position_distance * self.manhattan(a.pos, spot_pos)
+                priority += k_position_danger * self.threat[spot_pos]
 
-                action = ActionMove(a, path[0], f'pos on {target_pos}')
+                action = ActionMove(a, spot_pos, f'pos on {spot_pos}')
 
                 actions.put(priority, action)
 
@@ -389,13 +434,12 @@ class Grid(BaseClass):
         if cult_leader:
             current_threat = self.threat[cult_leader.pos]
             if current_threat:
-                covers = [n for n in self.neighbors(*cult_leader.pos) if self.can_move_on(n)]
+                covers = [n for n in self.neighbors(*cult_leader.pos) if self.can_move_on(cult_leader, n)]
 
                 for pos in covers:
                     action = ActionMove(cult_leader, pos, 'take cover')
 
-                    # interest is the number of conversion paths that start with this pos
-                    interest = len([p for p in conversion_paths if p[0] and p[0][0] == pos])
+                    interest = conversion_path and conversion_path.steps and conversion_path.steps[0].pos == pos
 
                     priority = k0_protect_cult_leader
                     priority += k_protect_threat_level * current_threat
@@ -412,14 +456,14 @@ class Grid(BaseClass):
     def can_see_trough(self, pos):
         return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
 
-    def can_move_on(self, pos):
-        return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
+    def can_move_on(self, unit, pos):
+        return self.in_grid(pos) and pos not in self.obstacles and (pos not in self.index or self.index[pos] is unit)
 
     def can_discover(self, pos):
         return self.in_grid(pos) and pos not in self.obstacles
 
-    def moving_cost(self, pos):
-        if not self.can_move_on(pos):
+    def moving_cost(self, unit, pos):
+        if not self.can_move_on(unit, pos):
             return -1
         return 1 + self.threat[pos]
 
@@ -491,11 +535,11 @@ class Grid(BaseClass):
     def shooting_distance(self, from_, to_):
         return len(self.line_of_sight(from_, to_))
 
-    def path(self, start, target):
+    def path(self, unit, target):
         nodes = Queue()
         its, break_on = 0, 400
 
-        origin = PathNode(*start)
+        origin = PathNode(*unit.pos)
         nodes.put(0, origin)
 
         while nodes:
@@ -505,7 +549,7 @@ class Grid(BaseClass):
                 path = []
                 previous = current
                 while previous:
-                    if previous != start:
+                    if previous != unit.pos:
                         path.insert(0, previous)
                     previous = previous.parent
                 return path
@@ -521,10 +565,10 @@ class Grid(BaseClass):
                 if (x, y) == current.parent:
                     continue
 
-                if not self.can_move_on((x, y)):
+                if not self.can_move_on(unit, (x, y)):
                     continue
 
-                moving_cost = self.moving_cost((x, y))
+                moving_cost = self.moving_cost(unit, (x, y))
 
                 cost = current.cost + moving_cost
                 priority = cost + 10 * Grid.manhattan((x, y), target)
@@ -535,12 +579,12 @@ class Grid(BaseClass):
 
         return None
 
-    def discover(self, start, key, limit=5):
+    def discover(self, unit, key, limit=5):
         paths = []
 
         nodes = []
         its, break_on = 0, 2000
-        origin = DiscoveryNode(*start)
+        origin = DiscoveryNode(*unit.pos)
 
         nodes.append(origin)
 
@@ -566,56 +610,78 @@ class Grid(BaseClass):
                 if pos in current.ancestors:
                     continue
 
-                if not self.can_move_on(pos):
+                if not self.can_move_on(unit, pos):
                     continue
 
                 node = DiscoveryNode(*pos, current.ancestors + [current])
                 nodes.append(node)
         return paths
 
-    def discover_multiple(self, start, key, limit=5):
+    def get_conversion_path(self, unit, 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()
-        its, break_on = 0, 2000
-        origin = DiscoveryNode(*start)
+        winners = Queue()
+        its, break_on = 0, 1000
+        origin = DiscoveryNode(*unit.pos)
+
+        abandon_at = 120  # number of paths explored
 
         nodes.put(0, origin)
 
+        for n in self.neighbors(*unit.pos):
+            if key(n):
+                origin.matches.append(n)
+
         while nodes:
+            its += 1
+            if its > break_on:
+                log(f"<!> get_conversion_path broke")
+                break
+
+            if len(nodes.items) > abandon_at:
+                log("> get_conversion_path early exit")
+                break
+
             current = nodes.get()
 
-            neighbors = self.neighbors(*current)
+            for pos in self.neighbors(*current):
+                if not self.can_move_on(unit, pos):
+                    continue
 
-            for pos in neighbors:
-                its += 1
-                if its > break_on:
-                    log(f"<!> discovery broke")
-                    break
+                moving_cost = 1
 
-                if pos not in current.matches and key(pos):
-                    current.matches.append(pos)
-                    if len(current.matches) >= limit:
-                        break
+                matches = []
+                for n in self.neighbors(*pos):
+                    if n not in current.matches and key(n):
+                        matches.append(n)
 
-                if not self.can_move_on(pos):
-                    continue
+                cost = current.cost + moving_cost
+                priority = 1000 * cost - (2000 * (len(current.matches) + len(matches)))
+                priority += 100 * len(
+                    [a for a in current.ancestors if a == pos])  # décourage de revenir à une case visitée
 
                 node = DiscoveryNode(
-                    *pos,
+                    pos[0],
+                    pos[1],
+                    cost,
                     current.ancestors + [current],
-                    current.ancestors[-1].matches if current.ancestors else None
+                    current.matches + matches
                 )
 
-                # on priorise le plus haut ratio matches / longueur de chemin (donc le plus bas inverse, pour la Queue).
-                priority = (10 * (len(node.ancestors) + 1)) // (1 + len(node.matches))
+                if matches:
+                    winners.put(40 * node.cost - 100 * len(node.matches), node)
 
                 nodes.put(priority, node)
 
             if len(current.matches) >= limit or its > break_on:
                 break
 
-        best_node = nodes.get()
-        path = best_node.ancestors[:-1:-1] + [best_node]
-        return path, best_node.matches
+        try:
+            best_node = winners.get()
+        except IndexError:
+            best_node = nodes.get()
+        return ConversionPath.make_from_discovery_node(best_node)
 
     def _repr_cell(self, pos):
         # return f"{self.control[pos]}/{self.threat[pos]}"
@@ -654,11 +720,12 @@ while 1:
     # TODO: ajouter une action "s'interposer" où une unité s'interpose entre le leader et un ennemi ; à mettre en balance avec le 'take cover'
     # TODO: remplacer le discover par un algo qui cherche le plus court chemin pour relier tous les neutres les uns après les autres
 
-    log(f"start round {GRID.round}")
 
     GRID.reinit_round()
     for _ in range(int(input())):
         GRID.update_unit(*[int(j) for j in input().split()])
+
+    log(f"start round {GRID.round}")
     GRID.update()
 
     actions = GRID.list_actions()

+ 75 - 16
cultist_war/pathfinder.py

@@ -45,10 +45,9 @@ def log(*msg):
     print("{} - ".format(str(time.time() - t0)[:5]), *msg)
 
 
-start = (0, 3)
-obstacles = [(0, 0), (1, 0), (3, 0), (9, 0), (11, 0), (12, 0), (0, 1), (1, 1), (5, 1), (7, 1), (11, 1), (12, 1), (3, 3), (9, 3)]
-neutrals = [(1, 4), (2, 3), (1, 5), (3, 5), (2, 6), (4, 1), (11, 4), (10, 3), (11, 5), (9, 5), (10, 6), (8, 1)]
-
+start = (1, 3)
+obstacles = [(0, 0), (1, 0), (3, 0), (9, 0), (11, 0), (12, 0), (0, 1), (1, 1), (5, 1), (7, 1), (11, 1), (12, 1), (3, 3), (9, 3), (1, 4), (2, 3)]
+neutrals = [(1, 5), (3, 4), (2, 6), (4, 2), (10, 2), (12, 5), (9, 5), (10, 6), (8, 2)]
 width = 13
 height = 7
 
@@ -62,24 +61,77 @@ def can_move_on(pos):
     return pos not in obstacles and pos not in neutrals
 
 
+class ConversionStep:
+    def __init__(self):
+        self.pos = None
+        self.candidates = []
+
+    def __repr__(self):
+        return f"<{self.pos}, c:{self.candidates}>"
+
+
+class ConversionPath:
+    def __init__(self):
+        self.steps = []
+
+    def __repr__(self):
+        return f"<{self.steps}>"
+
+    @classmethod
+    def make_from_discovery_node(cls, node):
+        nodes = node.ancestors + [node]
+        path = cls()
+        found = []
+
+        for node in nodes:
+            step = ConversionStep()
+            step.pos = tuple(node)
+            for m in node.matches:
+                if m in found:
+                    continue
+                step.candidates.append(m)
+                found.append(m)
+            path.steps.append(step)
+        return path
+
+    def path_to_next_candidate(self):
+        path = []
+        for step in self.steps:
+            path.append(step)
+            if step.candidates:
+                return path
+        return None
+
+
 def discover_multiple(start, key, limit=5):
     nodes = Queue()
-    its, break_on = 0, 20000
+    winners = Queue()
+    its, break_on = 0, 1000
     origin = DiscoveryNode(*start)
 
+    abandon_at = 200  # number of paths explored
+
     nodes.put(0, origin)
 
+    for n in get_neighbors(*start):
+        if key(n):
+            origin.matches.append(n)
+
     while nodes:
+        its += 1
+        if its > break_on:
+            log(f"<!> discovery broke")
+            break
+
+        if len(nodes.items) > abandon_at:
+            log("<!> get_conversion_path abandoned")
+            break
+
         current = nodes.get()
 
         neighbors = get_neighbors(*current)
 
         for pos in neighbors:
-            its += 1
-            if its > break_on:
-                log(f"<!> discovery broke")
-                break
-
             if not can_move_on(pos):
                 continue
 
@@ -91,8 +143,8 @@ def discover_multiple(start, key, limit=5):
                     matches.append(n)
 
             cost = current.cost + moving_cost
-            priority = 100 * cost - (200 * (len(current.matches) + len(matches)))
-            priority += 10 * len([a for a in current.ancestors if a == pos])  # décourage de revenir à une case visitée
+            priority = 1000 * cost - (2000 * (len(current.matches) + len(matches)))
+            priority += 100 * len([a for a in current.ancestors if a == pos])  # décourage de revenir à une case visitée
 
             node = DiscoveryNode(
                 pos[0],
@@ -102,18 +154,25 @@ def discover_multiple(start, key, limit=5):
                 current.matches + matches
             )
 
+            if matches:
+                winners.put(40 * node.cost - 100 * len(node.matches), node)
+
             nodes.put(priority, node)
 
         if len(current.matches) >= limit or its > break_on:
             break
 
-    best_node = nodes.get()
-    path = best_node.ancestors[1:] + [best_node]
-    return path, best_node.matches
+    # for p, w in winners.items:
+    #     log(f'* {p} - {w.ancestors + [w]} - {w.matches}')
+
+    best_node = winners.get()
+    path = ConversionPath.make_from_discovery_node(best_node)
+    log(its)
+    return path
 
 
 log("start")
 
-res = discover_multiple(start, key=lambda x: x in neutrals, limit=6)
+res = discover_multiple(start, key=lambda x: x in neutrals, limit=min(4, len(neutrals)))
 
 log(res)