Browse Source

add cultist wars base

olinox14 3 years ago
parent
commit
8c3fc550da

+ 3 - 0
.idea/.gitignore

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

+ 8 - 0
.idea/cig.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="jdk" jdkName="Python 3.9" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 6 - 0
.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
.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
.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/cig.iml" filepath="$PROJECT_DIR$/.idea/cig.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.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>

+ 190 - 0
cultist_war/main.py

@@ -0,0 +1,190 @@
+import sys
+import math
+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)
+
+
+def time_to(total, step):
+    """ number of steps to reach total """
+    return total // step + (1 if total % step > 0 else 0)
+
+
+class BaseClass:
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+
+class Player(BaseClass):
+    def __init__(self, id_):
+        self.id = id_
+
+
+ME = Player(int(input()))  # Input gives the player id: 0 plays first
+OPPONENT = Player(1 - ME.id)
+
+PLAYERS_INDEX = {p.id: p for p in [ME, OPPONENT]}
+PLAYERS_ORDER = sorted([ME, OPPONENT], key=lambda p: p.id)
+
+
+class Unit(BaseClass):
+    TYPE_CULTIST = 0
+    TYPE_CULT_LEADER = 1
+
+    OWNER_0 = 0
+    OWNER_1 = 1
+    NO_OWNER = 2
+
+    SHOOTING_RANGE = 6
+    SHOOTING_MAX_DAMAGE = 7
+
+    def __init__(self, id_):
+        self.id = id_
+        self.hp = 10
+        self.x = None
+        self.y = None
+        self.owner = None
+
+    @property
+    def owned(self):
+        return self.owner == ME.id
+
+    @property
+    def opponent(self):
+        return self.owner == OPPONENT.id
+
+    @property
+    def neutral(self):
+        return self.owner == self.NO_OWNER
+
+
+class CultLeader(Unit):
+    pass
+
+
+class Grid(BaseClass):
+    def __init__(self, width, height):
+        self.width = width
+        self.height = height
+        self.obstacles = []
+        self.index = {}
+        self.units = {}
+        self.round = 0
+
+    def update_unit(self, id_, type_, hp, x, y, owner):
+        if id_ not in self.units:
+            self.units[id_] = Unit(id_) if type_ != Unit.TYPE_CULT_LEADER else CultLeader(id_)
+
+        unit = self.units[id_]
+        unit.hp = hp
+        unit.x = x
+        unit.y = y
+        unit.owner = owner
+
+    def update_index(self):
+        self.index = {}
+        for unit in self.units:
+            self.index[(unit.x, unit.y)] = unit
+
+    def in_grid(self, pos):
+        return 0 <= pos[0] < self.width and 0 <= pos[1] < self.height
+
+    def can_see_trough(self, pos):
+        return self.in_grid(pos) and pos not in self.obstacles + list(self.index.values())
+
+    def can_move_on(self, pos):
+        return self.in_grid(pos) and pos not in self.obstacles + list(self.index.values())
+
+    @staticmethod
+    def manhattan(from_, to_):
+        xa, ya = from_
+        xb, yb = to_
+        return abs(xa - xb) + abs(ya - yb)
+
+    @classmethod
+    def line(cls, from_, to_):
+        """ Implementation of bresenham's algorithm """
+        xa, ya = from_
+        xb, yb = to_
+
+        if (xa, ya) == (xb, yb):
+            return [(xa, ya)]
+
+        # diagonal symmetry
+        vertically_oriented = (abs(yb - ya) > abs(xb - xa))
+        if vertically_oriented:
+            ya, xa, yb, xb = xa, ya, xb, yb
+
+        # horizontal symmetry
+        reversed_sym = (xa > xb)
+        if reversed_sym:
+            xb, yb, xa, ya = xa, ya, xb, yb
+
+        # angle
+        dx, dy = xb - xa, yb - ya
+        alpha = (abs(dy) / dx)
+
+        offset = 0.0
+        step = 1 if dy > 0 else -1
+
+        result = []
+        y_ = ya
+        for x_ in range(xa, xb + 1):
+            result.append((y_, x_) if vertically_oriented else (x_, y_))
+
+            offset += alpha
+            if offset > 0.5:
+                y_ += step
+                offset -= 1.0
+
+        if reversed_sym:
+            result.reverse()
+        return result
+
+
+# Create grid
+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']
+
+
+class Action(BaseClass):
+    pass
+
+
+class ActionWait(Action):
+    def exec(self):
+        print("WAIT")
+
+
+class ActionMove(Action):
+    def exec(self, id_, pos):
+        x, y = pos
+        print(f"{id_} MOVE {x} {y}")
+
+
+class ActionShoot(Action):
+    def exec(self, id_, target_id):
+        print(f"{id_} SHOOT {target_id}")
+
+
+class ActionConvert(Action):
+    def exec(self, id_, target_id):
+        print(f"{id_} CONVERT {target_id}")
+
+
+
+while 1:
+    for _ in range(int(input())):
+        GRID.update_unit(*[int(j) for j in input().split()])
+    GRID.update_index()
+
+    log(GRID.units)
+
+    GRID.round += 1

+ 23 - 0
cultist_war/notes.md

@@ -0,0 +1,23 @@
+# Notes
+
+Jouer façon echecs: prendre le centre puis protéger le "roi"
+
+
+Actions possibles :
+
+* Convertir un neutre
+* Attaquer le leader ennemi
+* Attaquer un cultiste ennemi
+* Convertir un cultiste ennemi
+* Mettre le leader à l'abri
+* Se déplacer pour occuper le centre
+* Déplacer le leader vers des "proies" potentielles
+
+
+Contraintes :
+
+* maintenir le leader en sécurité, donc autant que possible hors de portée des cultistes ennemis 
+
+Divers:
+
+* si on est 2e à jouer, une unité neutre a bougé de manière aléatoire

+ 0 - 55
i_n_f/script.py

@@ -688,61 +688,6 @@ class Grid(Base):
             self.cells[pivot].pivot_for = pivot_for
             self.cells[pivot].pivot_value = sum([1 + grid[p].get_unit_level() for p in pivot_for])
             
-    def update_pivot_for(self, player_id):
-        start = self.get_hq(player_id).pos
-        start_node = Node(start)
-        
-        buffer = [start_node]
-        nodes = {start_node}
-        
-        its, breakdown = 0, 1200
-        
-        ignored = [p for p, c in self.cells.items() if c.is_active_owner(player_id) and 
-                   len([n for n in self.neighbors(*p, diags=True) if self._active_owned(n, player_id)]) == 8]
-        
-        while buffer:
-            new_buffer = []
-            for node in buffer:
-                its += 1
-                if its > breakdown:
-                    log("pivots: broken")
-                    return
-                if node.pos in ignored:
-                    continue
-                for n in self.neighbors(*node.pos):
-                    if not n in node.path and self._active_owned(n, player_id):
-                        new_node = Node(n, node.path + [node.pos])
-                        nodes.add(new_node)
-                        new_buffer.append(new_node)
-            buffer = new_buffer
-        
-        paths_to = {}
-        for node in nodes:
-            if not node.pos in paths_to:
-                paths_to[node.pos] = []
-            paths_to[node.pos].append(node.path)
-    
-        pivots = {}
-        for candidate in paths_to:
-            if candidate == start:
-                continue
-            for p, paths in paths_to.items():
-                if not paths or not paths[0] or p in ignored:
-                    continue
-                if all(candidate in path for path in paths):
-                    if not candidate in pivots:
-                        pivots[candidate] = []
-                    pivots[candidate].append(p)
-        
-        log("player {} pivots: {}".format(player_id, {k: len(v) for k, v in pivots.items()}))
-        
-        for pivot, pivot_for in pivots.items():
-            self.cells[pivot].pivot_for = pivot_for
-            
-    def update_pivots(self):
-        self.update_pivot_for(ME)
-        self.update_pivot_for(OPPONENT)
-    
     def update_threats(self):
         # 1 + max number of units opponents can produce in one turn
         self.threat_level = 2 + (opponent.gold + opponent.income) // Unit.cost[1]

+ 475 - 0
labyrinth/script.py

