Browse Source

add alpha discover methog

olinox14 3 years ago
parent
commit
b23177ec32
1 changed files with 320 additions and 38 deletions
  1. 320 38
      cultist_war/main.py

+ 320 - 38
cultist_war/main.py

@@ -1,6 +1,5 @@
 import heapq
 import heapq
 import sys
 import sys
-import math
 import time
 import time
 
 
 debug = True
 debug = True
@@ -23,6 +22,34 @@ class BaseClass:
         return f"<{self.__class__.__name__}: {self.__dict__}>"
         return f"<{self.__class__.__name__}: {self.__dict__}>"
 
 
 
 
+class Node(BaseClass):
+    def __init__(self, pos, path=None):
+        self.pos = pos
+        self.path = path or []
+
+
+class PathNode(tuple):
+    def __new__(cls, x, y, parent=None):
+        n = tuple.__new__(cls, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, c:{self.cost}>"
+
+
+class DiscoverNode(tuple):
+    def __new__(cls, x, y, ancestors=None):
+        n = tuple.__new__(cls, (x, y))
+        n.ancestors = ancestors if ancestors is not None else []
+        n.cost = 0
+        return n
+
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, c:{self.cost}>"
+
+
 class Queue(BaseClass):
 class Queue(BaseClass):
     def __init__(self):
     def __init__(self):
         self.items = []
         self.items = []
@@ -112,12 +139,13 @@ class ActionWait(Action):
 
 
 
 
 class ActionMove(Action):
 class ActionMove(Action):
-    def __init__(self, unit, pos):
+    def __init__(self, unit, pos, message=''):
         self.unit = unit
         self.unit = unit
         self.pos = pos
         self.pos = pos
+        self.message = message
 
 
     def __repr__(self):
     def __repr__(self):
-        return f"<ActionMove: {self.unit.id} to {self.pos}>"
+        return f"<ActionMove: {self.unit.id} to {self.pos} ({self.message})>"
 
 
     def exec(self):
     def exec(self):
         print(f"{self.unit.id} MOVE {self.pos[0]} {self.pos[1]}")
         print(f"{self.unit.id} MOVE {self.pos[0]} {self.pos[1]}")
@@ -151,12 +179,26 @@ class Grid(BaseClass):
     def __init__(self, width, height):
     def __init__(self, width, height):
         self.width = width
         self.width = width
         self.height = height
         self.height = height
+
+        self.cells = []
         self.obstacles = []
         self.obstacles = []
+        self._neighbors = {}
+
         self.index = {}
         self.index = {}
         self.units = {}
         self.units = {}
         self.round = 0
         self.round = 0
+        self.threat = {}
+        self.control = {}
+        self.heat_map = {}
+
+    def pre_compute(self):
+        self.cells = [(x, y) for x in range(self.width) for y in range(self.height)]
 
 
-    def prepare_round(self):
+        for x, y in self.cells:
+            self._neighbors[(x, y)] = [(xn, yn) for xn, yn in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if
+                                      0 <= xn < self.width and 0 <= yn < self.height]
+
+    def reinit_round(self):
         self.units = {}
         self.units = {}
 
 
     def update_unit(self, id_, type_, hp, x, y, owner):
     def update_unit(self, id_, type_, hp, x, y, owner):
@@ -167,13 +209,63 @@ class Grid(BaseClass):
         unit.y = y
         unit.y = y
         unit.owner = owner
         unit.owner = owner
 
 
-    def update_index(self):
+    def update_threat_map(self):
+        self.threat = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
+
+        for u in self.opponent_cultists():
+            shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
+            for x, y in shooting_zone:
+                dist = shooting_zone[(x, y)]
+
+                if not self.line_of_sight(u.pos, (x, y)):
+                    continue
+
+                threat = Unit.SHOOTING_RANGE + 1 - dist
+                if threat > self.threat[(x, y)]:
+                    self.threat[(x, y)] = threat
+
+    def update_control(self):
+        self.control = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
+
+        for u in self.allied_cultists():
+            shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
+            for x, y in shooting_zone:
+                dist = shooting_zone[(x, y)]
+
+                if not self.line_of_sight(u.pos, (x, y)):
+                    continue
+
+                control = Unit.SHOOTING_RANGE + 1 - dist
+                if control > self.control[(x, y)]:
+                    self.control[(x, y)] = control
+
+    def update_heat_map(self):
+        lines = []
+        self.heat_map = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
+
+        cult_leader = self.cult_leader()
+
+        for o in self.opponent_cultists():
+            if cult_leader:
+                lines += self.line(o.pos, cult_leader.pos)
+
+            for n in self.neutrals():
+                lines += self.line(o.pos, n.pos)
+
+        for pos in lines:
+            self.heat_map[pos] += 1
+
+    def update(self):
         self.index = {}
         self.index = {}
         for unit in self.units.values():
         for unit in self.units.values():
             self.index[(unit.x, unit.y)] = unit
             self.index[(unit.x, unit.y)] = unit
 
 
-    def own_cult_leader(self):
-        return next(u for u in self.units.values() if type(u) is CultLeader and u.owned)
+        self.update_threat_map()
+        self.update_control()
+        self.update_heat_map()
+
+    def cult_leader(self):
+        return next((u for u in self.units.values() if type(u) is CultLeader and u.owned), None)
 
 
     def allied_cultists(self):
     def allied_cultists(self):
         return [u for u in self.units.values() if type(u) is not CultLeader and u.owned]
         return [u for u in self.units.values() if type(u) is not CultLeader and u.owned]
@@ -197,41 +289,86 @@ class Grid(BaseClass):
         k_convert_danger = 30
         k_convert_danger = 30
         k_shoot_opponent_cultist = 20
         k_shoot_opponent_cultist = 20
         k_shoot_opponent_cult_leader = 10
         k_shoot_opponent_cult_leader = 10
+        k0_protect_cult_leader = 10
+        k0_position = 50
+        k_position_distance = 10
+        k_position_heat = -5
+
+        cult_leader = self.cult_leader()
+
+        # Conversion des neutres
+        if cult_leader:
+            paths = self.discover(
+                cult_leader.pos,
+                key=(lambda pos: pos in self.index and self.index[pos].neutral),
+                limit=5
+            )
+            for path in paths:
+                log(path)
+
+            for path in paths:
+                target = self.index[path[-1]]
+
+                priority = 0
+                priority += k_convert_neutrals * len(path)
+                priority += k_convert_danger * sum([self.threat[pos] for pos in path])
+
+                if target in self.neighbors(*cult_leader.pos):
+                    action = ActionConvert(cult_leader, target)
+                else:
+                    action = ActionMove(cult_leader, path[0], f'go convert {target.id}')
+                actions.put(priority, action)
+
+        # Attaque d'unités ennemies
+        for a in self.allied_cultists():
+            for u in self.opponent_cultists():
+                shooting_distance = self.shooting_distance(a.pos, u.pos)
+                if shooting_distance and shooting_distance < u.SHOOTING_RANGE:
+                    action = ActionShoot(a, u)
 
 
-        k_limit = 200
+                    priority = (k_shoot_opponent_cult_leader if type(
+                        u) is CultLeader else k_shoot_opponent_cultist) * shooting_distance
 
 
-        cult_leader = self.own_cult_leader()
+                    actions.put(priority, action)
 
 
-        for n in self.neutrals():
-            action = ActionConvert(cult_leader, n)
+        # Position
+        for a in self.allied_cultists():
+            # on garde les trois points les plus chauds
+            hot_spots = sorted(self.heat_map.items(), key=lambda p: p[1], reverse=True)[:3]
 
 
-            distance = self.manhattan(cult_leader.pos, n.pos)
-            danger = 0
-            for u in self.opponent_cultists():
-                fire_dist = self.fire_dist(cult_leader.pos, u.pos)
-                if fire_dist < u.SHOOTING_RANGE:
-                    danger += (u.SHOOTING_MAX_DAMAGE - fire_dist)
+            for pos, heat in hot_spots:
+                action = ActionMove(a, pos)
 
 
-            priority = k_convert_neutrals * distance + k_convert_danger * danger
+                path = self.path(a.pos, pos)
 
 
-            if priority > k_limit:
-                continue
+                safe_path = []
+                for step in path:
+                    if self.threat[step] > 1:
+                        break
+                    safe_path.append(step)
 
 
-            actions.put(priority, action)
+                if not safe_path:
+                    continue
 
 
-        for a in self.allied_cultists():
-            for u in self.opponent_cultists():
-                fire_dist = self.fire_dist(a.pos, u.pos)
-                if fire_dist < u.SHOOTING_RANGE:
-                    action = ActionShoot(a, u)
+                priority = k0_position
+                priority += k_position_heat * heat
+                priority += k_position_distance * len(path)
 
 
-                    priority = (k_shoot_opponent_cult_leader if type(
-                        u) is CultLeader else k_shoot_opponent_cultist) * fire_dist
+                actions.put(priority, action)
 
 
-                    if priority > k_limit:
-                        continue
+        # Mise en sécurité du chef
+        if cult_leader:
+            current_threat = self.threat[cult_leader.pos]
+            if current_threat:
+                target = min(
+                    [n for n in self.neighbors(*cult_leader.pos) if self.can_move_on(n)],
+                    key=lambda x: self.threat[x]
+                )
 
 
-                    actions.put(priority, action)
+                action = ActionMove(cult_leader, target)
+                priority = k0_protect_cult_leader
+
+                actions.put(priority, action)
 
 
         return actions
         return actions
 
 
@@ -242,7 +379,15 @@ class Grid(BaseClass):
         return self.in_grid(pos) and pos not in self.obstacles + list(self.index.values())
         return self.in_grid(pos) and pos not in self.obstacles + list(self.index.values())
 
 
     def can_move_on(self, pos):
     def can_move_on(self, pos):
-        return self.in_grid(pos) and pos not in self.obstacles + list(self.index.values())
+        return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
+
+    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):
+            return -1
+        return 1 + self.threat[pos]
 
 
     @staticmethod
     @staticmethod
     def manhattan(from_, to_):
     def manhattan(from_, to_):