@@ -0,0 +1,475 @@
+import heapq
+import sys
+import time
+
+
+t0 = time.time()
+def log(*msg):
+    print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr)
+
+class Queue():
+    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, item, priority):
+        heapq.heappush(self.items, (priority, item))
+
+    def fput(self, item, priority):
+        while priority in [p for p, _ in self.items]:
+            priority += 1
+        self.put(item, priority)
+
+    def get(self):
+        return heapq.heappop(self.items)[1]
+
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+    
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, c:{self.cost}>"
+
+class Waypoint(tuple):
+    def __new__(self, x, y, neighbors=None):
+        n = tuple.__new__(self, (x, y))
+        n.neighbors = neighbors or []
+        return n
+
+    def __repr__(self):
+        return f"<({self[0]},{self[1]}), n:{self.neighbors}>"
+
+MOVES = {(0, -1) : "UP", (0, 1): "DOWN", (-1, 0): "LEFT", (1, 0) : "RIGHT"}
+
+h, w, countdown = [int(i) for i in input().split()]
+
+UP = "UP"
+RIGHT = "RIGHT"
+DOWN = "DOWN"
+LEFT = "LEFT"
+MOVES = [UP, RIGHT, DOWN, LEFT]
+
+class Grid():
+    UNKNOWN = 0
+    WALL = 1
+    EMPTY = 2
+    
+    VECT = {UP: (0,-1), RIGHT: (1,0), DOWN: (0,1), LEFT: (-1,0)}
+    INV = {UP: DOWN, RIGHT: LEFT, LEFT: RIGHT, DOWN: UP}
+    
+    def __init__(self, w, h):
+        self.w = w
+        self.h = h
+        self.cells = {(x, y): Grid.UNKNOWN for x in range(self.w) for y in range(self.h)}
+        self.start = None
+        self.control_room = None
+        self.kirk = None
+        
+        self.neighbors = {p: self.get_neighbors(*p) for p in self.cells}
+        self.waypoints = []
+        self.to_explore = None
+        
+    def update(self, kirk, scan):
+        self.previous_pos = self.kirk
+        self.kirk = kirk
+        if not self.start:
+            self.start = kirk
+            
+        for y, row in enumerate(scan):
+            for x, c in enumerate(row):
+                if c == "C":
+                    self.control_room = (x, y)
+                self.cells[(x, y)] = {"#": Grid.WALL, "?": Grid.UNKNOWN}.get(c, Grid.EMPTY)
+        
+        if self.to_explore is not None:
+            if self.cells[self.to_explore] != Grid.UNKNOWN:
+                self.to_explore = None
+        
+        self.update_corners()
+        
+    def _repr_cell(self, p, show=[]):
+        if p == self.kirk:
+            return "T"
+        elif p == self.start:
+            return "S"
+        elif p == self.control_room:
+            return "C"
+        elif self.cells[p] == Grid.WALL:
+            return "#"
+        elif p in show:
+            return "x"
+        elif self.cells[p] == Grid.EMPTY:
+            return "."
+        else:
+            return "?"
+        
+    def graph(self, show=[]):
+        return "\n".join(["".join([self._repr_cell((x, y), show) for x in range(self.w)]) for y in range(self.h)])
+
+    def set_visited(self, p):
+        self.cells[p] = Grid.EMPTY
+        
+    def set_wall(self, p):
+        self.cells[p] = Grid.WALL
+
+    def unknown_gravity_center(self):
+        wx, wy, wtotal = 0,0,0
+        for x, y in self.cells:
+            if self.cells[(x, y)] == Grid.UNKNOWN:
+                wx += x
+                wy += y
+                wtotal += 1
+        return (wx // wtotal, wy // wtotal) if wtotal else None
+
+    @staticmethod
+    def pos_after(current, move):
+        x, y = current
+        dx, dy = Grid.VECT[move]
+        return x + dx, y + dy
+
+    def get_move_to(self, p):
+        return next(m for m in MOVES if grid.pos_after(grid.kirk, m) == p)
+    
+    @staticmethod
+    def dist(p1, p2):
+        xa, ya = p1
+        xb, yb = p2
+        return abs(xa - xb) + abs(ya - yb) 
+
+    def get_neighbors(self, x, y, diags=False):
+        neighs = [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
+        if diags:
+            neighs += [(x - 1, y - 1), (x + 1, y - 1), (x - 1, y + 1), (x + 1, y + 1)]
+        return [(x, y) for x, y in neighs if 0 <= x < self.w and 0 <= y < self.h]
+
+    def line(self, x1, y1, x2, y2):
+        # special case
+        if (x1, y1) == (x2, y2):
+            return [(x1, y1)]
+
+        # diagonal symmetry
+        vertically_oriented = (abs(y2 - y1) > abs(x2 - x1))
+        if vertically_oriented:
+            y1, x1, y2, x2 = x1, y1, x2, y2
+
+        # horizontal symmetry
+        reversed_sym = (x1 > x2)
+        if reversed_sym:
+            x2, y2, x1, y1 = x1, y1, x2, y2
+
+        # angle
+        dx, dy = x2 - x1, y2 - y1
+        alpha = (abs(dy) / dx)
+
+        offset = 0.0
+        step = 1 if dy > 0 else -1
+
+        result = []
+        y = y1
+        for x in range(x1, x2 + 1):
+            if vertically_oriented:
+                result.append((y, x))
+            else:
+                result.append((x, y))
+
+            offset += alpha
+            if offset > 0.5:
+                y += step
+                offset -= 1.0
+
+        if reversed_sym:
+            result.reverse()
+        return result
+
+    def _get_quarters(self, x, y):
+        return {(x - 1, y - 1): [(x - 1, y), (x - 1, y - 1), (x, y - 1)], 
+                (x + 1, y - 1): [(x, y - 1), (x + 1, y - 1), (x + 1, y)], 
+                (x + 1, y + 1): [(x + 1, y), (x + 1, y + 1), (x, y + 1)], 
+                (x - 1, y + 1): [(x, y + 1), (x - 1, y + 1), (x - 1, y)]}
+        
+    def update_corners(self):
+        corners = set()
+        for p, c in self.cells.items():
+            if c == Grid.WALL:
+                for corner, quarter in self._get_quarters(*p).items():
+                    if all(self.cells.get(n, Grid.WALL) != Grid.WALL for n in quarter):
+                        corners.add(corner)
+        self.corners = list(corners)
+        
+    def way(self, start, target, known_only=False):
+        nodes = Queue()
+        origin = PathNode(*start)
+        nodes.put(origin, 0)
+        
+        waypoints = set(self.corners) | {start, target}
+        neighbors = {}
+        
+        passable = [Grid.EMPTY] if known_only else [Grid.EMPTY, Grid.UNKNOWN]
+        
+        for waypoint in waypoints:
+            neighbors[waypoint] = []
+            for other in waypoints:
+                if other is waypoint:
+                    continue
+                sight = self.line(*other, *waypoint)[1:-1]
+                if all(self.cells.get(p, Grid.WALL) in passable for p in sight) and not any(c in sight for c in self.corners):
+                    neighbors[waypoint].append(other)
+        
+        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
+
+            for x, y in neighbors[current]:
+                if (x, y) == current.parent or (x, y) == start:
+                    continue
+
+                cost = current.cost + Grid.dist((x, y), current)
+                priority = cost + Grid.dist((x, y), target)
+                
+                node = PathNode(x, y, current)
+                node.cost = cost
+                nodes.put(node, priority)
+                
+        return None
+
+    def path(self, start, target, known_only=False):
+        nodes = Queue()
+        origin = PathNode(*start)
+        nodes.put(origin, 0)
+        neighbors = []
+
+        its, break_on, broken = 0, 10000, False
+
+        self.costs = {}
+        for p, c in self.cells.items():
+            cost = 0
+            if c == Grid.WALL:
+                cost = -1
+            elif known_only and c == Grid.UNKNOWN:
+                cost = -1
+            else:
+                cost = 2 if c == Grid.UNKNOWN else 1
+            self.costs[p] = cost
+        
+        while nodes:
+            current = nodes.get()
+            
+            if current == target or broken:
+                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:
+                if (x, y) == current.parent:
+                    continue
+
+                its += 1
+                if its > break_on:
+                    broken = True
+                    break
+
+                if self.costs[(x, y)] < 0:
+                    continue
+                
+                cost = current.cost + self.costs[(x, y)]
+                priority = cost + Grid.dist((x, y), target)
+                
+                node = PathNode(x, y, current)
+                node.cost = cost
+                nodes.put(node, priority)
+                
+        return None
+
+    def optim_path(self, origin, target, known_only=False, first_steps=False):
+        way = self.way(origin, target, known_only)
+        if way:
+            if first_steps:
+                path = self.path(origin, way[0], known_only)
+            else:
+                way.insert(0, origin)
+                path = sum([self.path(start, target, known_only) for start, target in zip(way, way[1:])], [])
+        else:
+            path = self.path(origin, target, known_only)
+        return path
+    
+    def propagation(self):
+        # identify dead ends and visited
+        propagation = {}
+        for m in MOVES:
+            visited= set()
+            start = self.pos_after(self.kirk, m)
+            if self.cells[start] == Grid.WALL:
+                continue
+             
+            propagation[m] = 0
+            buffer = [start]
+            visited |= set(buffer)
+             
+            while buffer:
+                new_buffer = []
+                for p in buffer:
+                    for n in self.neighbors[p]:
+                        if not n in visited and \
+                           not n in new_buffer and \
+                           not n == self.kirk and \
+                           not self.cells[n] == Grid.WALL:
+                            new_buffer.append(n)
+                            visited.add(n)
+                            propagation[m] += 2 if self.cells[n] == Grid.UNKNOWN else 1
+                buffer = new_buffer
+        return propagation
+    
+    def discovery(self):
+        # how many cells would be discovered thanks to each move
+        discovery = {}
+        for m in MOVES:
+            pos= self.pos_after(self.kirk, m)
+            if self.cells[pos] == Grid.WALL:
+                continue
+            discovery[m] = len([p for p in self.cells if self.dist(p, pos) <= 4 and self.cells[p] == Grid.UNKNOWN])
+        return discovery
+            
+    def explore(self):
+        if not self.to_explore is None:
+            return self.to_explore
+        
+        buffer = [self.kirk]
+        tested = set()
+        
+        while buffer:
+            new_buffer = []
+            for p in buffer:
+                for n in self.neighbors[p]:
+                    if n in tested:
+                        continue
+                    c = self.cells[n]
+                    if c == Grid.UNKNOWN:
+                        return n
+                    if c != Grid.WALL:
+                        new_buffer.append(n)
+                    tested.add(n)
+            buffer = new_buffer
+            
+        return None
+            
+    def explore2(self):
+        q = Queue()
+        propagation = self.propagation()
+        discovery = self.discovery()
+        
+        unknown_center = self.unknown_gravity_center()
+        
+        log(f"* grav center: {unknown_center}")
+        for m in propagation:
+            new_pos = self.pos_after(self.kirk, m)
+            if new_pos == self.control_room:
+                continue
+            iprop = 100 * propagation[m] // len(self.cells)
+            idisc = 100 * discovery[m] // 16
+            
+            gofar = self.dist(grid.start, new_pos) > self.dist(self.start, self.kirk)
+            unknown_gravity = self.dist(unknown_center, new_pos) < self.dist(unknown_center, self.kirk)
+                
+            goback = (new_pos == self.previous_pos)
+            
+            interest = -2 * iprop - idisc - 10 * gofar - 30 * unknown_gravity + 50 * goback
+            
+            log(m, iprop, idisc, gofar, unknown_gravity, goback, interest)
+            q.fput(m, interest)
+        
+        return q.get()    
+        
+            
+grid = Grid(w, h)
+fuel = 1201
+coming_back = False
+turn = 0
+comeback_found = None
+
+while True:
+    turn += 1
+    fuel -= 1
+    
+    y, x = [int(i) for i in input().split()]
+    grid.update((x, y), [list(input()) for j in range(grid.h)])
+    
+    if grid.kirk == grid.control_room:
+        coming_back = True
+    log("Position:", grid.kirk, "CR:", grid.control_room)
+    log("Fuel:", fuel, "Countdown:", countdown)
+    path = []
+    
+    if grid.control_room is None:
+        log("> Looking for the control room")
+        target = grid.explore()
+        path = grid.optim_path(grid.kirk, target)
+        next_move = grid.get_move_to(path[0])
+    
+    elif not coming_back:
+        log("> Have found the control room")
+        
+        if not comeback_found:
+            comeback = grid.optim_path(grid.control_room, grid.start, True)
+        else:
+            comeback = comeback_found
+        log("comeback's length: ", len(comeback) if comeback else None)
+        log("comeback: ", comeback)
+        
+        if not comeback:
+            log("<!> Path to start can not be computed")
+            if fuel > 300:
+                log("<!> Path to start can not be computed: explore")
+                target = grid.explore()
+                path = grid.optim_path(grid.kirk, target)
+                next_move = grid.get_move_to(path[0])
+            else:
+                log("<!> Path to start can not be computed: go to control room")
+                path = grid.optim_path(grid.kirk, grid.control_room)
+                next_move = grid.get_move_to(path[0])
+                
+        elif len(comeback) > countdown:
+            log("<!> Path to start is to long, keep exploring")
+            target = grid.explore()
+            path = grid.optim_path(grid.kirk, target)
+            next_move = grid.get_move_to(path[0])
+            
+        else:
+            log("> Go to the control room")
+            comeback_found = comeback
+            path = grid.optim_path(grid.kirk, grid.control_room)
+            next_move = grid.get_move_to(path[0])
+    else:
+        log("> Come back to the ship")
+        path = grid.optim_path(grid.kirk, grid.start, True, True)
+        next_move = grid.get_move_to(path[0])
+        
+    log("\n"+grid.graph(path))
+        
+    print(next_move)
+    

+ 321 - 0
labyrinth/test.py

@@ -0,0 +1,321 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import time
+
+
+class Queue():
+    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, item, priority):
+        heapq.heappush(self.items, (priority, item))
+
+    def fput(self, item, priority):
+        while priority in [p for p, _ in self.items]:
+            priority += 1
+        self.put(item, priority)
+
+    def get(self):
+        return heapq.heappop(self.items)[1]
+    
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+    
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, c:{self.cost}>"
+
+class Waypoint(tuple):
+    def __new__(self, x, y, neighbors=None):
+        n = tuple.__new__(self, (x, y))
+        n.neighbors = neighbors or []
+        return n
+
+    def __repr__(self):
+        return f"<({self[0]},{self[1]}), n:{self.neighbors}>"
+
+class Grid():
+    
+    def __init__(self, grid, start, target):
+        rows = grid.strip().split("\n")
+        self.w = len(rows[0])
+        self.h = len(rows)
+        self.cells = {(x, y): c for y, row in enumerate(rows) for x, c in enumerate(list(row))}
+        self.control_room = target
+        self.kirk = start
+        
+        self.neighbors = {p: self.get_neighbors(*p) for p in self.cells}
+        self.update_corners()
+        
+    def _repr_cell(self, p, show=[]):
+        if p == self.kirk:
+            return "T"
+        elif p == self.control_room:
+            return "C"
+        elif p in show:
+            return "x"
+        else:
+            return self.cells[p]
+        
+    def graph(self, show=[]):
+        return "\n".join(["".join([self._repr_cell((x, y), show) for x in range(self.w)]) for y in range(self.h)])
+    
+    @staticmethod
+    def dist(from_, to_):
+        xa, ya = from_
+        xb, yb = to_
+        return abs(xa - xb) + abs(ya - yb) 
+    
+    def get_neighbors(self, x, y, diags=False):
+        neighs = [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
+        if diags:
+            neighs += [(x - 1, y - 1), (x + 1, y - 1), (x - 1, y + 1), (x + 1, y + 1)]
+        return [(x, y) for x, y in neighs if 0 <= x < self.w and 0 <= y < self.h]
+
+    def line(self, x1, y1, x2, y2):
+        # special case
+        if (x1, y1) == (x2, y2):
+            return [(x1, y1)]
+
+        # diagonal symmetry
+        vertically_oriented = (abs(y2 - y1) > abs(x2 - x1))
+        if vertically_oriented:
+            y1, x1, y2, x2 = x1, y1, x2, y2
+
+        # horizontal symmetry
+        reversed_sym = (x1 > x2)
+        if reversed_sym:
+            x2, y2, x1, y1 = x1, y1, x2, y2
+
+        # angle
+        dx, dy = x2 - x1, y2 - y1
+        alpha = (abs(dy) / dx)
+
+        offset = 0.0
+        step = 1 if dy > 0 else -1
+
+        result = []
+        y = y1
+        for x in range(x1, x2 + 1):
+            if vertically_oriented:
+                result.append((y, x))
+            else:
+                result.append((x, y))
+
+            offset += alpha
+            if offset > 0.5:
+                y += step
+                offset -= 1.0
+
+        if reversed_sym:
+            result.reverse()
+        return result
+
+    def _get_quarters(self, x, y):
+        return {(x - 1, y - 1): [(x - 1, y), (x - 1, y - 1), (x, y - 1)], 
+                (x + 1, y - 1): [(x, y - 1), (x + 1, y - 1), (x + 1, y)], 
+                (x + 1, y + 1): [(x + 1, y), (x + 1, y + 1), (x, y + 1)], 
+                (x - 1, y + 1): [(x, y + 1), (x - 1, y + 1), (x - 1, y)]}
+        
+    def update_corners(self):
+        corners = set()
+        for p, c in self.cells.items():
+            if c == "#":
+                for corner, quarter in self._get_quarters(*p).items():
+                    if all(self.cells.get(n, "#") != "#" for n in quarter):
+                        corners.add(corner)
+        self.corners = list(corners)
+        
+    def way(self, start, target, known_only=False):
+        nodes = Queue()
+        origin = PathNode(*start)
+        nodes.put(origin, 0)
+        
+        waypoints = set(self.corners) | {start, target}
+        neighbors = {}
+        passable = ["."] if known_only else [".", "?"]
+        
+        for waypoint in waypoints:
+            neighbors[waypoint] = []
+            for other in waypoints:
+                if other is waypoint:
+                    continue
+                sight = self.line(*other, *waypoint)[1:-1]
+                if all(self.cells.get(p, "#") in passable for p in sight) and not any(c in sight for c in self.corners):
+                    neighbors[waypoint].append(other)
+                
+#         print("\n".join([str(k) + ": " + str(v) for k, v in neighbors.items()]))
+#         print(len(neighbors))
+        
+        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
+
+            for x, y in neighbors[current]:
+                if (x, y) == current.parent or (x, y) == start:
+                    continue
+
+                cost = current.cost + Grid.dist((x, y), current)
+#                 cost = current.cost + 1
+                priority = cost + Grid.dist((x, y), target)
+                
+                node = PathNode(x, y, current)
+                node.cost = cost
+                nodes.put(node, priority)
+                
+        return None
+
+    def path(self, start, target, known_only=False):
+        nodes = Queue()
+        origin = PathNode(*start)
+        nodes.put(origin, 0)
+        neighbors = []
+
+        its, break_on, broken = 0, 10000000, False
+
+        self.costs = {}
+        for p, c in self.cells.items():
+            cost = 0
+            if c == "#":
+                cost = -1
+            elif known_only and c == "?":
+                cost = -1
+            else:
+                cost = 2 if c == "?" else 1
+            self.costs[p] = cost
+        
+        while nodes:
+            current = nodes.get()
+            
+            if current == target or broken:
+                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:
+                if (x, y) == current.parent:
+                    continue
+
+                its += 1
+                if its > break_on:
+                    broken = True
+                    break
+
+                if self.costs[(x, y)] < 0:
+                    continue
+                
+                cost = current.cost + self.costs[(x, y)]
+                priority = cost + Grid.dist((x, y), target)
+                
+                node = PathNode(x, y, current)
+                node.cost = cost
+                nodes.put(node, priority)
+                
+        return None
+
+    def optim_path(self, origin, target, known_only=False, first_steps=False):
+        way = self.way(origin, target, known_only)
+        print(way)
+        if way:
+            if first_steps:
+                path = self.path(origin, way[0], known_only)
+            else:
+                way.insert(0, origin)
+                path = sum([self.path(start, target, known_only) for start, target in zip(way, way[1:])], [])
+        else:
+            path = self.path(origin, target, known_only)
+        return path    
+    
+    def explore(self):
+        
+        buffer = [self.kirk]
+        tested = set()
+        
+        while buffer:
+            new_buffer = []
+            for p in buffer:
+                for n in self.neighbors[p]:
+                    if n in tested:
+                        continue
+                    c = self.cells[n]
+                    if c == "?":
+                        return n
+                    if c != "#":
+                        new_buffer.append(n)
+                    tested.add(n)
+            buffer = new_buffer
+            
+        return None
+    
+raw = """
+##############################
+#............................#
+#.#######################.#..#
+#.......................#.#..#
+#.....#.................#.#..#
+#.#######################.#..#
+#.....##......##......#....###
+#...####..##..##..##..#..#...#
+#.........##......##.....#...#
+?##########################.##
+?......#......#..............#
+????...#.....................#
+????#..####################..#
+????.........................#
+????##########################
+"""
+
+start = (4, 11)
+target = (6, 3) 
+
+grid = Grid(raw, start, target)
+
+print(grid.graph())
+
+print("\n\n")
+
+t0 = time.time()
+path = grid.optim_path(start, target, True)
+print(time.time() - t0)
+ 
+print(len(path))
+print(grid.graph(path))
+print(path)
+
+# t0 = time.time()
+# path = grid.path(start, target, True)
+# print(time.time() - t0)
+#  
+# print(len(path))
+# print(grid.graph(path))
+# print(path)
+

+ 575 - 0
last_crusade.py

@@ -0,0 +1,575 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+import time
+
+
+t0 = time.time()
+def log(*msg):
+    print("{} - ".format(str(time.time() - t0)[:5]), *msg, file=sys.stderr)
+
+TOP = 0
+LEFT = 1
+RIGHT = 2
+BOTTOM = 3
+DIRS = {TOP: (0, -1), BOTTOM: (0, 1), RIGHT: (1, 0), LEFT: (-1, 0)}
+
+PIVOT_UNCHANGED = 0
+PIVOT_LEFT = 1
+PIVOT_RIGHT = 2
+PIVOT_BACK = 3
+PIVOTS = {PIVOT_UNCHANGED: 0, PIVOT_LEFT: 1, PIVOT_RIGHT: 1, PIVOT_BACK: 2}
+COMMAND = {PIVOT_LEFT: "LEFT", PIVOT_RIGHT: "RIGHT"}
+
+SYM = {TOP: BOTTOM, BOTTOM: TOP, LEFT: RIGHT, RIGHT: LEFT}
+
+
+class Queue():
+    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, item, priority):
+        heapq.heappush(self.items, (priority, item))
+
+    def fput(self, item, priority):
+        while priority in [p for p, _ in self.items]:
+            priority += 1
+        self.put(item, priority)
+
+    def get(self):
+        return heapq.heappop(self.items)[1]
+    
+    def fget(self):
+        return heapq.heappop(self.items)
+
+class Position(tuple):
+    def __new__(self, x, y, face):
+        n = tuple.__new__(self, (x, y,face))
+        return n
+    
+    @property
+    def x(self):
+        return self[0]
+    
+    @property
+    def y(self):
+        return self[1]
+    
+    @property
+    def coords(self):
+        return self[:2]
+    
+    @property
+    def face(self):
+        return self[2]
+    
+class Item():
+    def __init__(self, x, y, side):
+        self.pos = Position(x, y, side)
+    
+    @classmethod
+    def from_input(cls, x, y, strface):
+        return cls(int(x), int(y), ["TOP", "LEFT", "RIGHT", "BOTTOM"].index(strface))
+
+class Indi(Item):
+    graph = "I"
+
+class Rock(Item):
+    graph = "R"
+
+class RockThreat(Item):
+    graph = "T"
+
+class Piece():
+    type = 0
+    pipes = {}
+    pivoted = {PIVOT_LEFT: 0, PIVOT_RIGHT: 0, PIVOT_BACK: 0}
+    graph = ["...",
+             "...",
+             "..."]
+    
+    def __init__(self, x, y, locked=False):
+        self.x = int(x)
+        self.y = int(y)
+        self.locked = locked
+
+    def __repr__(self):
+        return "<{}{}:{}>".format(self.type, "X"*self.locked, (self.x, self.y))
+
+    def result(self, from_):
+        return self.pipes[from_]
+
+    def open_on(self, face_in):
+        return face_in in self.pipes
+    
+    def apply(self, face_in):
+        return self.pipes.get(face_in, None)
+        
+    def connects(self, face_in, face_out):
+        return self.apply(face_in) == face_out
+        
+class Piece0(Piece):
+    pass
+    
+class Piece1(Piece0):
+    type = 1
+    pipes = {TOP: BOTTOM, RIGHT: BOTTOM, LEFT: BOTTOM}
+    pivoted = {PIVOT_LEFT: 1, PIVOT_RIGHT: 1, PIVOT_BACK: 1}
+    graph = ["x x",
+             "   ",
+             "x x"]
+
+class Piece2(Piece0):
+    type = 2
+    pipes = {LEFT: RIGHT, RIGHT: LEFT}
+    pivoted = {PIVOT_LEFT: 3, PIVOT_RIGHT: 3, PIVOT_BACK: 2}
+    graph = ["xxx",
+             "   ",
+             "xxx"]
+
+class Piece3(Piece0):
+    type = 3
+    pipes = {TOP: BOTTOM}
+    pivoted = {PIVOT_LEFT: 2, PIVOT_RIGHT: 2, PIVOT_BACK: 3}
+    graph = ["x x",
+             "x x",
+             "x x"]
+        
+class Piece4(Piece0):
+    type = 4
+    pipes = {TOP: LEFT, RIGHT: BOTTOM}
+    pivoted = {PIVOT_LEFT: 5, PIVOT_RIGHT: 5, PIVOT_BACK: 4}
+    graph = ["x x",
+             " / ",
+             "x x"]
+    
+class Piece5(Piece0):
+    type = 5
+    pipes = {LEFT: BOTTOM, TOP: RIGHT}
+    pivoted = {PIVOT_LEFT: 4, PIVOT_RIGHT: 4, PIVOT_BACK: 5}
+    graph = ["x x",
+             " \ ",
+             "x x"]
+
+class Piece6(Piece0):
+    type = 6
+    pipes = {LEFT: RIGHT, RIGHT: LEFT}
+    pivoted = {PIVOT_LEFT: 9, PIVOT_RIGHT: 7, PIVOT_BACK: 8}
+    graph = ["x x",
+             "   ",
+             "xxx"]
+
+class Piece7(Piece0):
+    type = 7
+    pipes = {TOP: BOTTOM, RIGHT: BOTTOM}
+    pivoted = {PIVOT_LEFT: 6, PIVOT_RIGHT: 8, PIVOT_BACK: 9}
+    graph = ["x x",
+             "x  ",
+             "x x"]
+
+class Piece8(Piece0):
+    type = 8
+    pipes = {RIGHT: BOTTOM, LEFT: BOTTOM}
+    pivoted = {PIVOT_LEFT: 7, PIVOT_RIGHT: 9, PIVOT_BACK: 6}
+    graph = ["xxx",
+             "   ",
+             "x x"]
+
+class Piece9(Piece0):
+    type = 9
+    pipes = {TOP: BOTTOM, LEFT: BOTTOM}
+    pivoted = {PIVOT_LEFT: 8, PIVOT_RIGHT: 6, PIVOT_BACK: 7}
+    graph = ["x x",
+             "  x",
+             "x x"]
+
+class Piece10(Piece0):
+    type = 10
+    pipes = {TOP: LEFT}
+    pivoted = {PIVOT_LEFT: 13, PIVOT_RIGHT: 11, PIVOT_BACK: 12}
+    graph = ["x x",
+             "  x",
+             "xxx"]
+
+class Piece11(Piece0):
+    type = 11
+    pipes = {TOP: RIGHT}
+    pivoted = {PIVOT_LEFT: 10, PIVOT_RIGHT: 12, PIVOT_BACK: 13}
+    graph = ["x x",
+             "x  ",
+             "xxx"]
+
+class Piece12(Piece0):
+    type = 12
+    pipes = {RIGHT: BOTTOM}
+    pivoted = {PIVOT_LEFT: 11, PIVOT_RIGHT: 13, PIVOT_BACK: 10}
+    graph = ["xxx",
+             "x  ",
+             "x x"]
+
+class Piece13(Piece0):
+    type = 13
+    pipes = {LEFT: BOTTOM}
+    pivoted = {PIVOT_LEFT: 12, PIVOT_RIGHT: 10, PIVOT_BACK: 11}
+    graph = ["xxx",
+             "  x",
+             "x x"]
+
+classes = [Piece0, Piece1, Piece2, Piece3, Piece4, 
+          Piece5, Piece6, Piece7, Piece8, Piece9, 
+          Piece10, Piece11, Piece12, Piece13]
+
+
+def new_piece(x, y, v):
+    x, y, v = int(x), int(y), int(v)
+    type_, locked = abs(v), v < 0
+    piece = classes[type_](x, y, locked)
+    return piece
+
+def turn(piece, pivot):
+    if pivot == PIVOT_UNCHANGED:
+        return piece
+    if piece.locked:
+        raise Exception("can not pivot a locked piece")
+    pivoted_type = piece.pivoted[pivot]
+    if pivoted_type == piece.type:
+        return piece
+    return classes[pivoted_type](piece.x, piece.y)
+
+class PathNode():
+    def __init__(self, position, parent=None, pivot=PIVOT_UNCHANGED):
+        self.pos = position
+        self.pivot = pivot
+        
+        self.parent = parent
+        self.cost = 0
+        
+        self.round = 0
+        self.require = []
+    
+    def __lt__(self, other):
+        return self.pos < other.pos
+    
+    def __repr__(self):
+        return f"<{self.pos}, pivot:{self.pivot}, cost:{self.cost}>"
+
+
+class Grid():
+    
+    def __init__(self, w, h, rows, x_exit):
+        self.w = w
+        self.h = h
+        self.cells = {(x, y): new_piece(x, y, v) for y, row in enumerate(rows) for x, v in enumerate(row)}
+        
+        # add exit
+        self.exit = (x_exit, h)
+        self.cells[(x_exit, h)] = Piece1(x_exit, h, locked=True)
+        
+        self.indi = None
+        self.rocks = []
+    
+    def get_cell(self, coords):
+        return self.cells.get(coords, Piece0(*coords))
+    
+    def update(self, indi, rocks):
+        self.indi = indi
+        self.rocks = rocks
+
+        self.include_threats()
+        self.update_rocks_moves()
+    
+    @property
+    def items(self):
+        return [self.indi] + self.rocks
+        
+    def graph(self):
+        res = "\n   "
+        res += "".join([f"{x:02d} " for x in range(self.w)])
+        res += "\n"
+        
+        items = []
+        if self.indi:
+            items.append(self.indi)
+        items += self.rocks
+        
+        for y in range(self.h):
+            for i in range(3):
+                if i ==0:
+                    res += "{:02d} ".format(y)
+                else:
+                    res += "   "
+                for x in range(self.w):
+                    piece = grid.cells[(x, y)]
+                    line = list(piece.graph[i])
+                    
+                    for item in items:
+                        if item.pos.coords == (x, y):
+                            if item.pos.face == TOP and i == 0:
+                                line[1] = item.graph
+                            elif item.pos.face == RIGHT and i == 1:
+                                line[2] = item.graph
+                            elif item.pos.face == LEFT and i == 1:
+                                line[0] = item.graph
+                            elif item.pos.face == BOTTOM and i == 2:
+                                line[1] = item.graph
+                    if item.pos.x == x_exit and item.pos.y == h - 1 and i == 2:
+                        line[1] = "E"
+                    
+                    line = "".join(line)
+                    if piece.locked:
+                        line = line.replace("x", "#")
+                    res += line
+#                     res += "|"
+                res += "\n"
+                
+        return res
+    
+    def item_at(self, x, y, side=None):
+        return next((i.pos.coords == (x, y) and (side is None or i.pos.face == side) for i in self.items), None)
+    
+    @staticmethod
+    def dist(p1, p2):
+        xa, ya = p1
+        xb, yb = p2
+        return abs(xa - xb) + abs(ya - yb) 
+    
+    def include_threats(self):
+        
+        threats = []
+        for p, cell in self.cells.items():
+            x, y = p
+            
+            if x == 0:
+                if LEFT in cell.pipes and not self.item_at(x, y):
+                    threats.append(RockThreat(x, y, LEFT))
+            elif x == (self.w - 1) and not self.item_at(x, y):
+                if RIGHT in cell.pipes:
+                    threats.append(RockThreat(x, y, RIGHT))
+            
+            if y == 0:
+                if TOP in cell.pipes and not self.item_at(x, y):
+                    threats.append(RockThreat(x, y, TOP))
+
+        self.rocks += threats
+    
+    def update_rocks_moves(self):
+        self.trajectories = []
+        
+        for rock in self.rocks:
+            trajectory = self.trajectory(rock)
+            is_threat = isinstance(rock, RockThreat)
+            if is_threat:
+                # to simulate the 1 turn delay before arriving
+                trajectory.insert(0, rock.pos)
+            
+            stops = []
+            for i, pos in enumerate(trajectory):
+                piece = self.cells[pos.coords]
+                if piece.locked or i == 0:
+                    continue
+                for pivot in [PIVOT_LEFT, PIVOT_RIGHT, PIVOT_BACK]:
+                    pivoted = turn(piece, pivot)
+                    if not pivoted.open_on(pos.face):
+                        stop = (i, pivot)
+                        stops.append(stop)
+                        break
+            
+            self.trajectories.append((trajectory, stops, is_threat))
+        log(self.trajectories)
+            
+    def after(self, position, pivot=PIVOT_UNCHANGED):
+        x, y, face = position
+        piece = self.cells[(x, y)]
+        piece = turn(piece, pivot)
+        face_out = piece.apply(face)
+        if face_out is None:
+            return None
+        dx, dy = DIRS[face_out]
+        position = Position(x+dx, y+dy, SYM[face_out])
+        return position
+    
+    def trajectory(self, item, with_start=True):
+        path = []
+        if with_start:
+            path.append(item.pos)
+        current = self.after(item.pos)
+        while current is not None:
+            path.append(current)
+            next_pos = self.after(current)
+            if not next_pos or next_pos.coords == self.exit or not self.get_cell(next_pos.coords).open_on(next_pos.face):
+                break
+            current = self.after(current)
+        return path
+    
+    def path(self):
+        subject = self.indi
+        
+        nodes = Queue()
+        origin = PathNode(subject.pos)
+        nodes.put(origin, 0)
+        
+        its, break_on = 0, 6000
+        broken = False
+        
+        while nodes:
+            current = nodes.get()
+            
+            its += 1
+            if its > break_on:
+                log("pathfinding broken")
+                broken = True
+            
+            if broken or current.pos.coords == self.exit:
+                path = []
+                previous = current.parent
+                while previous:
+                    if previous != origin:
+                        path.insert(0, previous)
+                    previous = previous.parent
+                return path
+
+            next_pos = self.after(current.pos, current.pivot)
+            if next_pos is None:
+                log(f"! Next piece doesn't exist: ", current.pos, current.pivot)
+                continue
+
+            next_cell = self.get_cell(next_pos.coords)
+            requirements = []
+            
+            deadend = False
+            for trajectory, stops, is_threat in self.trajectories:
+                if len(trajectory) > (current.round + 1) and trajectory[(current.round + 1)].coords == next_pos.coords:
+                    
+                    alternatives = [Action(trajectory[i], pivot) for i, pivot in stops if 0 < i <= (current.round + 1)]
+                    
+                    require = next(iter(alternatives), None)
+                    if require is None: 
+                        if not is_threat: #unstoppable rock
+                            deadend = True
+                            log("node is deadend: ", next_pos.coords)
+                            break
+                        else:
+                            continue
+                    
+                    if len(alternatives) == 1:
+                        require.last_chance = True
+                    
+                    requirements.append(require)
+            if deadend:
+                continue
+            
+            for pivot, rotations in PIVOTS.items():
+                if rotations > 0:
+                    if next_cell.locked:
+                        continue
+                    elif next_cell.pivoted[pivot] == next_cell.type:
+                        # useless rotation
+                        continue
+                if current is origin and rotations > 1:
+                    continue
+                
+                pivoted = turn(next_cell, pivot)
+                
+                if pivoted.open_on(next_pos.face):
+                    node = PathNode(next_pos, current, pivot)
+                    node.cost = current.cost + rotations
+                    node.round = current.round + 1
+                    node.require = requirements
+                    
+                    priority = node.cost
+                    nodes.put(node, priority)
+
+        return []
+
+    def apply(self, action):
+        self.cells[action.coords] = turn(self.cells[action.coords], action.pivot)
+
+class Action():
+    def __init__(self, coords, pivot, last_chance=False):
+        self.coords = coords[:2]
+        self.pivot = pivot
+        self.last_chance = last_chance
+
+    def command(self):
+        return "{} {} {}".format(*self.coords, COMMAND[self.pivot])
+
+    def __repr__(self):
+        return f"<{self.command()}, last_chance: {self.last_chance}>"
+    
+    def __lt__(self, other):
+        return self.last_chance and not other.last_chance or self.coords < other.coords
+
+# Get input
+w, h = [int(i) for i in input().split()]
+rows = [input().split() for _ in range(h)]
+x_exit = int(input())
+
+# instanciate grid
+grid = Grid(w, h, rows, x_exit)
+
+while True:
+    # get input
+    indi = Indi.from_input(*input().split())
+    rocks = [Rock.from_input(*input().split()) for _ in range(int(input()))]
+    
+    # update grid
+    grid.update(indi, rocks)
+    log(grid.graph())
+    
+    path = grid.path()
+    log(path)
+    
+    if path:
+        plan = []
+        
+        requirements = []
+        for i, node in enumerate(path):
+            if node.pivot == PIVOT_BACK:
+                action = Action(node.pos, PIVOT_RIGHT, i <= 1)
+                log("Node requirement: ", action)
+                if i <= 1:
+                    requirements.insert(0, action)
+                else:
+                    requirements.append(action)
+                
+            for action in node.require:
+                log("Node requirement: ", action)
+                if action.last_chance:
+                    requirements.insert(0, action)
+                else:
+                    requirements.append(action)
+                
+                
+        for i, node in enumerate(path):
+            
+            if node.pivot in [PIVOT_LEFT, PIVOT_RIGHT]:
+                plan.append(Action(node.pos, node.pivot))
+            elif node.pivot == PIVOT_BACK:
+                plan.append(Action(node.pos, PIVOT_RIGHT))
+            else:
+                if requirements:
+                    action = requirements.pop(0)
+                    plan.append(action)
+                
+        log(plan)
+    else:
+        log("no path, use previous plan")
+
+    try:
+        action = plan.pop(0)
+        grid.apply(action)
+        print(action.command())
+    except IndexError:
+        print("WAIT")

+ 13 - 0
pirates_treasure.py

@@ -0,0 +1,13 @@
+def neighbors(x, y):
+    return [(x + 1, y), (x + 1, y + 1),(x, y + 1),(x - 1, y + 1),(x + 1, y - 1),(x - 1, y - 1),(x - 1, y ),(x, y - 1)]
+
+input()
+my = {(x, y): int(v) for y in range(int(input())) for x, v in enumerate(input().split())}
+
+for c in my:
+    if my[c] == 0:
+        for j in neighbors(*c):
+            if my.get(j,1) == 0:
+                break
+        else:
+            print(f"{x} {y}")

+ 1074 - 0
spring2020/s2020.py

@@ -0,0 +1,1074 @@
+"""
+Created on 7 mai 2020
+
+@author: olinox
+"""
+import sys
+import time
+import heapq
+
+# TODO: faire un simple pathfinding en cas d'echec au calcul de route
+# TODO: prévoir un plan fallback quand pas de chemin
+
+debug = False
+t0 = time.time()
+
+LOG_INPUT = (0 and __file__[-8:] != 's2020.py')
+HIJACK_INPUT = (1 and __file__[-8:] == 's2020.py')
+
+_force_input = []
+if HIJACK_INPUT:
+    _force_input = [
+        ['33 16', '#################################', '    #   # # #   #   # # #   #    ', '### # # # # # ##### # # # # # ###', '# #   #     #   #   #     #   # #', '# # # ### ### # # # ### ### # # #', '# # #         #   #         # # #', '# # ### # # # ##### # # # ### # #', '#       # #   #   #   # #       #', '##### # # # # # # # # # # # #####', '#     #   # #   #   # #   #     #', '# # # # ### ### # ### ### # # # #', '# #   #     #       #     #   # #', '### # ### # # # # # # # ### # ###', '### #     #           #     # ###', '### # # # ##### # ##### # # # ###', '#################################', '0 0', '6', '0 1 2 9 ROCK 0 0', '1 1 13 11 PAPER 0 0', '1 0 19 11 PAPER 0 0', '2 1 26 13 SCISSORS 0 0', '3 1 29 12 ROCK 0 0', '4 1 21 8 PAPER 0 0', '32', '1 9 1', '3 9 1', '4 9 1', '5 9 1', '14 11 1', '15 11 1', '16 11 1', '17 11 1', '18 11 1', '13 12 1', '13 13 1', '25 13 1', '24 13 1', '23 13 1', '27 13 1', '29 11 1', '29 10 1', '29 9 1', '29 13 1', '29 14 1', '21 7 1', '21 6 1', '21 5 1', '21 9 1', '21 10 1', '21 11 1', '21 12 1', '21 13 1', '5 1 10', '27 1 10', '12 13 10', '20 13 10'],
+        ['137 100', '7', '0 1 9 5 SCISSORS 0 0', '1 1 5 7 ROCK 0 0', '2 1 25 6 SCISSORS 0 0', '3 1 23 5 SCISSORS 0 0', '3 0 6 5 ROCK 0 2', '4 1 13 5 PAPER 0 0', '4 0 8 5 PAPER 0 2', '10', '9 9 1', '4 7 1', '3 7 1', '2 7 1', '1 7 1', '5 14 1', '23 2 1', '23 1 1', '13 2 1', '13 1 1']
+    ]
+
+INPUTS = []
+
+
+def l_input(*args):
+    if HIJACK_INPUT:
+        if not _force_input:
+            print('no more hijack')
+            sys.exit()
+        inp = _force_input[0].pop(0)
+        if len(_force_input[0]) == 0:
+            del _force_input[0]
+    else:
+        inp = input()
+    INPUTS.append(inp)
+    return inp
+
+
+def log(*msg):
+    if debug:
+        print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr)
+        sys.stderr.flush()
+
+
+# OWNER
+ME = 0
+OPPONENT = 1
+
+
+# Base classes
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+
+class Queue(Base):
+    def __init__(self, inv=False):
+        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, item, priority):
+        heapq.heappush(self.items, (priority, item))
+
+    def fput(self, item, priority):
+        while priority in [p for p, _ in self.items]:
+            priority += 1
+        self.put(item, priority)
+
+    def get(self):
+        return heapq.heappop(self.items)[1]
+
+    def get_items(self):
+        return heapq.heappop(self.items)
+
+
+class Node(Base):
+    def __init__(self, pos, path=None):
+        self.pos = pos
+        self.path = path or []
+
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, c:{self.cost}>"
+
+
+class RiskNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.proba = 0
+        return n
+
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, p:{self.proba}>"
+
+
+class RouteNode():
+    def __init__(self, segment, parent=None):
+        self.segment = segment
+        self.parent = parent
+        self.value = 0
+        self.cost = 0
+        self.visited = []
+
+    def __repr__(self):
+        return f"<{self.segment}, c:{self.cost}, v:{self.value}>"
+
+
+# Script classes
+
+class Player():
+    def __init__(self, id_):
+        self.id_ = id_
+        self.score = 0
+        self.pacs = []
+
+
+UNKNOWN_CELL = "?"
+WALL_CELL = "#"
+FLOOR_CELL = " "
+
+
+class Cell(Base):
+    def __init__(self, x, y, type_=UNKNOWN_CELL):
+        self.x = x
+        self.y = y
+        self.type_ = type_
+        self._value = 1  # on part du principe que toutes les cases contiennent au moins une pastille au départ
+        self.status = UNOCCUPIED
+        self.neighbors = []  # only movable
+
+        self.seen = False
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    @property
+    def unknown(self):
+        return self.type_ == UNKNOWN_CELL or (self.is_floor and not self.seen)
+
+    @property
+    def is_floor(self):
+        return self.type_ == FLOOR_CELL
+
+    @property
+    def movable(self):
+        return self.is_floor
+
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        self._value = value
+        self.seen = True
+
+    def print_(self):
+        if self.unknown:
+            return "?"
+        elif self.movable:
+            return {0: " ", 1: "*", 10: "$"}[self.value]
+        else:
+            return WALL_CELL
+
+
+class Segment(Base):
+    REPR = "?"
+    uid = 0
+
+    def __init__(self, cells):
+        self.id_ = Segment.new_uid()
+        self._cells = []
+        self.cells = cells
+
+        # clockwise
+        self.neighbors = []
+
+        self.status = UNOCCUPIED
+
+    @property
+    def cells(self):
+        return self._cells
+
+    @cells.setter
+    def cells(self, cells):
+        self._cells = cells
+        self.coords = [c.pos for c in cells]
+        self.length = len(cells)
+        self.update()
+
+    def update_status(self):
+        self.status = max(c.status for c in self.cells)
+
+    def update(self):
+        self.value = sum(c.value for c in self.cells if not c.status == TARGETTED)
+        self.update_status()
+
+    def print_(self):
+        return self.REPR
+
+    @classmethod
+    def new_uid(cls):
+        cls.uid += 1
+        return cls.uid
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__} {self.id_}: {'-'.join([str(c.pos) for c in self.cells])}>"
+
+    def go_trough(self, from_):
+
+        ifrom = self.coords.index(from_)
+        if ifrom == 0:
+            return list(self.coords)
+        elif ifrom == (len(self.coords) - 1):
+            return self.coords[::-1]
+        else:
+            raise ValueError(f"{from_} not in segment")
+
+
+class StartingPos(Segment):
+    REPR = "O"
+
+    def __init__(self, cell, pac_id):
+        super().__init__([cell])
+        self.pac_id = pac_id
+
+    def cell(self):
+        return self.cells[0]
+
+
+class Intersection(Segment):
+    REPR = "+"
+
+    def __init__(self, cell):
+        super().__init__([cell])
+
+    def cell(self):
+        return self.cells[0]
+
+
+class Corridor(Segment):
+    REPR = "-"
+
+
+class LoopCorridor(Corridor):
+    REPR = "~"
+
+
+class Deadend(Segment):
+    REPR = "."
+
+
+class Grid(Base):
+    MAX_ITS = 360
+
+    def __init__(self, width, height):
+        self.width = width
+        self.height = height
+        self.cells = {}
+        self.pacs = {}
+        self.pellets = {}
+        self.risk = {}
+        self.segments = []
+
+        self.owned_pacs = []
+        self.opponent_pacs = []
+
+        self.segment_index = {}
+        self.pacs_index = {}
+
+        self.path_cache = {}
+        self.consumed_its = 0
+
+
+    @property
+    def grid(self):
+        return [[self.cells[(x, y)].print_() for x in range(self.width)] for y in range(self.height)]
+
+    def print_(self):
+        return "\n" + "\n".join(["".join([c for c in row]) for row in self.grid])
+
+    def __getitem__(self, key):
+        return self.cells[key]
+
+    @classmethod
+    def from_matrix(cls, matrix):
+        width = len(matrix[0])
+        height = len(matrix)
+        cells = {(x, y): Cell(x, y, v) for y, r in enumerate(matrix) for x, v in enumerate(r)}
+        grid = Grid(width, height)
+        grid.cells = cells
+
+        for pos, cell in grid.cells.items():
+            cell.neighbors = []
+            for n in grid.neighbors(*pos):
+                try:
+                    if grid.cells[n].movable:
+                        cell.neighbors.append(n)
+                except KeyError:
+                    log(f'/!\ {n} not in cells')
+        return grid
+
+    def segmentize(self):
+
+        self.segments = []
+        self.segment_index = {}
+
+        segments = []
+
+        # intersections and starting points
+        pivots = []
+        pivots_coords = set()
+        for pos, cell in self.cells.items():
+            if pos in self.pacs_index:
+                segment = StartingPos(cell, self.pacs_index[pos].id)
+                segments.append(segment)
+
+                self.segment_index[pos] = segment
+                pivots.append(segment)
+                pivots_coords.add(pos)
+
+
+            elif cell.movable and len(cell.neighbors) >= 3:
+                segment = Intersection(cell)
+                segments.append(segment)
+                self.segment_index[pos] = segment
+                pivots.append(segment)
+                pivots_coords.add(pos)
+
+        # corridors and deadends
+        for pivot in pivots:
+
+            pivot_cell = pivot.cell()
+
+            for n in pivot_cell.neighbors:
+
+                if n in self.segment_index:
+                    continue
+
+                nc = self.cells[n]
+                buffer = []
+
+                while nc:
+                    buffer.append(nc)
+
+                    if nc.pos in pivots_coords:
+                        # corridor ou startingpos
+                        segment = Corridor(buffer[:-1])
+                        break
+
+                    elif len(nc.neighbors) == 1:
+                        segment = Deadend(list(buffer))
+                        break
+
+                    elif len(nc.neighbors) == 2:
+                        nextnc = next((self.cells[p] for p in nc.neighbors if
+                                       self.cells[p] not in buffer and p != pivot_cell.pos), None)
+
+                        if nextnc is None:
+                            # c'est une boucle, on vient de revenir au point de depart
+                            segment = LoopCorridor(list(buffer))
+                            break
+                        nc = nextnc
+
+                    else:
+                        log('what happened??')
+                for c in segment.cells:
+                    self.segment_index[c.pos] = segment
+
+                segments.append(segment)
+
+        # connect
+        for pivot in pivots:
+            pivot_cell = pivot.cell()
+            for n in pivot_cell.neighbors:
+                pivot.neighbors.append(n)
+                neighbor_seg = self.segment_index[n]
+                if type(neighbor_seg) not in (Intersection, StartingPos):
+                    neighbor_seg.neighbors.append(pivot_cell.pos)
+
+        self.segments = segments
+
+        for s in self.segments:
+            s.update()
+
+    def segment_neighbors(self, seg):
+        return [self.segment_index[n] for n in seg.neighbors]
+
+    def print_segments(self):
+        rows = []
+        for y in range(self.height):
+            row = []
+            for x in range(self.width):
+                try:
+                    row.append(self.segment_index[(x, y)].print_())
+                except KeyError:
+                    row.append("#")
+            rows.append(row)
+        return "\n".join(["".join([c for c in row]) for row in rows])
+
+    def update(self, visible_pacs, visible_pellets):
+
+        self.consumed_its = 0
+
+        for c in self.cells.values():
+            c.status = UNOCCUPIED
+
+        # update pacs
+        self.pacs_index = {}
+        me.pacs = []
+
+        for a in visible_pacs:
+            pac_id, is_mine, x, y, type_id, speed_turns_left, ability_cooldown = a
+            if is_mine:
+                id_ = pac_id
+            else:
+                id_ = -1 * (pac_id + 1)
+
+            if not id_ in grid.pacs:
+                pac = Pac(id_, is_mine)
+                self.pacs[id_] = pac
+            else:
+                pac = self.pacs[id_]
+
+            pac.update(x, y, type_id, speed_turns_left, ability_cooldown)
+
+            self.cells[(x, y)].status = ALLY_HERE if is_mine else ENNEMY_HERE
+            self.pacs_index[(x, y)] = pac
+
+            if is_mine:
+                me.pacs.append(pac)
+            else:
+                if not pac in opponent.pacs:
+                    opponent.pacs.append(pac)
+
+        # update threats
+        for pac in me.pacs:
+            for ennemy in opponent.pacs:
+                dist = 2 if not pac.is_accelerated else 3
+                scope = self.scope(ennemy.pos, dist)
+                if pac.pos in scope and pac.lose_against(ennemy):
+                    pac.threaten_by = ennemy.id
+
+        # special: if a 10-cell still exist, we'll know
+        for c in self.cells.values():
+            if c.value:
+                c.value = 1
+
+        # update pellets
+        self.pellets = {}
+        for a in visible_pellets:
+            x, y, v = a
+            self.pellets[(x, y)] = Pellet(x, y, v)
+
+            if v == 10:
+                # always visible
+                self.cells[(x, y)].value = v
+
+        # update cells values
+        in_sight = set()
+        for pac in me.pacs:
+            sight = set(self.line_of_sight(pac.pos))
+            in_sight |= sight
+
+        for pos in in_sight:
+            val = self.pellets[pos].value if pos in self.pellets else 0
+            self.cells[pos].value = val
+
+    @staticmethod
+    def manhattan(from_, to_):
+        xa, ya = from_
+        xb, yb = to_
+        return abs(xa - xb) + abs(ya - yb)
+
+    def neighbors(self, x, y, diags=False):
+        neighs = []
+        for nx, ny in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]:
+            if not 0 <= ny < self.height:
+                continue
+            if nx == -1:
+                nx = self.width - 1
+            if nx == self.width:
+                nx = 0
+            neighs.append((nx, ny))
+        return neighs
+
+    @staticmethod
+    def closest(from_, in_):
+        return min(in_, key=lambda x: Grid.manhattan(from_, x))
+
+    def line_of_sight(self, from_):
+        x0, y0 = from_
+
+        sight = [(x0, y0)]
+
+        x = x0 + 1
+        while 1:
+            if x == self.width:
+                x = 0
+            if x == x0:
+                break  # tour complet
+            if not self.cells[(x, y0)].is_floor:
+                break
+            sight.append((x, y0))
+            x += 1
+
+        x = x0 - 1
+        while 1:
+            if x == -1:
+                x = self.width - 1
+            if x == x0:
+                break  # tour complet
+            if not self.cells[(x, y0)].is_floor:
+                break
+            sight.append((x, y0))
+            x -= 1
+
+        y = y0 + 1
+        while self.cells[(x0, y)].is_floor:
+            sight.append((x0, y))
+            y += 1
+
+        y = y0 - 1
+        while self.cells[(x0, y)].is_floor:
+            sight.append((x0, y))
+            y -= 1
+
+        return sight
+
+    def scope(self, start, dist=1):
+        """ zone de mouvement d'un pac """
+        scope = {start}
+
+        for _ in range(dist):
+            new_scope = set()
+            for p in scope:
+                new_scope |= {n for n in self.neighbors(*p) if self.cells[n].is_floor}
+            scope |= new_scope
+        return list(scope)
+
+    def path(self, start, target):
+        nodes = Queue()
+        its, break_on = 0, 2000
+
+        origin = PathNode(*start)
+        nodes.put(origin, 0)
+
+        neighbors = []
+
+        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
+
+                cell = self.cells[(x, y)]
+                if not cell.movable:
+                    continue
+
+                moving_cost = 1
+                if cell.unit and cell.unit.owned:
+                    moving_cost += cell.unit.level
+                if cell.under_tower:
+                    moving_cost += 2
+
+                cost = current.cost + moving_cost
+                priority = cost + 10 * Grid.manhattan((x, y), target)
+
+                node = PathNode(x, y, current)
+                node.cost = cost
+                nodes.put(node, priority)
+
+        return None
+
+    def eval_risk(self):
+        # eval the nest opponent moves
+
+        self.risk = {}
+
+        for pac in opponent.pacs:
+
+            current = RiskNode(pac.x, pac.y)
+            current.proba = 100
+            nodes = {0: [current]}
+            round_ = 1
+            while round_ < 8:
+                nodes[round_] = []
+
+                for parent in nodes[round_ - 1]:
+                    neighbors = self.neighbors(*parent)
+
+                    candidates_cells = [self.cells[n] for n in neighbors if self.cells[n].movable]
+
+                    coming_from = tuple(parent.parent) if parent.parent else None
+
+                    # le x4/+1 est pour laisser une proba aux cases sans pellet, ça donne du 80% pour la case à 1 et 20% pour celle à 0
+                    total_val = sum(
+                        [(4 * cell.value if (cell.x, cell.y) != coming_from else 0) + 1 for cell in candidates_cells])
+
+                    for cell in candidates_cells:
+                        node = RiskNode(cell.x, cell.y)
+                        node.parent = parent
+
+                        proba = (100 * ((4 * cell.value if (cell.x, cell.y) != coming_from else 0) + 1)) / total_val
+                        node.proba = round((proba * parent.proba) / 100)
+
+                        nodes[round_].append(node)
+
+                round_ += 1
+
+            for round_, round_nodes in nodes.items():
+                if not round_ in self.risk:
+                    self.risk[round_] = {}
+                for node in round_nodes:
+                    x, y = node
+                    if not (x, y) in self.risk[round_]:
+                        self.risk[round_][(x, y)] = node.proba
+                    else:
+                        if node.proba > self.risk[round_][(x, y)]:
+                            self.risk[round_][(x, y)] = node.proba
+
+    def print_risk(self, round_):
+        if not round_ in self.risk:
+            return "no data to show"
+
+        grid = []
+        for y in range(self.height):
+            row = []
+            for x in range(self.width):
+                if (x, y) in self.risk[round_]:
+                    c = str(self.risk[round_][(x, y)])
+                    if len(c) == 1:
+                        c = f".{c}."
+                    elif len(c) == 2:
+                        c = f".{c}"
+                elif not self.cells[(x, y)].movable:
+                    c = "###"
+                else:
+                    c = "..."
+                row.append(c)
+            grid.append(row)
+        res = "\n".join(["".join([c for c in row]) for row in grid])
+        return f"ROUND {round_}\n" + res
+
+    def route_finding(self, start):
+
+        if self.consumed_its > self.MAX_ITS:
+            log('max its already consumed')
+            return None
+
+        nodes = Queue(inv=True)
+
+        targeted_cost = 18
+        reserve = Queue()
+
+        its, break_on = 0, 80
+        broken = False
+
+        starting_seg = self.segment_index[start]
+
+        origin = RouteNode(starting_seg)
+        origin.visited = [starting_seg.id_]
+        origin.value = 0
+        origin.cost = 1
+        nodes.put(origin, 0)
+
+        current = None
+        while 1:
+
+            if broken:
+                break
+
+            if not nodes:
+                break
+
+            priority, current = nodes.get_items()
+
+            if current.cost >= targeted_cost:
+                reserve.fput(current, priority)
+                continue
+
+            neighbors = self.segment_neighbors(current.segment)
+
+            for seg in neighbors:
+
+                its += 1
+                if its >= break_on or (its + self.consumed_its) > self.MAX_ITS:
+                    broken = True
+                    break
+
+                if seg.status > ALLY_HERE and not seg is starting_seg:
+                    continue
+
+                if seg.status == ALLY_HERE and not current.cost:
+                    continue
+
+                value = seg.value if seg.id_ not in current.visited else 0
+
+                cost = seg.length
+
+                half_turn = (current.parent is not None and current.parent.segment.id_ == seg.id_ and type(seg) is not LoopCorridor)
+                if half_turn:
+                    cost *= 2
+
+                node = RouteNode(seg, current)
+                node.value = current.value + value
+                node.cost = current.cost + cost
+                node.visited = list(current.visited) + [seg.id_]
+
+                priority = 100 * (node.cost - node.value)
+                nodes.fput(node, priority)
+
+        self.consumed_its += its
+
+        if broken:
+            # loop was interrupted
+            if reserve:
+                # some routes were put in the reserve
+                log('> routefinding interrupted')
+                target = reserve.get()
+            elif nodes:
+                # no routes has been fully explored
+                log('> routefinding interrupted without reserve')
+                target = nodes.get()
+                while target.value == 0:
+                    if not nodes:
+                        log('not a single valued node to target... :(')
+                        return None
+                    target = nodes.get()
+            else:
+                # something went wrong
+                log('<!> routefinding broken')
+                return []
+
+        else:
+            if reserve:
+                # all paths were explored
+                target = reserve.get()
+            else:
+                # something went wrong
+                log('<!> routefinding broken')
+                return []
+
+        route = []
+        previous = target
+        while previous:
+            if previous.segment.id_ != starting_seg.id_:
+                route.insert(0, previous)
+            previous = previous.parent
+
+        return [node.segment for node in route]
+
+    def route_to_path(self, route, from_):
+
+        path = []
+        cur_pos = from_
+
+        for seg in route:
+            part = []
+
+            # is it an half-turn?
+            if cur_pos in seg.coords:
+                # on parcourt le dernier segment a l'envers avant de poursuivre
+                part = seg.go_trough(cur_pos)[1:]
+
+            else:
+                for n in self.neighbors(*cur_pos):
+                    try:
+                        part = seg.go_trough(n)
+                        break
+                    except ValueError:
+                        pass
+
+            if not part:
+                log("error: error while rebuilding the path")
+                return path
+
+            path += part
+            cur_pos = path[-1]
+            if not route:
+                break
+
+        return path
+
+    def cache_path(self, pac_id, path):
+        self.path_cache[pac_id] = (path, sum(self.cells[c].value for c in path))
+
+    def has_cached_path(self, pac_id):
+        return pac_id in self.path_cache
+
+    def get_cached_path(self, pac_id):
+        """ return the cached path if its total value did not change """
+        if not pac_id in self.path_cache:
+            return None
+
+        path, val = self.path_cache[pac_id]
+        if not path:
+            return None
+
+        cur_val = sum(self.cells[c].value for c in path)
+
+        if val != cur_val:
+            return None
+
+        if any(other.pos in path for other in me.pacs if other.id != pac_id):
+            return None
+
+        return path
+
+    def record_move(self, pac, new_pos):
+        del self.pacs_index[pac.pos]
+        self.pacs_index[new_pos] = pac
+
+        self.cells[pac.pos].status = UNOCCUPIED
+        self.cells[new_pos].status = ALLY_HERE
+
+        self.segment_index[pac.pos].status = UNOCCUPIED
+        self.segment_index[new_pos].status = ALLY_HERE
+
+        pac.pos = new_pos
+
+
+SPEED_DURATION = 5
+ABILITY_COOLDOWN = 10
+
+ROCK = "ROCK"
+PAPER = "PAPER"
+SCISSORS = "SCISSORS"
+
+_COMBOS = ((ROCK, SCISSORS),
+           (SCISSORS, PAPER),
+           (PAPER, ROCK))
+
+WIN_AGAINST = {t1: t2 for t1, t2 in _COMBOS}
+LOSE_AGAINST = {t2: t1 for t1, t2 in _COMBOS}
+
+# Types d'occupation des cases
+UNOCCUPIED = 0
+TARGETTED = 5
+ALLY_HERE = 10
+ENNEMY_HERE = 20
+PREDATOR_HERE = 21
+HARMLESS_HERE = 22
+PREY_HERE = 23
+
+
+class Pac(Base):
+
+    def __init__(self, pac_id, mine):
+        self.id = pac_id
+        self.owner = ME if mine else OPPONENT
+        self.type = None
+
+        self.speed_cooldown = 0
+        self.speed_turns_left = 0
+        self.ability_cooldown = 0
+
+        self.threaten_by = None
+
+        self.action_cmd = ""
+
+    def update(self, x, y, type_id, speed_turns_left, ability_cooldown):
+        self.x = x
+        self.y = y
+        self.type = type_id
+        self.speed_turns_left = speed_turns_left
+
+        self.ability_cooldown = ability_cooldown
+        self.action_cmd = ""
+
+        self.threaten_by = None
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    @pos.setter
+    def pos(self, pos):
+        self.x, self.y = pos
+
+    @property
+    def owned(self):
+        return self.owner == ME
+
+    def can_accelerate(self):
+        return self.ability_cooldown == 0
+
+    def can_switch(self):
+        return self.ability_cooldown == 0
+
+    @property
+    def is_accelerated(self):
+        return self.speed_turns_left > 0
+
+    def win_against(self, other_pac):
+        if other_pac.owned:
+            raise Exception("Why would you fight against your friend?")
+        return WIN_AGAINST[self.type] == other_pac.type
+
+    def lose_against(self, other_pac):
+        if other_pac.owned:
+            raise Exception("Why would tou fight against your friend?")
+        return LOSE_AGAINST[self.type] == other_pac.type
+
+    def move(self, x, y):
+        self.action_cmd = f"MOVE {self.id} {x} {y}"
+
+    def speed_up(self):
+        if self.speed_cooldown > 0:
+            raise Exception("Speed cooldown still run")
+        self.ability_cooldown = ABILITY_COOLDOWN
+        self.speed_turns_left = SPEED_DURATION
+
+        self.action_cmd = f"SPEED {self.id}"
+
+    def switch(self, new_type):
+        if new_type == self.type:
+            raise Exception('has already the type {new_type}')
+        self.type = new_type
+        self.ability_cooldown = ABILITY_COOLDOWN
+        self.action_cmd = f"SWITCH {self.id} {new_type}"
+
+    def switch_to_beat_up(self, other_pac):
+        new_type = LOSE_AGAINST[other_pac.type]
+        self.switch(new_type)
+
+    def wait(self):
+        self.action_cmd = f"MOVE {self.id} {self.x} {self.y}"
+
+
+class Pellet(Base):
+    def __init__(self, x, y, value=1):
+        self.x = x
+        self.y = y
+        self.value = value
+
+
+# Main loop
+
+ROUND = 0
+
+_, height = [int(i) for i in l_input().split()]
+grid = Grid.from_matrix([list(l_input()) for _ in range(height)])
+
+me = Player(ME)
+opponent = Player(OPPONENT)
+
+while 1:
+
+    cmds = []
+
+    log(f"--- ROUND {ROUND} ---")
+
+    me.score, opponent.score = [int(i) for i in l_input().split()]
+
+    visible_pacs = []
+    visible_pac_count = int(l_input())  # all your pacs and enemy pacs in sight
+    for _ in range(visible_pac_count):
+        pac_id, is_mine, x, y, type_id, speed_turns_left, ability_cooldown = l_input().split()
+        visible_pacs.append(
+            (int(pac_id), is_mine != "0", int(x), int(y), type_id, int(speed_turns_left), int(ability_cooldown)))
+
+    visible_pellets = []
+    visible_pellet_count = int(l_input())  # all pellets in sight
+    for _ in range(visible_pellet_count):
+        x, y, v = [int(j) for j in l_input().split()]
+        visible_pellets.append((x, y, v))
+
+    log('> input fetched')
+
+    grid.update(visible_pacs, visible_pellets)
+    log('> grid updated')
+    log(grid.print_())
+
+    if LOG_INPUT:
+        log(INPUTS)
+
+    for pac in me.pacs:
+        grid.segmentize()
+        grid.print_segments()
+
+        log(f'# Pac {pac.id}')
+
+        cached_path = grid.get_cached_path(pac.id)
+
+        if pac.threaten_by:
+            log('<!> Threaten by: ', pac.threaten_by)
+
+        if cached_path and len(cached_path) > 3 and pac.can_accelerate() and not pac.threaten_by:
+            pac.speed_up()
+            continue
+
+        if pac.threaten_by and pac.can_switch():
+            switch_to = next(
+                type_ for type_, against in WIN_AGAINST.items() if against == grid.pacs[pac.threaten_by].type)
+            log(f'Switch to resist! >> {switch_to}')
+            pac.switch(switch_to)
+            continue
+
+        if cached_path is None:
+            log('routefinding')
+            route = grid.route_finding(pac.pos)
+            if route:
+                path = grid.route_to_path(route, pac.pos)
+                grid.cache_path(pac.id, path)
+                log('> done')
+            else:
+                path = []
+        else:
+            path = cached_path
+            log('use cached path')
+
+        for pos in path[:2]:
+            grid.cells[pos].status = TARGETTED
+
+        #         log(f"path {path[:10]}...")
+        if path:
+            if pac.is_accelerated and len(path) > 1:
+                next_ = path.pop(1)
+            else:
+                next_ = path.pop(0)
+        else:
+            log('error: no path given')
+            next_ = next((n for n in grid.neighbors(pac.x, pac.y) if grid.cells[n].movable), None)
+
+        if next_:
+            pac.move(*next_)
+            grid.record_move(pac, next_)
+            continue
+
+        log(f"no action given, wait")
+        pac.wait()
+
+    print("|".join([pac.action_cmd for pac in me.pacs]))
+    INPUTS = []
+    ROUND += 1

+ 1017 - 0
spring2022/spring2022.py

@@ -0,0 +1,1017 @@
+import sys
+import time
+import heapq
+import math
+
+debug = True
+
+t0 = time.time()
+
+
+# todo: attacking strategy: place one guy nearer to opponent hq
+# todo: considerate shielding monsters threatening opponent hq
+# todo: review the order heroes are playing in each strategy
+
+
+def log(*msg):
+    if debug:
+        print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr, flush=True)
+
+
+def time_to(total, step):
+    """ number of steps to reach total """
+    return total // step + (1 if total % step > 0 else 0)
+
+
+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 GridItem(BaseClass):
+    def __init__(self, pos=(-1, -1)):
+        self._previous_pos = None
+        self._pos = pos
+
+    @property
+    def x(self):
+        return self.pos[0]
+
+    @property
+    def y(self):
+        return self.pos[1]
+
+    @property
+    def pos(self):
+        return self._pos
+
+    @pos.setter
+    def pos(self, pos):
+        self._previous_pos = self.pos
+        self._pos = pos
+
+    @property
+    def previous_pos(self):
+        return self._previous_pos
+
+
+class HeadQuarter(GridItem):
+    SIGHT = 6000
+
+    def __init__(self, pos):
+        super().__init__(pos)
+        self.health = 3
+        self.mana = 10
+
+
+class Role:
+    NAME = "None"
+    POSITION = (1500, 1500)
+    ALT_POSITIONS = []
+
+    def __repr__(self):
+        return self.__class__.__name__
+
+
+class Defender(Role):
+    NAME = "Defender"
+    POSITION = (1250, 1250)
+
+
+class DefenderRight(Defender):
+    NAME = "Defender (right)"
+    POSITION = (500, 3000)
+
+
+class DefenderLeft(Defender):
+    NAME = "Defender (left)"
+    POSITION = (3000, 500)
+
+
+class GateKeeper(Role):
+    NAME = "Gatekeeper"
+    POSITION = (4500, 3000)
+
+
+class GateKeeperRight(GateKeeper):
+    NAME = "Gatekeeper (right)"
+    POSITION = (1500, 5000)
+
+
+class GateKeeperLeft(GateKeeper):
+    NAME = "Gatekeeper (left)"
+    POSITION = (5500, 1000)
+
+
+class Hunter(Role):
+    NAME = "Hunter"
+    POSITION = (6000, 4000)
+    ALT_POSITIONS = [(3500, 6500), (8000, 1500)]
+
+
+class HunterRight(Hunter):
+    NAME = "Hunter (right)"
+    POSITION = (3500, 6500)
+    ALT_POSITIONS = [(1500, 7500), (6000, 4000)]
+
+
+class HunterLeft(Hunter):
+    NAME = "Hunter (left)"
+    POSITION = (8000, 1500)
+    ALT_POSITIONS = [(9000, 500), (6000, 4000)]
+
+
+class Attacker(Role):
+    NAME = "Attacker"
+    POSITION = (7500, 4000)
+    ALT_POSITIONS = [(5000, 6500), (8000, 2000)]
+
+
+class AttackerRight(Attacker):
+    NAME = "Attacker (right)"
+    POSITION = (5000, 6500)
+    ALT_POSITIONS = [(1500, 7500), (6000, 4000)]
+
+
+class AttackerLeft(Attacker):
+    NAME = "Attacker (left)"
+    POSITION = (8000, 2000)
+    ALT_POSITIONS = [(9000, 500), (6000, 4000)]
+
+
+class Saboteur(Role):
+    NAME = "Saboteur"
+    POSITION = (15000, 6000)
+    ALT_POSITIONS = [(12000, 3500), (10000, 6000)]
+
+
+class Strategy(BaseClass):
+    def __init__(self, roles, default=Defender):
+        self.roles = roles
+        self.pos = 0
+        self.default = default
+
+    def pick(self):
+        """ Get the next role in line for the hero, or defender by default """
+        try:
+            role = self.roles[self.pos]
+            self.pos += 1
+            return role
+        except IndexError:
+            return self.default
+
+
+class StrategyFullDefense(Strategy):
+    NAME = "Full Defense"
+
+    def __init__(self):
+        super().__init__([Defender, DefenderLeft, DefenderRight])
+
+
+class StrategyDefensive(Strategy):
+    NAME = "Defensive"
+
+    def __init__(self):
+        super().__init__([Attacker, GateKeeper, Defender])
+
+
+class StrategyBalanced(Strategy):
+    NAME = "Balanced"
+
+    def __init__(self):
+        super().__init__([Attacker, Hunter, Defender])
+
+
+class StrategyPrudentFarm(Strategy):
+    NAME = "Farm (prudent)"
+
+    def __init__(self):
+        super().__init__([HunterLeft, HunterRight, GateKeeper])
+
+
+class StrategyFarm(Strategy):
+    NAME = "Farm"
+
+    def __init__(self):
+        super().__init__([Attacker, HunterLeft, HunterRight])
+
+
+class StrategyAggressive(Strategy):
+    NAME = "Aggressive"
+
+    def __init__(self):
+        super().__init__([Saboteur, Attacker, GateKeeper])
+
+
+class Action(BaseClass):
+    def __init__(self, priority=1000):
+        self.priority = priority
+        self.feasibility = 0
+        self.done = False
+        self._pos = None
+
+    @property
+    def pos(self):
+        return self._pos
+
+    def do(self):
+        self.done = True
+
+    def __repr__(self):
+        return f"{self.__class__.__name__} [{self.priority}/{self.feasibility}]"
+
+
+class Move(Action):
+    def __init__(self, pos, priority=1000):
+        super().__init__(priority)
+        self._pos = pos
+
+    def do(self):
+        super().do()
+        print(f"MOVE {self.pos[0]} {self.pos[1]}")
+
+    def __repr__(self):
+        return f"{self.__class__.__name__} {self.pos} [{self.priority}/{self.feasibility}]"
+
+
+class Explore(Move):
+    pass
+
+
+class MoveAt(Action):
+    def __init__(self, target, priority=1000):
+        super().__init__(priority)
+        self.target = target
+
+    @property
+    def pos(self):
+        return self.target.pos
+
+    def do(self):
+        super().do()
+        print(f"MOVE {self.pos[0]} {self.pos[1]}")
+
+    def __repr__(self):
+        return f"{self.__class__.__name__} {self.target.id} [{self.priority}/{self.feasibility}]"
+
+
+class Defend(MoveAt):
+    pass
+
+
+class Hunt(MoveAt):
+    pass
+
+
+class Reposition(Move):
+    pass
+
+
+class Cast(Action):
+    SPELL = ""
+    MANA_COST = 10
+
+    def __init__(self, priority=1000):
+        super().__init__(priority)
+        self.params = []
+
+    def do(self):
+        super().do()
+        print(f"SPELL {self.SPELL} {' '.join(map(str, self.params))}")
+
+    def __repr__(self):
+        return f"{self.__class__.__name__} {' '.join(map(str, self.params))} [{self.priority}/{self.feasibility}]"
+
+
+class CastWind(Cast):
+    SPELL = "WIND"
+    RADIUS = 1280
+    DEPTH = 2200
+
+    def __init__(self, pos, priority=1000):
+        super().__init__(priority)
+        self.params = pos
+
+
+class CastControl(Cast):
+    SPELL = "CONTROL"
+    RANGE = 2200
+
+    def __init__(self, target_id, pos, priority=1000):
+        super().__init__(priority)
+        x, y = pos
+        self.params = [target_id, x, y]
+
+
+class CastShield(Cast):
+    SPELL = "SHIELD"
+    RANGE = 2200
+    DURATION = 12
+
+    def __init__(self, target_id, priority=1000):
+        super().__init__(priority)
+        self.params = [target_id]
+
+
+class Wait(Action):
+    def do(self):
+        print(f"WAIT")
+
+
+class Entity(GridItem):
+    TYPE_MONSTER = 0
+    TYPE_HERO = 1
+    TYPE_OPPONENT_HERO = 2
+
+    def __init__(self, id_, pos=(-1, -1)):
+        super().__init__(pos)
+        self.id = id_  # Unique identifier
+        self.shield_life = -1  # Count down until shield spell fades
+        self.health = -1  # Remaining health of this monster
+        self.is_controlled = False
+
+        # Computed properties
+        self.visible = False
+        self.distance_to_hq = None
+        self.moving_time_to_hq = None
+        self.distance_to_opponent_hq = None
+        self.moving_time_to_opponent_hq = None
+        self.distance_from_entity = {}
+        self.moving_time_from_entity = {}
+        self.go_toward_hq = False
+        self.go_toward_opponent_hq = False
+        self.is_on_my_side = False
+        self.is_on_opponent_side = False
+        self.camper = False
+
+    def init_round(self):
+        self.visible = False
+        self.distance_to_hq = None
+        self.moving_time_to_hq = None
+        self.distance_to_opponent_hq = None
+        self.moving_time_to_opponent_hq = None
+        self.distance_from_entity = {}
+        self.moving_time_from_entity = {}
+        self.go_toward_hq = False
+        self.go_toward_opponent_hq = False
+        self.is_on_my_side = False
+        self.is_on_opponent_side = False
+        self.camper = False
+
+
+class Hero(Entity):
+    ATTACK = 2
+    ATTACK_RANGE = 800
+    SPEED = 800
+    SIGHT = 2200
+
+    def __init__(self, id_):
+        super().__init__(id_)
+        self.role = Role
+        self.current_role_pos = None
+
+
+class OpponentHero(Hero):
+    def __init__(self, id_):
+        super().__init__(id_)
+
+
+class Monster(Entity):
+    THREAT_FOR_ME = 1
+    THREAT_FOR_OPPONENT = 2
+    NO_THREAT = 0
+    SPEED = 400
+    THREAT_RANGE = 5000
+    ATTACK_RANGE = 300
+
+    def __init__(self, id_):
+        super().__init__(id_)
+        self.near_base = False
+        self.threat_for = self.NO_THREAT
+        self.vx = 0  # Trajectory of this monster
+        self.vy = 0
+
+    @property
+    def next_pos(self):
+        return self.x + self.vx, self.y + self.vy
+
+    @property
+    def threats_hq(self):
+        return self.threat_for == self.THREAT_FOR_ME
+
+    @property
+    def threats_opponent_hq(self):
+        return self.threat_for == self.THREAT_FOR_OPPONENT
+
+    @property
+    def alive(self):
+        return self.health > 0
+
+
+class Grid(BaseClass):
+    MAX_X = 17630
+    MAX_Y = 9000
+
+    def __init__(self):
+        self.threat_level = 0
+        self.round = 0
+        self._index = {}
+        self.hq = None
+        self.opponent_hq = None
+        self.strategy = None
+
+    @property
+    def index(self):
+        return self._index
+
+    def get(self, _id):
+        return self._index[_id]
+
+    @property
+    def entities(self):
+        return [e for e in self._index.values() if e.visible]
+
+    @property
+    def heroes(self):
+        return sorted([v for v in self.entities if type(v) is Hero], key=lambda x: x.id)
+
+    @property
+    def monsters(self):
+        return [v for v in self.entities if type(v) is Monster]
+
+    @property
+    def opponent_heroes(self):
+        return [v for v in self.entities if type(v) is OpponentHero]
+
+    def init_game(self):
+        self.hq = HeadQuarter([int(i) for i in input().split()])
+        self.opponent_hq = HeadQuarter((Grid.MAX_X, Grid.MAX_Y) if self.hq.x == 0 else (0, 0))
+        _ = int(input())  # Heroes per player, always 3
+
+    def update_entity(self, entity):
+        entity.distance_to_hq = self.distance(self.hq.pos, entity.pos)
+        entity.moving_time_to_hq = self.moving_time_to(entity, self.hq.pos)
+        entity.distance_to_opponent_hq = self.distance(self.opponent_hq.pos, entity.pos)
+        entity.moving_time_to_opponent_hq = self.moving_time_to(entity, self.opponent_hq.pos)
+        entity.distance_from_entity = {e.id: self.distance(e.pos, entity.pos) for e in self.entities if
+                                       e.id != entity.id}
+        entity.moving_time_from_entity = {e.id: self.moving_time_to(entity, e.pos) for e in self.entities if
+                                          e.id != entity.id}
+        if type(entity) is Monster:
+            entity.go_toward_hq = (entity.vx <= 0 and entity.vy <= 0 and self.hq.x == 0 or
+                                   entity.vx >= 0 and entity.vy >= 0 and self.hq.x != 0)
+            entity.go_toward_opponent_hq = (entity.vx >= 0 and entity.vy >= 0 and self.opponent_hq.x != 0 or
+                                            entity.vx <= 0 and entity.vy <= 0 and self.opponent_hq.x == 0)
+
+        entity.is_on_my_side = entity.distance_to_hq < entity.distance_to_opponent_hq
+        entity.is_on_opponent_side = not entity.is_on_my_side
+
+        if type(entity) is OpponentHero:
+            if entity.previous_pos and entity.pos == entity.previous_pos and entity.is_on_my_side:
+                entity.camper = True
+
+    def init_round(self):
+        """ get inputs and  reinit the state of entities """
+        self.round += 1
+        log(f"Round {self.round}")
+
+        # *** Update HQ status
+        self.hq.health, self.hq.mana = [int(j) for j in input().split()]
+        self.opponent_hq.health, self.opponent_hq.mana = [int(j) for j in input().split()]
+
+        # *** Reinit the entities state
+        for entity in self.entities:
+            entity.init_round()
+
+        # *** Update entities
+        entity_count = int(input())  # Amount of heroes and monsters you can see
+        for i in range(entity_count):
+
+            _id, _type, _x, _y, shield_life, is_controlled, health, vx, vy, near_base, threat_for = [int(j) for j in
+                                                                                                     input().split()]
+            # Entity is not indexed already
+            if _id not in self.index:
+                if _type == Entity.TYPE_MONSTER:
+                    entity = Monster(_id)
+
+                elif _type == Entity.TYPE_HERO:
+                    entity = Hero(_id)
+
+                elif _type == Entity.TYPE_OPPONENT_HERO:
+                    entity = OpponentHero(_id)
+
+                else:
+                    log("Error: unknown type ({_type})")
+                    break
+                self.index[_id] = entity
+
+            # Update entity
+            entity = self.get(_id)
+            entity.pos = (_x, _y)
+            entity.shield_life = shield_life
+            entity.is_controlled = is_controlled
+            entity.health = health
+            entity.visible = True
+
+            if type(entity) is Monster:
+                entity.near_base = bool(near_base)
+                entity.threat_for = threat_for
+                entity.vx = vx
+                entity.vy = vy
+
+        # purge index
+        self.purge()
+
+        # update entities
+        for entity in self.entities:
+            self.update_entity(entity)
+
+        # *** define strategy and roles
+        self.threat_level = self.compute_threat_level()
+
+        self.strategy = self.define_strategy()
+        for hero in self.heroes:
+            role = self.strategy.pick()
+            if hero.role != role:
+                hero.current_role_pos = None
+            elif hero.pos in self.get_role_positions(hero.role):
+                # remember as the last occupied role position
+                hero.current_role_pos = hero.pos
+            hero.role = role
+
+        log(f'## Threat: {self.threat_level} - Strategy: {self.strategy.NAME} ##')
+        log("Heroes: {}".format(','.join(
+            [f"<{h.id},P:{h.pos},M:{h.moving_time_to_hq},O:{int(h.is_on_opponent_side)}>" for h in self.heroes])))
+        log("Monsters: {}".format(','.join(
+            [f"<{m.id},P:{m.pos},T:{int(m.threats_hq)},V:{int(m.visible)},M:{m.moving_time_to_hq},"
+             f"N:{int(m.go_toward_hq)},O:{int(m.go_toward_opponent_hq)},S:{int(m.shield_life > 0)}>" for m
+             in self.monsters])))
+        log("Opponents: {}".format(','.join(
+            [f"<{o.id},P:{o.pos},S:{o.shield_life},M:{o.moving_time_to_hq},C:{int(o.camper)}>" for o in self.opponent_heroes])))
+
+    def purge(self):
+        """ remove dead entities from the index """
+        self._index = {k: e for k, e in self._index.items() if type}
+
+        index = {}
+        for entity in self.entities:
+            if isinstance(entity, Monster):
+                if self.visible(entity.next_pos) and not entity.visible:
+                    # entity should be visible, but has not been seen, presumed dead
+                    continue
+
+            index[entity.id] = entity
+
+        self._index = index
+
+    def compute_threat_level(self):
+        level = 0
+        for monster in self.monsters:
+            if monster.threats_hq:
+                level += (monster.health + monster.shield_life) * max(24 - monster.moving_time_to_hq, 0)
+        for enemy in self.opponent_heroes:
+            if enemy.is_on_my_side:
+                level += 50
+
+        return level
+
+    def define_strategy(self):
+        # *** Define strategy
+
+        if self.threat_level > 1400:
+            return StrategyFullDefense()
+        if self.threat_level > 1000:
+            return StrategyDefensive()
+        elif self.threat_level > 800:
+            return StrategyBalanced()
+
+        if self.round < 40:
+            if self.threat_level:
+                return StrategyPrudentFarm()
+            else:
+                return StrategyFarm()
+        elif self.round < 100:
+            return StrategyBalanced()
+        else:
+            return StrategyAggressive()
+
+    def get_action_for(self, hero):
+        actions = []
+
+        k0_defend = 10
+        k0_reposition = 250
+        k0_hunt = 200
+        k0_cast_wind = 150
+        k0_cast_offensive_wind = 220
+        k0_cast_control = 100
+        k0_cast_control_monster = 100
+        k0_cast_offensive_control = 200
+        k0_cast_shield = 150
+        k0_cast_shield_on_monster = 80
+
+        k_attack_time = 5
+        k_hero_nearer = 30
+        k_defend_against_shielded = -20
+        k_defender_priority_bonus = -50
+        k_go_toward_hq = -30
+        k_hunt_distance = 5
+        k_hunt_pv = 2
+        k_position_distance_from_pos = -8
+        k_cast_wind_threats = -1
+        k_cast_wind_enemy = -20
+        k_cast_wind_enemy_on_my_side = -10
+        k_cast_wind_enemy_near_hq = -5
+        k_cast_wind_proximity = 5
+        k_cast_wind_bad_target = 10
+        k_cast_wind_last_defense = -30
+        k_cast_wind_last_defense_limit = 10
+        k_control_health = -2
+        k_control_time = 5
+        k_control_going_toward = -20
+        k_shield_enemy_near = -20
+        k_shield_monster = -1
+        k_shield_monster_distance = 3
+        k_cast_shield_distance = 10
+        k_camper_bonus = -50
+        k_cast_control_on_opponent_defender = 10
+        k_cast_control_on_opponent_defender_threat = -1
+
+        defend_threshold = 18
+        min_control_hp = 15
+        min_shield_distance = max(CastWind.RADIUS, CastControl.RANGE)
+        min_shield_threat = 60
+        offensive_buff = -1
+        offensive_buff_control = -2
+
+        k_feasibility_moving_time = 10
+        k_feasibility_casting_cost = 5
+        k_feasibility_far_from_pos = 5
+        k_feasibility_defender_far_from_pos = 10
+
+        min_mana_priority = 240
+
+        # *** Defend
+        for monster in self.monsters:
+            if monster.threats_hq and monster.moving_time_to_hq < defend_threshold:
+                priority = k0_defend
+
+                # Réduire la priorité en fonction de la distance
+                priority += (k_attack_time * monster.moving_time_to_hq)
+                # TODO: réduire priority pour chaque monstre suppl que l'attaque pourrait toucher
+
+                priority += k_defend_against_shielded * int(monster.shield_life > 0)
+
+                # Réduire la priorité pour chaque héros plus proche du qg
+                for other_hero in [h for h in self.heroes if h is not hero]:
+                    if other_hero.moving_time_to_hq < monster.moving_time_to_hq:
+                        priority += k_hero_nearer
+
+                if isinstance(hero, (Defender, GateKeeper)):
+                    priority += k_defender_priority_bonus
+
+                action = Defend(monster, priority)
+                actions.append(action)
+
+        # *** Position
+        role_pos = self.get_role_position(hero)
+        if hero.pos != role_pos:
+            priority = k0_reposition
+            priority += k_position_distance_from_pos * self.moving_time_to(hero, role_pos)
+            action = Reposition(role_pos, priority)
+            actions.append(action)
+
+        # *** Hunt
+        if hero.is_on_my_side and \
+                not issubclass(hero.role, (Defender, GateKeeper)) and \
+                not (issubclass(hero.role, Saboteur) and hero.is_on_my_side):
+            for monster in self.monsters:
+                if monster.threats_opponent_hq:
+                    # it's defense domain or not worth it
+                    continue
+                priority = k0_hunt
+                priority += k_hunt_distance * hero.moving_time_from_entity[monster.id]
+                priority += k_go_toward_hq * monster.go_toward_hq
+                priority += k_hunt_pv * monster.health
+                action = Hunt(monster, priority)
+                actions.append(action)
+
+        # *** Cast WIND on threats
+        if self.hq.mana >= CastWind.MANA_COST:
+            affected = [e for e in self.in_wind_area(hero) if not e.shield_life]
+            if affected:
+                priority = k0_cast_wind
+
+                # Affected entities
+                priority += k_cast_wind_threats * sum([m.health for m in affected if type(m) is Monster and m.threats_hq])
+                priority += k_cast_wind_enemy * len([m for m in affected if type(m) is OpponentHero])
+                priority += k_cast_wind_enemy_on_my_side * len([m for m in affected if type(m) is OpponentHero and m.is_on_my_side])
+                priority += k_cast_wind_enemy_near_hq * sum([m.moving_time_to_hq for m in affected if type(m) is OpponentHero if m.distance_to_hq < HeadQuarter.SIGHT])
+                priority += k_camper_bonus * len([m for m in affected if type(m) is OpponentHero and m.is_on_my_side and m.camper])
+
+                # Hq average proximity
+                priority += k_cast_wind_proximity * hero.moving_time_to_hq
+
+                # last defence
+                priority += sum([k_cast_wind_last_defense * (k_cast_wind_last_defense_limit - m.moving_time_to_hq) for m in affected if type(m) is Monster and m.moving_time_to_hq < k_cast_wind_last_defense_limit])
+
+                action = CastWind(self.opponent_hq.pos, priority)
+                actions.append(action)
+
+        # *** Cast CONTROL on approaching monsters
+        if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST):
+            possible_targets = [m for m in self.monsters if
+                                m.distance_from_entity[hero.id] < CastControl.RANGE and not m.shield_life]
+            for monster in possible_targets:
+                if monster.health < min_control_hp:
+                    # too weak
+                    continue
+
+                if monster.go_toward_opponent_hq:
+                    # already targeting enemy
+                    continue
+
+                if monster.distance_to_hq <= (monster.THREAT_RANGE + monster.SPEED):
+                    # too late...
+                    continue
+
+                if monster.is_controlled:
+                    # already controlled
+                    continue
+
+                priority = k0_cast_control
+                priority += k_control_health * monster.health
+                priority += k_control_time * min(monster.moving_time_to_opponent_hq, monster.moving_time_to_hq)
+                priority += k_control_going_toward * monster.go_toward_hq
+                action = CastControl(monster.id, self.opponent_hq.pos, priority)
+                actions.append(action)
+
+        # *** Cast CONTROL on campers
+        if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST):
+            possible_targets = [e for e in self.opponent_heroes if
+                                e.distance_from_entity[hero.id] < CastControl.RANGE
+                                and not e.shield_life and e.camper]
+            for enemy in possible_targets:
+                if enemy.is_controlled:
+                    # already controlled
+                    continue
+                priority = k0_cast_control
+                action = CastControl(enemy.id, self.opponent_hq.pos, priority)
+                actions.append(action)
+
+        # *** Cast SHIELD on self
+        opponents_within_casting_range = [o.id for o in self.opponent_heroes if
+                                          o.distance_from_entity[hero.id] < min_shield_distance]
+        if hero.shield_life == 0 and \
+                self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and \
+                opponents_within_casting_range\
+                and self.threat_level > min_shield_threat\
+                and hero.is_on_my_side:
+            # log(f"{hero.id}: shield against {[(o.id, o.pos, o.distance_from_entity[hero.id]) for o in self.opponent_heroes]}")
+            priority = k0_cast_shield
+            priority += k_shield_enemy_near * len(opponents_within_casting_range)
+            priority += k_cast_shield_distance * min(hero.moving_time_to_hq, hero.moving_time_to_opponent_hq)
+            action = CastShield(hero.id, priority)
+            actions.append(action)
+
+        if hero.is_on_opponent_side and not isinstance(self.strategy, (StrategyDefensive, StrategyFullDefense)):
+
+            threat_on_opponent_hq = sum(m.health + m.shield_life for m in self.monsters if m.threats_opponent_hq)
+            threat_on_opponent_hq -= 10 * len([o for o in self.opponent_heroes if o.distance_to_opponent_hq < Monster.THREAT_RANGE])
+            log(f"Offensive buff: {threat_on_opponent_hq}")
+
+            for monster in self.monsters:
+                # cast control on monsters near opponent hq
+                if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST) and monster.distance_from_entity[hero.id] < CastControl.RANGE:
+                    if monster.health < min_control_hp:
+                        # too weak
+                        continue
+
+                    if monster.go_toward_opponent_hq:
+                        # already targeting enemy
+                        continue
+
+                    if monster.shield_life > 0:
+                        continue
+
+                    priority = k0_cast_control_monster
+                    action = CastControl(monster.id, self.opponent_hq.pos, priority)
+                    actions.append(action)
+
+            if threat_on_opponent_hq > 50:
+                for monster in self.monsters:
+                    # cast shield on monsters threatening opponent hq
+                    if self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and monster.distance_from_entity[hero.id] < CastShield.RANGE:
+                        if not monster.threats_opponent_hq or not monster.go_toward_opponent_hq:
+                            # not targeting enemy
+                            continue
+                        if monster.shield_life > 0:
+                            continue
+                        priority = k0_cast_shield_on_monster
+                        priority += k_shield_monster_distance * monster.moving_time_to_opponent_hq
+                        priority += k_shield_monster * monster.health
+                        action = CastShield(monster.id, priority)
+                        actions.append(action)
+
+            # cast wind on monsters threatening opponent hq
+            affected = [e for e in self.in_wind_area(hero) if not e.shield_life]
+            if self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and affected:
+                priority = k0_cast_offensive_wind
+                priority += k_cast_wind_threats * sum([m.health for m in affected if type(m) is Monster and m.threats_opponent_hq and not m.shield_life])
+                priority += k_cast_wind_bad_target * len([e for e in affected if type(e) is OpponentHero and not e.shield_life])
+                priority += k_cast_wind_proximity * hero.moving_time_to_opponent_hq
+                priority += offensive_buff * threat_on_opponent_hq
+                action = CastWind(self.opponent_hq.pos, priority)
+                actions.append(action)
+
+            # cast wind on enemies defending opponent hq
+            affected = [e for e in self.in_wind_area(hero) if not e.shield_life]
+            if self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and affected:
+                priority = k0_cast_offensive_wind
+                priority += k_cast_wind_enemy * len([e for e in affected if type(e) is OpponentHero and not e.shield_life])
+                priority += k_cast_wind_bad_target * len([e for e in affected if type(e) is Monster and not e.shield_life])
+                priority += k_cast_wind_proximity * hero.moving_time_to_opponent_hq
+                priority += offensive_buff * threat_on_opponent_hq
+                action = CastWind(self.hq.pos, priority)
+                actions.append(action)
+
+            for enemy in self.opponent_heroes:
+                if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST) and enemy.distance_from_entity[hero.id] < CastControl.RANGE:
+                    if enemy.is_controlled:
+                        # already controlled
+                        continue
+                    if enemy.shield_life:
+                        continue
+
+                    priority = k0_cast_offensive_control
+                    priority += k_cast_control_on_opponent_defender * enemy.moving_time_to_opponent_hq
+                    priority += k_cast_control_on_opponent_defender_threat * threat_on_opponent_hq
+                    priority += offensive_buff_control * threat_on_opponent_hq
+                    action = CastControl(enemy.id, self.hq.pos, priority)
+                    actions.append(action)
+
+        # Estimate feasibility
+        for action in actions:
+            action.feasibility = -1
+
+            # ignore impossibles actions
+            if isinstance(action, Defend):
+                if action.target.moving_time_to_hq < hero.moving_time_to_hq:
+                    # too late...
+                    continue
+
+            action.feasibility = 0
+
+            if action.pos and not isinstance(action, Reposition):
+                action.feasibility += k_feasibility_moving_time * self.moving_time_to(hero, action.pos)
+
+            if isinstance(action, Cast):
+                action.feasibility += k_feasibility_casting_cost * action.MANA_COST
+
+            action.feasibility += k_feasibility_far_from_pos * self.moving_time_to(hero, hero.current_role_pos or hero.role.POSITION)
+
+        actions = sorted([a for a in actions if a.feasibility >= 0], key=lambda a: a.priority + a.feasibility)
+
+        try:
+            log(actions)
+            return actions[0]
+        except IndexError:
+            return Wait()
+
+    def update_after_action(self, hero, action):
+        if isinstance(action, (Move, MoveAt)):
+            hero.pos = self.position_after_moving(hero, action.pos, hero.SPEED)
+            # log(f"{hero.id}: move to {action.pos}, new pos: {hero.pos}")
+            for monster in self.in_attack_area(hero):
+                monster.health -= hero.ATTACK
+                if monster.health <= 0:
+                    del self._index[monster.id]
+
+        if isinstance(action, Cast):
+            self.hq.mana -= action.MANA_COST
+
+        if isinstance(action, CastWind):
+            affected = self.in_wind_area(hero)
+            for entity in affected:
+                entity.pos = self.position_after_moving(entity, action.params, action.DEPTH)
+                self.update_entity(entity)
+
+        if isinstance(action, CastShield):
+            self.index[action.params[0]].shield_life = CastShield.DURATION
+
+        if isinstance(action, CastControl):
+            target_id, x, y = action.params
+            target = self.index[target_id]
+            target.pos = self.position_after_moving(target, (x, y), target.SPEED)
+            self.update_entity(target)
+            target.is_controlled = True
+            if type(target) is Monster:
+                target.go_toward_opponent_hq = True
+
+        if not isinstance(action, Reposition):
+            # reinit alternative positions if hero has acted elsewhere
+            hero.current_role_pos = None
+
+    def play(self, hero):
+        action = self.get_action_for(hero)
+        log(f"> {action}")
+        action.do()
+        self.update_after_action(hero, action)
+
+    @staticmethod
+    def manhattan(from_, to_):
+        xa, ya = from_
+        xb, yb = to_
+        return abs(xa - xb) + abs(ya - yb)
+
+    @staticmethod
+    def distance(from_, to_):
+        xa, ya = from_
+        xb, yb = to_
+        return int(math.sqrt((xa - xb) ** 2 + abs(ya - yb) ** 2))
+
+    @staticmethod
+    def moving_time_to(entity, target):
+        distance = Grid.distance(entity.pos, target)
+        return time_to(distance, entity.SPEED)
+
+    @staticmethod
+    def position_after_moving(entity, pos, speed):
+        """ get the entity position after it moved toward `pos` from `speed` """
+        x0, y0 = entity.pos
+        x1, y1 = pos
+        dx, dy = (x1 - x0) / (x1 or 1), (y1 - y0) / (y1 or 1)
+
+        return x0 + int(dx * speed), y0 + int(dy * speed)
+
+    def visible(self, pos):
+        return self.distance(self.hq.pos, pos) < self.hq.SIGHT or \
+               any(self.distance(h.pos, pos) < h.SIGHT for h in self.heroes)
+
+    def get_role_positions(self, role):
+        positions = [role.POSITION] + role.ALT_POSITIONS
+        if self.hq.x != 0:
+            return [(self.MAX_X - x, self.MAX_Y - y) for x, y in positions]
+        else:
+            return positions
+
+    def get_role_position(self, hero):
+        """ get the position for the given role """
+        role_positions = self.get_role_positions(hero.role)
+
+        pos = role_positions[0]
+        # log(f"{hero.pos} - {hero.current_role_pos} - {role_positions}")
+        if hero.current_role_pos:
+            try:
+                i = role_positions.index(hero.current_role_pos)
+                pos = role_positions[i + 1]
+            except (ValueError, IndexError):
+                pass
+        return pos
+
+    def in_attack_area(self, hero):
+        return [e for e in self.monsters if e.distance_from_entity[hero.id] <= Hero.ATTACK_RANGE]
+
+    def in_wind_area(self, hero):
+        return [e for e in self.entities if type(e) is not Hero and e.distance_from_entity[hero.id] <= CastWind.RADIUS]
+
+
+def main():
+    # *** init game
+    grid = Grid()
+    grid.init_game()
+
+    while True:
+        # *** get inputs and init round
+        grid.init_round()
+
+        # *** Compute priorities
+        for hero in grid.heroes:
+            grid.play(hero)
+
+
+main()

+ 1076 - 0
unleash/unleash.py

@@ -0,0 +1,1076 @@
+'''
+
+@author: olivier.massot, oct. 2019
+'''
+import heapq
+import sys
+import time
+
+
+# TODO
+# * poser les mines de préférence sur des emplacements qui sont sous radar ennemi
+# * si une case est suspectée d'abriter un radar, regarder s'il se passe 3 tours sans que les ennemis y creuse. Si c'est le cas, baisser l'intérêt de toutes ces cases 
+# * prevoir le mouvements suivant lors du retour au qg
+
+
+debug = True
+verbose_input = False
+
+t0 = time.time()
+
+def log(*msg):
+    if debug:
+        print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr)
+        sys.stderr.flush()
+
+def input_():
+    s = input()
+    if verbose_input:
+        log("I>", s)
+    return s
+
+# OWNER
+ME = 0
+OPPONENT = 1
+
+# ENTITIES AND ITEMS
+NONE = -1
+ROBOT = 0
+OPP_ROBOT = 1
+RADAR = 2
+TRAP = 3
+ORE = 4
+
+# PARAMS
+COOLDOWN = 5
+RADAR_SCOPE = 4
+MOVING = 4
+
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+class Queue(Base):
+    def __init__(self):
+        self.items = []
+        self.indices = {}
+    
+    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, item, priority):
+        heapq.heappush(self.items, (priority, item))
+
+    def fput(self, item, priority):
+        
+        indice = 0
+        if priority in self.indices:
+            indice = self.indices[priority] + 1
+            
+        self.indices[priority] = indice
+        self.put(item, (priority, indice))
+
+    def get(self):
+        return heapq.heappop(self.items)[1]
+
+    @classmethod
+    def from_iter(cls, iterable):
+        q = cls()
+        for item, p in iterable:
+            q.fput(item, p)
+        return q
+           
+
+class Task(Base):
+    def __init__(self, target = None):
+        self.target = target
+
+class GoDig(Task):
+    pass
+
+class GetOre(Task):
+    pass
+
+class BringBackOre(Task):
+    pass
+
+class GetRadar(Task):
+    pass
+
+class GetTrap(Task):
+    pass
+
+class SetRadar(Task):
+    pass
+
+class SetTrap(Task):
+    pass
+
+class Lure(Task):
+    pass
+
+class Dispose(Task):
+    pass
+
+class Sabotage(Task):
+    pass
+
+
+
+class Action(Base):
+    def resolve(self, command):
+        log(f"  >> {command}")
+        print(command)
+    
+class Wait(Action):
+    def resolve(self):
+        print("WAIT")
+        
+class Move(Action):
+    def __init__(self, x, y):
+        self.x = int(x)
+        self.y = int(y)
+    
+    @property
+    def target(self):
+        return self.x, self.y
+    
+    def resolve(self):
+        super().resolve(f"MOVE {self.x} {self.y}")
+
+class Dig(Action):
+    def __init__(self, x, y):
+        self.x = int(x)
+        self.y = int(y)
+    
+    @property
+    def target(self):
+        return self.x, self.y
+    
+    def resolve(self):
+        super().resolve(f"DIG {self.x} {self.y}")
+        
+class Request(Action):
+    def __init__(self, item):
+        self.item = item
+        
+    def resolve(self):
+        super().resolve(f"REQUEST {self.item}")
+        
+class RequestRadar(Request):
+    def __init__(self):
+        super().__init__("RADAR")
+        
+    def resolve(self):
+        me.radar_cooldown = COOLDOWN
+        super().resolve()
+        
+class RequestTrap(Request):
+    def __init__(self):
+        super().__init__("TRAP")
+        
+    def resolve(self):
+        me.trap_cooldown = COOLDOWN
+        super().resolve()
+        
+
+class Entity(Base):
+    def __init__(self, id_, x, y):
+        self.id_ = int(id_)
+        self.x = int(x)
+        self.y = int(y)
+
+    @property
+    def pos(self):
+        return self.x, self.y
+
+    def update(self, x, y, _):
+        self.x = x
+        self.y = y
+
+class Radar(Entity):
+    def __init__(self, id_, x, y):
+        super().__init__(id_, x, y)
+
+class Trap(Entity):
+    def __init__(self, id_, x, y):
+        super().__init__(id_, x, y)
+        self.area = [self.pos] + grid.neighbors(*self.pos, diags = False)
+
+class Ore(Entity):
+    def __init__(self, id_, x, y):
+        super().__init__(id_, x, y)
+
+class Robot(Entity):
+    def __init__(self, id_, x, y, item, owner):
+        super().__init__(id_, x, y)
+        self.item = item if item != NONE else None
+        self.owner = owner
+        
+        self.has_played = False
+        self.moved = False
+        self.may_have_item = False
+        self.may_have_found_ore_at = None
+        self.is_bait = False
+        
+        self.last_action = None
+        self.todo = None
+        self.last_task = None
+
+    def update(self, x, y, item):
+        
+        self.has_played = False
+        self.moved = (x, y) != self.pos
+        self.last_task = self.todo
+        self.todo = None
+        
+        if not self.owned and not self.ko:
+            # try to guess last action
+            if self.moved:
+                self.last_action = Move(x, y)
+            else:
+                if self.in_hq():
+                    self.last_action = Request("?")
+                else:
+                    self.last_action = Dig(x, y)
+        
+            if isinstance(self.last_action, Request):
+                # * if is in hq and did not move: may_have_item
+                self.may_have_item = True
+                log(f"% [Robot {self.id_}] May have item")
+                    
+            elif isinstance(self.last_action, Move) and x == 0:
+                # * If just get in HQ, last may_have_found_ore_at updated as has_ore=true
+                if self.may_have_found_ore_at:
+                    log(f"% [Robot {self.id_}] May have found ore at: {self.may_have_found_ore_at}")
+                    grid.cells[self.may_have_found_ore_at].possibly_ore = True
+                    self.may_have_found_ore_at = None
+        
+            if isinstance(self.last_action, Dig):
+                
+                if self.may_have_item:
+                    # * if not in hq and may_have_item and dig, cell become suspect (and scope with it). may_have_item become false
+                    self.may_have_item = False
+                
+                    may_have_digged = [self.pos] + grid.neighbors(*self.pos)
+                    may_have_digged = [c for c in may_have_digged if c[0] > 0]
+                    
+                    log(f"% [Robot {self.id_}] Suspect cells: {may_have_digged}")
+                    for n in may_have_digged:
+                        grid.cells[n].suspect = 10
+                    
+                    grid.cells[self.pos].suspect = 10
+                    
+                else:
+                    # had no item and digged: may have found ore
+                    if self.may_have_found_ore_at:
+                        grid.cells[self.may_have_found_ore_at].seems_empty = True
+                    self.may_have_found_ore_at = self.pos
+                    
+                    for n in [self.pos] + grid.neighbors(*self.pos):
+                        if grid.cells[n].memorized_ore:
+                            grid.cells[n].memorized_ore -= 0.2
+                            grid.cells[n].suspect -= 2
+
+        if self.owned and not self.ko:
+            if isinstance(self.last_action, Dig):
+                target = grid.cells[self.last_action.target]
+                if item == ORE:
+                    target.had_ore = True
+                    target.memorized_ore -= 1
+                else:
+                    target.dry = True
+                    target.memorized_ore = 0
+                    if target.new_hole:
+                        target.was_empty = True
+                target.suspect = 0
+            
+        self.x = x
+        self.y = y
+        self.item = item if item != NONE else None
+        
+        if self.ko:
+            self.todo = None
+        
+    @property
+    def owned(self):
+        return self.owner == ME
+
+    @property
+    def ko(self):
+        return self.x < 0
+
+    def in_hq(self):
+        return self.x == 0
+    
+    def next_to_hq(self):
+        return self.x <= 1
+
+    @property
+    def hold_radar(self):
+        return self.item == RADAR
+
+    @property
+    def hold_trap(self):
+        return self.item == TRAP
+
+    @property
+    def hold_ore(self):
+        return self.item == ORE
+
+    def go_to(self, pos, around_ok=True):
+        
+        if around_ok:
+            # best position to interact with the target cell (ie: the less dangerous)
+            pos = min([pos] + grid.neighbors(*pos), key=lambda c: (grid.cells[c].moving_cost(), grid.distance(self.pos, c)))
+            log(f"  (best position for action: {pos})")
+            
+        path = grid.path(self.pos, pos)
+        if path:
+            log(f"Path: {path}")
+            new_pos = path[0]
+            self.x, self.y = new_pos
+        else:
+            new_pos = pos
+            log("<!!> No path found")
+        
+        for n in grid.scope2[new_pos]:
+            grid.cells[n].ally_near = True
+            
+        self.move(*new_pos)
+        
+    def go_back_to_hq(self):
+        self.current_digging_target = None
+        c = grid.closest(self.pos, grid.hq)
+        self.go_to(c, around_ok=False)
+        
+
+    def digging_queue(self):
+        q = Queue()
+
+        last_target = None
+        if type(self.last_task) in (GoDig, GetOre):
+            last_target = self.last_task.target
+
+        for c, cell in grid.cells.items():
+            if cell.dry or cell.is_hq or cell.has_trap or cell.suspect:
+                continue
+            if cell.under_radar and not cell.ore:
+                continue # we know there is nothing here
+            
+            p = 0
+            
+            p += ((4 * grid.moving_distance(self.pos, c) + c[0]) // 2) # mean between dist to robot and dist to hq
+            p -= max(cell.interest, 30)
+            
+            if c == last_target:
+                p -= 3 # avoid hesitations
+            
+            q.put(c, p)
+
+        return q
+
+    def best_to_dig(self):
+        try:
+            return self.digging_queue().get()
+        except IndexError:
+            return None
+        
+    def radar_positioning_queue(self):
+        q = Queue()
+
+        last_target = None
+        if type(self.last_task) is SetRadar:
+            last_target = self.last_task.target
+
+        for c, cell in grid.cells.items():
+            if cell.is_hq or cell.under_radar or cell.has_trap or cell.suspect:
+                continue
+            
+            p = 0
+            p += ((4 * grid.moving_distance(self.pos, c) + c[0]) // 2)
+            p -= cell.interest
+            
+            if x in (5, 8, 11, 14, 18, 22, 26) and 2 <= y <= 12:
+                p -= 10
+            if y in (3, 7, 11) and 5 <= x <= 26:
+                p -= 10
+                
+            for c2 in grid.scope4[c]:
+                other = grid.cells[c2]
+                if not (other.dry or other.discovered or other.is_hq or other.has_trap):
+                    p -= 1
+            
+            if c[0] < 5:
+                p += 20
+            
+            if cell.ore: # one stone two birds: we can take the ore while setting the radar
+                p -= 2
+                
+            if c == last_target:
+                p -= 3 # avoid hesitations
+            
+            q.put(c, p)
+            
+        return q
+    
+    def best_for_radar(self):
+        
+        try:
+            return self.radar_positioning_queue().get()
+        except IndexError:
+            grid.no_more_radar = True
+            return None
+
+    def trap_positioning_queue(self):
+        q = Queue()
+        
+        last_target = None
+        if type(self.last_task) is SetTrap:
+            last_target = self.last_task.target
+
+        for c, cell in grid.cells.items():
+            if cell.is_hq or cell.has_trap or cell.suspect:
+                continue
+            
+            if cell.scarecrow: # this cell should already have become suspect to ennemy
+                continue
+            
+            p = 0
+            p += ((4 * grid.moving_distance(self.pos, c) + c[0]) // 2)
+            p -= cell.interest
+            
+            if cell.ore == 2:
+                p -= 30
+            elif cell.ore or cell.possibly_ore or cell.memorized_ore:
+                p -= 10
+            
+            if any(r.pos in grid.scope2[c] for r in me.robots):
+                p += 5
+            
+            if c == last_target:
+                p -= 5 # avoid hesitations
+            
+            q.put(c, p)
+        return q
+            
+    def best_for_trap(self):
+        try:
+            return self.trap_positioning_queue().get()
+        except IndexError:
+            return None
+        
+    def dispose(self):
+        # dispose of the current item in the fisrt place available
+        q = Queue()
+        for c, cell in grid.cells.items():
+            if cell.is_hq or cell.under_trap or cell.ore:
+                continue
+            p = 0
+            p += 3 * grid.manhattan(self.pos, c)
+                
+            q.put(c, p)
+            
+        target = q.get()
+        log(f"Dispose of item {self.item} at {target}")
+        self.go_dig(target)
+    
+    def go_set_radar(self, pos):
+        self.go_dig(pos)
+    
+    def go_set_trap(self, pos):
+        self.go_dig(pos)
+    
+    def go_dig(self, pos):
+        if pos == self.pos or pos in grid.neighbors(*self.pos):
+            if self.item == TRAP:
+                grid.cells[pos].has_trap = True
+                grid.cells[pos].under_trap = True
+            self.dig(*pos)
+        else:
+            self.go_to(pos)
+
+    def collateral_for(self, trap):
+        area = grid.collateral_area_for(trap.pos)
+        return [e for e in self.entities if isinstance(e, Robot) and e.pos in area]
+        
+    # commands
+    def _act(self, action):
+        self.has_played = True
+        self.last_action = action
+        action.resolve()
+
+    def wait(self):
+        self._act(Wait())
+
+    def move(self, x, y):
+        self._act(Move(x, y))
+
+    def dig(self, x, y):
+        self._act(Dig(x, y))
+        
+        if self.is_bait:
+            grid.cells[(x, y)].scarecrow = True
+        self.is_bait = False
+
+    def request(self, item):
+        self._act(Request(item))
+        
+    def request_radar(self):
+        me.radar_cooldown = COOLDOWN
+        self._act(RequestRadar())
+        
+    def request_trap(self):
+        me.trap_cooldown = COOLDOWN
+        self._act(RequestTrap())
+
+class Team():
+    def __init__(self, player):
+        self.player = player
+        self.score = 0
+        self.robots = []
+        
+        self.radar_cooldown = None
+        self.trap_cooldown = None
+
+    def update(self, entities):
+        self.robots = sorted([e for e in entities if isinstance(e, Robot) and e.owner == self.player], key= lambda r: r.id_)
+        
+    def active_robots(self):
+        return [r for r in self.robots if not r.ko]
+        
+class Cell():
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+        self.ore = 0
+        self.hole = 0
+        
+        self.had_ore = False
+        self.suspect = 0
+        self.possibly_ore = False
+        self.new_hole = False
+        self.dry = False
+        self.scarecrow = False
+        self.seems_empty = False
+        self.was_empty = False
+        self.available_ore = 0
+        self.memorized_ore = 0
+        self.under_radar = False
+        self.discovered = False
+        self.ally_near = False
+        
+    @property
+    def pos(self):
+        return self.x, self.y
+    
+    @property
+    def is_hq(self):
+        return self.x == 0
+    
+    def print_state(self):
+        if not self.hole and not self.under_radar:
+            return '?'
+        elif self.has_radar:
+            return 'R'
+        elif self.has_trap:
+            return 'T'
+        elif self.had_ore:
+            return '1'
+        elif self.possibly_ore:
+            return '*'
+        else:
+            return '0'
+
+    def update(self, ore, hole):
+        
+        self.new_hole = hole and not self.hole
+        
+        if self.under_radar and self.ore:
+            self.memorized_ore = self.ore
+        if self.under_radar:
+            self.discovered = True
+        
+        self.ore = int(ore) if ore != '?' else None
+        self.hole = (int(hole) == 1)
+        
+        if self.ore:
+            self.had_ore = True
+        
+        self.interest = 0
+        self.tokens = 0
+        self.has_radar = False
+        self.under_radar = False
+        self.has_trap = False
+        self.under_trap = False
+        self.occupied = False
+        self.under_suspect = False
+        self.ennemy_near = False
+        
+    def moving_cost(self):
+        if self.has_trap:
+            return 15 + 10 * self.ennemy_near + 5 * self.ally_near
+        if self.under_trap or self.suspect:
+            return 10 + 10 * self.ennemy_near + 5 * self.ally_near
+        elif self.under_suspect:
+            return 10 + 5 * self.ennemy_near + 5 * self.ally_near
+        return 10
+
+    def may_have_ore(self):
+        return self.ore or self.memorized_ore or self.possibly_ore
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+    
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, c:{self.cost}>"
+    
+class Grid(Base):
+    w = 30
+    h = 15
+    
+    def __init__(self):
+        self.cells = {(x, y): Cell(x, y) for x in range(Grid.w) for y in range(Grid.h)}
+        self.hq = [(0, y) for y in range(Grid.h)]
+        self.scope2 = {c: self.zone(c, 2) for c in self.cells}
+        self.scope3 = {c: self.zone(c, 3) for c in self.cells}
+        self.scope4 = {c: self.zone(c, 4) for c in self.cells}
+        self.dscope4 = {c: self.dzone(c, 4) for c in self.cells}
+        self.scope5 = {c: self.zone(c, 5) for c in self.cells}
+        self.entities = {}
+        self.at = {}
+        
+        self.no_more_radar = False
+        
+    @property
+    def grid(self):
+        return [[self.cells[(x, y)] for x in range(Grid.w)] for y in range(Grid.h)]
+        
+    def print_grid(self, key=None):
+        if key is None:
+            key = lambda x: x
+        return "\n"+ "\n".join(["".join([f"{key(c)}|" for c in row]) for row in self.grid])
+        
+    def update(self, edata):
+        
+        new_entities = {}
+        just_destroyed = []
+        
+        for e in edata:
+            id_, type_, x, y, item = e
+            
+            if id_ in self.entities:
+                e = self.entities[id_]
+                
+                if e.x > 0 and x < 0:
+                    just_destroyed.append(id_)
+                
+                e.update(x, y, item)
+                new_entities[id_] = e
+            else:
+                if type_ == ROBOT:
+                    e = Robot(id_, x, y, item, ME)
+                elif type_ == OPP_ROBOT:
+                    e = Robot(id_, x, y, item, OPPONENT)
+                elif type_ == RADAR:
+                    e = Radar(id_, x, y)
+                elif type_ == TRAP:
+                    e = Trap(id_, x, y)
+                new_entities[id_] = e
+                
+        # exploded mines
+        exploded = [e.pos for id_, e in self.entities.items() if type(e) is Trap and not id_ in new_entities]
+        for id_, e in self.entities.items():
+            if type(e) is Robot and e.owned and id_ in just_destroyed:
+                # this robot was just destroyed
+                if isinstance(e.last_action, Dig):
+                    exploded.append(e.last_action.target)
+        
+        self.entities = new_entities
+        
+        # indexes
+        self.at = {c: [e for e in self.entities.values() if e.pos == c] for c in self.cells}
+    
+        # update cells states
+        for e in new_entities.values():
+            if isinstance(e, Radar):
+                self.cells[e.pos].has_radar = True
+                for c in self.scope4[e.pos]:
+                    self.cells[c].under_radar = True
+                    self.cells[c].possibly_ore = False
+                    if not self.cells[c].ore:
+                        self.cells[c].memorized_ore = 0
+                    
+            if isinstance(e, Trap):
+                self.cells[e.pos].has_trap = True
+                for c in e.area:
+                    self.cells[c].under_trap = True
+    
+            if isinstance(e, Robot):
+                if e.ko:
+                    continue
+                self.cells[e.pos].occupied = True
+                
+                if e.owner == OPPONENT:
+                    for c in self.scope5[e.pos]:
+                        self.ennemy_near = True
+                
+        # interest of undiscovered cells
+        for c, cell in self.cells.items():
+            cell.available_ore = cell.ore or int(not cell.under_radar and cell.had_ore and not cell.dry)
+            
+        for c, cell in self.cells.items():
+            
+            suspection = cell.suspect * (((200 - round_) // 50) + 1)
+            
+            if cell.ore:
+                cell.interest = 40 - suspection
+            
+            elif int(cell.memorized_ore):
+                cell.interest = 20 - suspection
+            
+            else:
+                for x, y, d in grid.dscope4[c]:
+                    other = grid.cells[(x, y)]
+                    di = (5 - d)
+                    if other.had_ore or other.possibly_ore:
+                        cell.interest += di
+                    if x == 0:
+                        cell.interest -= di # move away from HQ a little
+                    if y == 0 or y == (grid.h - 1):
+                        cell.interest -= 2 # counterbalance the advantage of border cells
+                    if other.was_empty or other.seems_empty:
+                        cell.interest -= di # this was an empty cell
+                cell.interest //= 4
+                            
+                if x < 8 and round_ < 20: # 8th row seems the best to start
+                    cell.interest -= abs(8 - x)
+        
+                if cell.available_ore:
+                    cell.interest += 10
+                
+            if cell.suspect:
+                for c2 in self.neighbors(*c):
+                    grid.cells[c2].under_suspect = True
+    
+        for pos in exploded:
+            log(f"Trap exploded at {pos}, cells around no longer suspects")
+            for c in [pos] + self.neighbors(*pos):
+                self.cells[c].suspect = 0
+    
+        me.update(new_entities.values())
+        opponent.update(new_entities.values())
+    
+    def available_ore(self):
+        return any(c.available_ore for c in self.cells.values())
+    
+    def is_radar_needed(self):
+        if grid.no_more_radar:
+            return False
+        discovered = sum([cell.ore for cell in self.cells.values() if cell.discovered and cell.ore and not cell.suspect and not cell.has_trap])
+        robots_alive = len([r for r in me.robots if not r.ko])
+        if discovered > 4 * robots_alive:
+            return False
+        return True
+    
+    @staticmethod
+    def manhattan(from_, to_):
+        xa, ya = from_
+        xb, yb = to_
+        return abs(xa - xb) + abs(ya - yb) 
+
+    @staticmethod
+    def distance(from_, to_): # alias
+        return Grid.manhattan(from_, to_)
+    
+    @staticmethod
+    def moving_distance(from_, to_):
+        q, r = divmod(max(0, Grid.manhattan(from_, to_) - 1), MOVING)
+        return q + int(bool(r))
+    
+    def neighbors(self, x, y, diags=False, inside_only=True):
+        n = [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
+        if diags:
+            n += [(x - 1, y - 1), (x + 1, y - 1), (x - 1, y + 1), (x + 1, y + 1)]
+        if inside_only:
+            n = [(x, y) for x, y in n if 0 <= x < Grid.w and 0 <= y < Grid.h]
+        return n
+    
+    @classmethod
+    def zone(cls, center, radius):
+        return [(x, y) for x in range(0, cls.w) for y in range(0, cls.h) if cls.manhattan(center, (x, y)) <= radius]      
+    
+    @classmethod
+    def dzone(cls, center, radius):
+        z = []
+        for y in range(0, cls.h):
+            for x in range(0, cls.w):
+                d = cls.manhattan(center, (x, y))
+                if d <= radius:
+                    z.append((x, y, d))
+        return z
+                    
+    def collateral_area_for(self, trap_pos, current=None):
+        area = {trap_pos}
+        for n in self.neighbors(*trap_pos):
+            if current and n in current:
+                continue
+            area.add(n)
+            if self.cells[n].has_trap:
+                area |= self.collateral_area_for(n, area)
+        return area
+    
+    @staticmethod
+    def closest(from_, in_):
+        return min(in_, key=lambda x: Grid.manhattan(from_, x))
+    
+    def path(self, start, target):
+        nodes = Queue()
+        its, break_on = 0, 4000
+        
+        origin = PathNode(*start)
+        nodes.put(origin, 0)
+        
+        neighbors = []
+
+        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.scope4[current]
+
+            for x, y in neighbors:
+                its += 1
+                if its > break_on:
+                    log("<!> pathfinding broken")
+                    return None
+                
+                if (x, y) == current.parent:
+                    continue
+                
+                cell = self.cells[(x, y)]
+                moving_cost = cell.moving_cost()
+                    
+                if moving_cost < 0:
+                    continue
+                    
+                cost = current.cost + moving_cost
+                priority = cost + 5 * Grid.manhattan((x, y), target)
+                
+                node = PathNode(x, y, current)
+                node.cost = cost
+                nodes.put(node, priority)
+                
+        return None
+    
+    
+Grid.w, Grid.h = [int(i) for i in input_().split()]
+
+grid = Grid()
+me = Team(ME)
+opponent = Team(OPPONENT)
+round_ = 0
+
+while True:
+    round_ += 1
+    # get scores
+    me.score, opponent.score = [int(i) for i in input_().split()]
+    log("# Get new data")
+    # get cells data
+    for y in range(Grid.h):
+        s = input_().split()
+        data = list(zip(s[::2], s[1::2]))
+        for x, args in enumerate(data):
+            grid.cells[(x, y)].update(*args)
+    
+    # various data
+    n, me.radar_cooldown, me.trap_cooldown = [int(i) for i in input_().split()]
+#     log("Cooldown: ", me.radar_cooldown, me.trap_cooldown)
+    
+    # entities data
+    e_data = [(int(j) for j in input_().split()) for i in range(n)]
+    
+    grid.update(e_data)
+#     log("Entities: ", grid.entities)
+    
+#     log(grid.print_grid(key=lambda c: "{}".format("x" if c.suspect else "_")))
+#     log(grid.print_grid(key=lambda c: c.print_state()))
+    
+    ## attributing tasks
+    # Predefined tasks
+    log("# Attribute task")
+    get_radar_affected = False
+    get_trap_affected = False
+    sabotage_at = []
+    
+    for r in me.active_robots():
+    
+        if r.hold_radar:
+            target = r.best_for_radar()
+            
+            if target:
+                r.todo = SetRadar(target)
+                grid.cells[target].tokens += 1
+            else:
+                log(f"  <!> No place found for the radar, dispose")
+                r.todo = Dispose()
+            
+        elif r.hold_trap:
+            target = r.best_for_trap()
+    
+            if target:
+                r.todo = SetTrap(target)
+                grid.cells[target].tokens += 1
+            else:
+                log(f"  <!> No place found for the trap, dispose")
+                r.todo = Dispose()
+    
+        elif r.hold_ore:
+            r.todo = BringBackOre()
+            
+        elif me.radar_cooldown == 0 and not get_radar_affected and (r.next_to_hq() or not grid.available_ore()):
+            if grid.is_radar_needed():
+                r.todo = GetRadar()
+                get_radar_affected = True
+            else:
+                log("  * No radar needed at this time")
+            
+#         elif me.trap_cooldown == 0 and r.in_hq() and round_ < 170 and not get_trap_affected:
+#             r.todo = GetTrap()
+#             get_trap_affected = True
+#         
+#         elif r.in_hq() and round_ < 150 and not isinstance(r.last_action, Wait) and random.randint(1,3) == 2:
+#             r.todo = Lure()
+            
+        else:
+            for c in [r.pos] + grid.neighbors(*r.pos):
+                cell = grid.cells[c]
+                if (cell.suspect and cell.may_have_ore()) or cell.has_trap and not c in sabotage_at:
+                    collateral_area = grid.collateral_area_for(c)
+                    allies_victims = [r_ for r_ in me.robots if r_.pos in collateral_area]
+                    opponent_victims = [r_ for r_ in opponent.robots if r_.pos in collateral_area]
+                    
+                    if len(opponent_victims) > len(allies_victims) or (cell.suspect and len(opponent_victims) == len(allies_victims)):
+                        r.todo = Sabotage(c)
+                        sabotage_at.append(c)
+                        log(f"Going for sabotage at {c}, area: {len(collateral_area)}, allies: {[x.id_ for x in allies_victims]}, ennemies: {[x.id_ for x in opponent_victims]}")
+                        break
+
+    # To-Priorize tasks
+    task_queue = Queue()
+    
+    # exploration
+    for r in me.active_robots():
+        for c, cell in grid.cells.items():
+            if cell.dry or cell.is_hq or cell.has_trap:
+                continue
+            if cell.under_radar and not cell.ore:
+                continue # we know there is nothing here
+            
+            if cell.ore or int(cell.memorized_ore):
+                task_queue.fput((r, GetOre(c)), grid.distance(r.pos, c) - cell.interest)
+            else:
+                task_queue.fput((r, GoDig(c)), grid.distance(r.pos, c) - cell.interest)
+    
+    while any(r.todo is None for r in me.robots if not r.ko):
+        try:
+            r, task = task_queue.get()
+        except IndexError:
+            log("<!> Not enough task for everyone")
+            break
+
+        if r.todo:
+            continue
+
+        cell = grid.cells[task.target]
+        if isinstance(task, GetOre) and cell.tokens >= cell.available_ore:
+            continue
+
+        r.todo = task
+        cell.tokens += 1
+
+    log("# Execution")
+             
+    for r in me.robots:
+        
+        log(f"** Robot {r.id_} [{r.pos}] plays (has item: {r.item})")
+        if r.ko:
+            log(" -- KO --")
+            r.wait()
+            continue
+        
+        log(f"> Task: {r.todo}")
+        
+        if type(r.todo) is GetRadar:
+            log(f"  Go get a radar at HQ")
+            if r.in_hq():
+                r.request_radar()
+            else:
+                r.go_back_to_hq()
+                    
+        elif type(r.todo) is GetTrap:
+            log(f"  Go get a trap at HQ")
+            if r.in_hq():
+                r.request_trap()
+            else:
+                r.go_back_to_hq()
+            
+        elif type(r.todo) is BringBackOre:
+            log("  Bring back ore to HQ")
+            r.go_back_to_hq()
+            
+        elif type(r.todo) is Lure:
+            log("  $ Wait to trick")
+            r.is_bait = True
+            r.wait()
+                
+        elif type(r.todo) is GetOre:
+            log(f"  Go get ore at {r.todo.target}")
+            r.go_dig(r.todo.target)
+            
+        elif type(r.todo) is GoDig:
+            log(f"  Go dig at {r.todo.target}")
+            r.go_dig(r.todo.target)
+            
+        elif type(r.todo) is SetRadar:
+            log(f"  Go set a radar at {r.todo.target}")
+            r.go_set_radar(r.todo.target)
+            
+        elif type(r.todo) is SetTrap:
+            log(f"  Go set a trap at {r.todo.target}")
+            r.go_set_trap(r.todo.target)
+            
+        elif type(r.todo) is Dispose:
+            log(f"  Dispose of item")
+            r.dispose()
+            
+        elif type(r.todo) is Sabotage:
+            log(f"  Sabotage at {r.todo.target}")
+            r.go_dig(r.todo.target)
+            
+        else:
+            log("  <!> No task (or unknown)")
+
+        
+        if not r.has_played:
+            log("  <!> Has not played")
+            r.wait()
+