@@ -250,6 +395,21 @@ class Grid(BaseClass):
         xb, yb = to_
         xb, yb = to_
         return abs(xa - xb) + abs(ya - yb)
         return abs(xa - xb) + abs(ya - yb)
 
 
+    def neighbors(self, x, y):
+        return self._neighbors[(x, y)]
+
+    def zone(self, pos, radius):
+        x0, y0 = pos
+        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
+
+        return zone
+
     @classmethod
     @classmethod
     def line(cls, from_, to_):
     def line(cls, from_, to_):
         """ Implementation of bresenham's algorithm """
         """ Implementation of bresenham's algorithm """
@@ -290,31 +450,153 @@ class Grid(BaseClass):
             result.reverse()
             result.reverse()
         return result
         return result
 
 
-    def fire_line(self, from_, to_):
+    def line_of_sight(self, from_, to_):
         line = self.line(from_, to_)
         line = self.line(from_, to_)
         return line if all(self.can_see_trough(c) for c in line) else []
         return line if all(self.can_see_trough(c) for c in line) else []
 
 
-    def fire_dist(self, from_, to_):
-        return len(self.fire_line(from_, to_))
+    def shooting_distance(self, from_, to_):
+        return len(self.line_of_sight(from_, to_))
+
+    def path(self, start, target):
+        nodes = Queue()
+        its, break_on = 0, 300
+
+        origin = PathNode(*start)
+        nodes.put(0, origin)
+
+        while nodes:
+            current = nodes.get()
+
+            if current == target:
+                path = []
+                previous = current
+                while previous:
+                    if previous != start:
+                        path.insert(0, previous)
+                    previous = previous.parent
+                return path
+
+            neighbors = self.neighbors(*current)
+
+            for x, y in neighbors:
+                its += 1
+                if its > break_on:
+                    log("<!> pathfinding broken")
+                    return None
+
+                if (x, y) == current.parent:
+                    continue
+
+                if not self.can_move_on((x, y)):
+                    continue
+
+                moving_cost = self.moving_cost((x, y))
+
+                cost = current.cost + moving_cost
+                priority = cost + 10 * Grid.manhattan((x, y), target)
+
+                node = PathNode(x, y, current)
+                node.cost = cost
+                nodes.put(priority, node)
+
+        return None
+
+    def discover(self, start, key, limit=5):
+        paths = []
+        found = []
+
+        nodes = Queue()
+        its, break_on = 0, 2000
+
+        origin = DiscoverNode(*start)
+        nodes.put(0, origin)
+
+        while nodes:
+            current = nodes.get()
+
+            if current not in found and current != start and key(tuple(current)):
+                path = []
+                previous = current
+                while previous.ancestors:
+                    if previous != start:
+                        path.insert(0, previous)
+                    previous = previous.ancestors[-1]
+                found.append(path[-1])
+                paths.append(path)
+
+                if len(paths) >= limit:
+                    return paths
+
+            neighbors = self.neighbors(*current)
+
+            for x, y in neighbors:
+                its += 1
+                if its > break_on:
+                    log("<!> discovering ended earlier than expected")
+                    return paths
+
+                if (x, y) in current.ancestors:
+                    continue
+
+                if not self.can_discover((x, y)):
+                    continue
+
+                moving_cost = self.moving_cost((x, y))
+
+                cost = current.cost + moving_cost
+                priority = cost + Grid.manhattan((x, y), start)
+
+                ancestors = current.ancestors + [current]
+
+                node = DiscoverNode(x, y, ancestors)
+                node.cost = cost
+                nodes.put(priority, node)
+
+        return paths
+
+    def _repr_cell(self, pos):
+        # return f"{self.control[pos]}/{self.threat[pos]}"
+        # return self.heat_map[pos]
+
+        if pos in self.obstacles:
+            return "X"
+
+        unit = self.index.get(pos, None)
+        if type(unit) is CultLeader:
+            return "C"
+        elif unit is None:
+            return "."
+        else:
+            return "U"
+
+    def graph(self):
+        return "\n".join(
+            ["|".join([str(self._repr_cell((x, y))) for x in range(self.width)]) for y in range(self.height)])
 
 
 
 
 # Create grid
 # Create grid
 GRID = Grid(*[int(i) for i in input().split()])
 GRID = Grid(*[int(i) for i in input().split()])
-GRID.obstacles = [(i, j) for i in range(GRID.height) for j, val in enumerate(input()) if val == 'x']
+obstacles_input = [input() for y in range(GRID.height)]
+GRID.obstacles = [(x, y) for y, row in enumerate(obstacles_input) for x, val in enumerate(row) if val == 'x']
+
+GRID.pre_compute()
 
 
 while 1:
 while 1:
     # TODO: prendre en compte le terrain dans la ligne de visée et les déplacements
     # TODO: prendre en compte le terrain dans la ligne de visée et les déplacements
+    log(f"start round {GRID.round}")
 
 
-    GRID.prepare_round()
+    GRID.reinit_round()
     for _ in range(int(input())):
     for _ in range(int(input())):
         GRID.update_unit(*[int(j) for j in input().split()])
         GRID.update_unit(*[int(j) for j in input().split()])
-    GRID.update_index()
+    GRID.update()
 
 
     actions = GRID.list_actions()
     actions = GRID.list_actions()
 
 
     for action in actions.items:
     for action in actions.items:
         log(f"* {action}")
         log(f"* {action}")
 
 
+    # print("\n" + GRID.graph(), file=sys.stderr)
+
     try:
     try:
         action = actions.get()
         action = actions.get()
     except IndexError:
     except IndexError: