omassot 6 лет назад
Сommit
2cfe39f98f

+ 0 - 0
__init__.py


BIN
caribbean/Capture.PNG


+ 0 - 0
caribbean/__init__.py


+ 1385 - 0
caribbean/script.py

@@ -0,0 +1,1385 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+import time
+
+
+# TODO:
+# * find a way to change direction without slowing down if possible
+# * Try to integer extra steps into the path algo, instead of adding independents paths
+# * Increase the shooting rate in the second part of the game (no barrels left)
+# * get_shooting_spots: try to intercept ennemy
+# * make a difference between moving cost, mines, cannonballs, out of grid and other ships
+# * Try to temporize when taking barrel, the more the ship can wait the better
+# * try to cut the road to ennemies when getting barrels
+# * if an ennemy has more rum that every owned ship: hunt!
+
+# Enhancements
+# * interception trajectories
+# * paths with extra-steps
+# * Part 1: Speed up
+# * Part 2: Increase shooting rate
+
+debug = True
+
+t0 = time.time()
+
+def log(*msg):
+    if debug:
+        print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr)
+
+current_turn = 0
+
+class Queue():
+    def __init__(self):
+        self.items = []
+    
+    def __bool__(self):
+        return bool(self.items)
+
+    def __repr__(self):
+        return str(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]
+
+    @classmethod
+    def merge(cls, *args, reverse=False):
+        q = cls()
+        q.items = list(heapq.merge(*[a.items for a in args], key=lambda x: x[1], reverse=reverse))
+        return q
+
+class InterestQueue(Queue):
+    def __add__(self, other):
+        self.items += other.items
+        return self
+    
+    def put(self, item):
+        heapq.heappush(self.items, item)
+        
+    def get(self):
+        return heapq.heappop(self.items)
+    
+    @classmethod
+    def merge(cls, *args, reverse=False):
+        q = cls()
+        q.items = list(heapq.merge(*[a.items for a in args], reverse=reverse))
+        return q
+    
+class ObjectivesQueue(InterestQueue):
+    
+    @classmethod
+    def re_eval(cls, q, pos=None, d=None):
+        new_q = cls()
+        while q:
+            o = q.get()
+            o.eval(pos, d)
+            new_q.put(o)
+        return new_q
+    
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+
+
+class BaseObjective(Base):
+    def __init__(self, target):
+        self.target = target
+        self.interest = 0
+
+    def __lt__(self, other):
+        return self.interest < other.interest
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: target={self.target.id};int={self.interest})>"
+
+    def _pre_eval(self, pos = None, d = None):
+        self.distance = Grid.manhattan(pos, self.target.pos) if pos is not None else 0
+        self.alignment = abs(Grid.diff_directions(Grid.direction_to(*pos, *self.target.pos), d)) if d is not None else 0
+        
+    def eval(self, pos = None, d = None):
+        self._pre_eval(pos, d)
+        self._compute_interest()
+        
+    def _compute_interest(self):
+        self.interest = 7 * self.distance + 3 * self.alignment
+        
+class GetBarrel(BaseObjective):
+    
+    def _pre_eval(self, pos = None, d = None):
+        super()._pre_eval(pos, d)
+        self.ennemy_near = any(Grid.manhattan(e.next_pos, self.target.pos) < self.distance for e in grid.ennemy_ships)
+    
+    def _compute_interest(self):
+        self.interest = 6 * self.distance + 9 * self.alignment + 3 * self.target.dispersal + self.target.mine_threat * 2 + 12 * self.ennemy_near
+        if self.distance <= 2 and self.alignment > 1:
+            # dead angle
+            self.interest += 36
+
+class Attack(BaseObjective):
+    def _compute_interest(self):
+        self.interest = 7 * self.distance + 3 * self.alignment + self.target.stock // 4 - 20 * self.target.blocked_since
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        n.orientation = 0
+        return n
+    
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, c:{self.cost}, o:{self.orientation}>"
+
+class Grid(Base):
+    w = 23
+    h = 21
+    _neighbors = {}
+    _next_cell = {}
+        
+    def __init__(self):
+        self.load_entities({})
+        
+    @classmethod
+    def preload(cls):
+        cls._neighbors = {}
+        for x in range(-1, cls.w + 1):
+            for y in range(-1, cls.h + 1):
+                cls.cache_neighbors(x, y)
+                
+        cls._next_cell = {}
+        for x in range(0, cls.w):
+            for y in range(0, cls.h):
+                cls.cache_next_cell(x, y)
+                
+        for x in range(0, cls.w):
+            for y in range(0, cls.h):
+                Ship.cache_area(x, y)
+    
+    @classmethod
+    def contains(cls, x, y):
+        return 0 <= x < cls.w and 0 <= y < cls.h 
+    
+    def __contains__(self, key):
+        return self.contains(*key)
+
+    def __iter__(self):
+        for item in ((x, y) for x in range(self.w) for y in range(self.h)):
+            yield item
+
+    # data
+    
+    def load_entities(self, entities):
+        
+        # special: mines too far from ships are not recorded but still exist
+        ghost_mines = []
+        if hasattr(self, "mines"):
+            for m in self.mines:
+                if not m.id in entities:
+                    if all((self.manhattan(m.pos, ship.pos) > 5) for ship in self.owned_ships):
+                        m.ghost = True
+                        ghost_mines.append(m)
+
+        self.entities = entities
+        self.index = {}
+        self.ships = []
+        self.owned_ships = []
+        self.ennemy_ships = []
+        self.ships = []
+        self.barrels = []
+        self.mines = []
+        self.cannonballs = []
+        
+        self.threat = {}
+        
+        for e in list(entities.values()) + ghost_mines:
+            self.index[e.pos] = e
+            type_ = type(e)
+            
+            if type_ is Ship:
+                self.ships.append(e)
+                if e.owned:
+                    self.owned_ships.append(e)
+                else:
+                    self.ennemy_ships.append(e)
+
+            elif type_ is Barrel:
+                self.barrels.append(e)
+
+            elif type_ is Mine:
+                self.mines.append(e)
+
+            elif type_ is Cannonball:
+                self.cannonballs.append(e)
+                if not e.pos in self.threat or e.countdown < self.threat[e.pos]:
+                    self.threat[e.pos] = e.countdown
+                
+        for s in self.owned_ships:
+            s.allies = [other for other in self.owned_ships if other is not s]
+        for s in self.ennemy_ships:
+            s.allies = [other for other in self.ennemy_ships if other is not s]
+
+        for s in self.ships:
+            s.next_pos_proba(2)
+
+        self.next_to_mine = {}
+        for m in self.mines:
+            for n in self.neighbors(*m.pos):
+                self.next_to_mine[n] = m
+
+        self.next_to_barrel = {}
+        for b in self.barrels:
+            for n in self.neighbors(*b.pos):
+                self.next_to_barrel[n] = b
+
+        self.update_moving_costs()
+        
+        grav_center = self.barrels_gravity_center()
+        for b in self.barrels:
+            b.dispersal = Grid.manhattan(grav_center, b.pos) if grav_center != None else 0
+            b.mine_threat = any(type(self.at(*c)) is Mine for c in self.neighbors(*b.pos))
+                
+        for s in self.owned_ships:
+            
+            s._can_move = {c: (s.moving_cost(*c) < 1000) 
+                           for c in [s.front, s.front_left, s.left, s.front_right,
+                                     s.right, s.back_left, s.back_right]}
+            
+            s.objectives = ObjectivesQueue()
+            s.ennemies = ObjectivesQueue()
+            
+            for b in self.barrels:
+                obj = GetBarrel(b)
+                obj.eval(s.prow, s.orientation)
+                s.objectives.put(obj)
+                
+            for e in self.ennemy_ships:
+                obj = Attack(e)
+                obj.eval(s.pos, s.orientation)
+                s.ennemies.put(obj)
+        
+    def at(self, x, y):
+        try:
+            return self.index[(x, y)]
+        except KeyError:
+            return None
+        
+    @classmethod
+    def is_border(cls, x, y):
+        return x == -1 or y == -1 or x == cls.w or y == cls.h
+        
+    def collision_at(self, x, y):
+        e = self.at(x, y)
+        return type(e) in [Mine, Ship, Cannonball] or not (x, y) in self.__iter__()
+        
+    def barrels_gravity_center(self):
+        wx, wy, wtotal = 0,0,0
+        for b in self.barrels:
+            wx += (b.x * b.amount) 
+            wy += (b.y * b.amount)
+            wtotal += b.amount
+        return (wx // wtotal, wy // wtotal) if wtotal else None
+
+    def update_moving_costs(self):
+        base_costs = {}
+        self.collisions = []
+        
+        for x in range(-1, self.w + 1):
+            for y in range(-1, self.h + 1):
+                base_costs[(x, y)] = 10 # base moving cost
+                
+        for x, y in base_costs:
+            if x in (-1, self.w + 1) or y in (-1, self.h):
+                base_costs[(x, y)] = 1000 # out of the map
+            elif x in (0, self.w - 1) or y in (0, self.h - 1):
+                base_costs[(x, y)] = 15 # borders are a little more expensive     
+        
+        for c in self.next_to_mine:
+            base_costs[c] += 30
+        for m in self.mines:
+            base_costs[m.pos] += 1000
+            
+            if m.pos in self.threat:
+                if self.threat[m.pos] <= 2:
+                    # avoid the area of a mines going to explode
+                    for n in self.neighbors(*m.pos):
+                        base_costs[n] += 1000
+                        
+        for c in self.cannonballs:
+            if 0 < c.countdown <= 2:
+                base_costs[c.pos] += 1000
+                
+        for ship in self.ships:
+            ship._moving_costs = {}
+            ship._moving_costs.update(base_costs)
+            for other in self.ships:
+                if other is ship:
+                    continue
+                dist = self.manhattan(ship.pos, other.pos)
+                if dist > 6:
+                    continue
+                for c in self.zone(other.pos, 3):
+                    ship._moving_costs[c] += 25
+                
+                next_positions = other.next_pos_proba()
+                for c, proba in next_positions[1].items():
+                    if proba >= 20:
+                        ship._moving_costs[c] = ship._moving_costs.get(c, 0) + 20 * proba
+                if ship.owned and not other.owned and other.behind in ship._moving_costs:
+                    # the other ship could mine
+                    ship._moving_costs[other.behind] += 100
+
+    def shooting_spot(self, ship, target, current=None):
+        shooting_spots = Queue()
+        target_pos = target.next_pos if type(target) is Ship else target.pos
+        
+        for x, y in self.zone(target_pos, 8):
+            if ship.moving_cost(x, y) > 100:
+                continue
+            if self.manhattan((x, y), target_pos) <= 2:
+                continue
+            
+            interest = 0 # the lower the better
+            
+            if (x, y) == current:
+                interest -= 20
+            
+            interest += ship.moving_cost(x, y)
+            
+            # avoid cells too close from borders
+            if not 3 < x < (self.w - 3):
+                interest += 50
+            if not 3 <= y < (self.h - 3):
+                interest += 50
+            
+            # priorize cells in the current direction
+            diff = abs(Grid.diff_directions(ship.orientation, Grid.direction_to(*ship.prow, x, y)))
+            interest += 10 * abs(diff)
+            
+            # priorize spots at distance 6 from active ship
+            interest += (10 * abs(6 - self.manhattan((x, y), ship.pos)))
+            
+            # priorize spots at distance 6 from targetted ship
+            interest += (10 * abs(6 - self.manhattan((x, y), target.pos)))
+            
+            shooting_spots.put((x, y), interest)
+        return shooting_spots.get()
+    
+    def runaway_spot(self, ship):
+        runaway_spot = Queue()
+        
+        for x, y in iter(self):
+            if ship.moving_cost(x, y) > 100:
+                continue
+            
+            interest = 0 # the lower the better
+            
+            interest += ship.moving_cost(x, y)
+            
+            # avoid cells too close from borders
+            if not 3 < x < (self.w - 3):
+                interest += 70
+            if not 3 <= y < (self.h - 3):
+                interest += 70
+            
+            # priorize cells in the current direction
+            diff = abs(Grid.diff_directions(ship.orientation, Grid.direction_to(*ship.prow, x, y)))
+            interest += 20 * abs(diff)
+            
+            # priorize spots at distance 6 from active ship
+            interest += (20 * abs(6 - self.manhattan((x, y), ship.pos)))
+            
+            # max distance from ennemies
+            interest -= (10 * min([Grid.manhattan((x, y), e.next_pos) for e in self.ennemy_ships]))
+            
+            runaway_spot.put((x, y), interest)
+        return runaway_spot.get()
+    
+    # geometrical algorithms
+    @staticmethod
+    def from_cubic(xu, yu, zu):
+        return (zu, int(xu + (zu - (zu & 1)) / 2))
+
+    @staticmethod
+    def to_cubic(x, y):
+        zu = x
+        xu = int(y - (x - (x & 1)) / 2)
+        yu = int(-xu - zu)
+        return (xu, yu, zu)
+
+    @staticmethod
+    def manhattan(from_, to_):
+        xa, ya = from_
+        xb, yb = to_
+        return abs(xa - xb) + abs(ya - yb) 
+    
+    @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]      
+
+    @staticmethod
+    def closest(from_, in_):
+        return min(in_, key=lambda x: Grid.manhattan(from_, x.pos))
+
+    @staticmethod
+    def directions(y):
+        if y % 2 == 0:
+            return [(1, 0), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1)]
+        else:
+            return [(1, 0), (1,-1), (0,-1), (-1, 0), (0, 1), (1, 1)]
+
+    @staticmethod
+    def direction_to(x0, y0, x, y):
+        dx, dy = (x - x0), (y - y0)
+        if dx > 0:
+            if dy == 0:
+                return 0
+            elif dy > 0:
+                return 5
+            else:
+                return 1
+        elif dx < 0:
+            if dy == 0:
+                return 3
+            elif dy > 0:
+                return 4
+            else:
+                return 2
+        else:
+            if dy > 0:
+                return 5 if y0 % 2 == 0 else 4
+            else:
+                return 1 if y0 % 2 == 0 else 2
+
+    @staticmethod
+    def add_directions(d1, d2):
+        d = d2 + d1
+        if d < 0:
+            d += 6
+        elif d > 5:
+            d -= 6
+        return d
+    
+    @staticmethod
+    def diff_directions(d1, d2):
+        d = d2 - d1
+        if d <= -3:
+            d += 6
+        elif d > 3:
+            d -= 6
+        return d
+    
+#     @staticmethod
+#     def next_cell(x, y, d, repeat=1):
+#         for _ in range(repeat):
+#             dx, dy = Grid.directions(y)[d]
+#             x, y = x + dx, y + dy
+#         return x, y
+    
+    @staticmethod
+    def symetry(d):
+        return d + 3 if d < 3 else d - 3
+    
+    @staticmethod
+    def abs_neighbors(x, y):
+        return ((x + dx, y + dy) for dx, dy in Grid.directions(y))
+    
+    @classmethod
+    def cache_neighbors(cls, xc, yc):
+        cls._neighbors[(xc, yc)] = [(x, y) for x, y in Grid.abs_neighbors(xc, yc) if 0 <= x < cls.w and 0 <= y < cls.h]
+
+    @classmethod
+    def neighbors(cls, x, y):
+        try:
+            return cls._neighbors[(x, y)]
+        except KeyError:
+            cls.cache_neighbors(x, y)
+            return cls._neighbors[(x, y)]
+    
+    @classmethod
+    def abs_next_cell(cls, x, y, d):
+        dx, dy = Grid.directions(y)[d]
+        return x + dx, y + dy
+            
+    @classmethod
+    def cache_next_cell(cls, x, y):
+        for d, dv in enumerate(Grid.directions(y)):
+            dx, dy = dv
+            cls._next_cell[(x, y, d)] = (x + dx, y + dy)
+
+    @classmethod
+    def next_cell(cls, x, y, d, repeat=1):
+        for _ in range(repeat):
+            try:
+                x, y = cls._next_cell[(x, y, d)]
+            except KeyError:
+                x, y = cls.abs_next_cell(x, y, d)
+        return x, y
+    
+    def rotate(self, center, coordinates, rotations):
+        if coordinates == [center] or rotations % 6 == 0:
+            return coordinates
+        x0, y0 = center
+        xu0, yu0, zu0 = self.to_cubic(x0, y0)
+        result = []
+
+        for x, y in coordinates:
+            xu, yu, zu = self.to_cubic(x, y)
+            dxu, dyu, dzu = xu - xu0, yu - yu0, zu - zu0
+            for _ in range(rotations):
+                dxu, dyu, dzu = -dzu, -dxu, -dyu
+            xru, yru, zru = dxu + xu0, dyu + yu0, dzu + zu0
+            xr, yr = self.from_cubic(xru, yru, zru)
+            result.append((xr, yr))
+        return result
+
+    # pathfinding
+    def path(self, start, start_d, target, moving_costs={}, inertia=None, incl_start=False, limit=10000):
+        nodes = Queue()
+        break_on, iteration = limit, 0
+        broken = False
+        
+        effective_start = start
+        origin = PathNode(*effective_start)
+        origin.orientation = start_d
+        
+        if inertia is not None:
+            for _ in range(inertia):
+                effective_start = self.next_cell(*effective_start, start_d)
+                origin = PathNode(*effective_start, origin)
+                origin.orientation = start_d
+        
+        nodes.put(origin, 0)
+        
+        neighbors = []
+
+        while nodes:
+            current = nodes.get()
+
+            if broken or current == target:
+                
+                path = []
+                previous = current
+                while previous:
+                    if previous != start or incl_start:
+                        path.insert(0, previous)
+                    previous = previous.parent
+                return path, iteration
+
+            neighbors = self.neighbors(*current)
+
+            for x, y in neighbors:
+                
+                if (x, y) == current.parent:
+                    continue
+
+                iteration += 1
+                if break_on > 0 and iteration >= break_on:
+                    broken = True
+                
+                moving_cost = moving_costs.get((x, y), 1000)
+                if moving_cost >= 1000:
+                    continue
+                
+                d = Grid.direction_to(*current, x, y)
+                if inertia == 0:
+                    # special: if started with speed 0, first move will require a speed_up, 
+                    # making impossible a direction change at first node
+                    if current.parent and current.parent == start and d != current.orientation:
+                        continue
+                
+                diff = abs(Grid.diff_directions(current.orientation, d))
+                if diff > 1:
+                    # change direction one degree at a time
+                    if current == (11,9) and (x, y) == (12,10): log("d")
+                    continue
+                    
+                area = Ship.get_area(x, y, d)[::2]
+                if diff:
+                    inertial_area = Ship.get_area(x, y, current.orientation)
+                    area += [inertial_area[0], inertial_area[2]]
+                if any((moving_costs.get(c, 1000) >= 1000 and not Grid.is_border(*c)) for c in area):
+                    continue
+                    
+                cost = current.cost + moving_cost + diff * 20
+                    
+                if (x, y) == start and inertia == 0 and d == start_d:
+                    # prefer to go right at start (if no speed)
+                    cost -= 10
+
+                priority = cost + 10 * Grid.manhattan((x, y), target)
+                
+                node = PathNode(x, y, current)
+                node.cost = cost
+                node.orientation = d
+                nodes.put(node, priority)
+                
+        return None, iteration
+
+class Entity(Base):
+    def __init__(self, ent_id):
+        self.id = int(ent_id)
+        self.x, self.y = 0, 0
+        self.args = [0,0,0,0]
+
+    def update(self, x, y, *args):
+        self.x, self.y = int(x), int(y)
+        
+    @property
+    def pos(self):
+        return (self.x, self.y)
+    
+    def __lt__(self, other):
+        # default comparison, used to avoid errors when used with queues and priorities are equals
+        return self.id < other.id
+    
+    
+class Position(Base):
+    def __init__(self, pos, d, speed, weight=10):
+        self.pos = pos
+        self.d = d
+        self.speed = speed
+        self.weight = weight
+        self.area = Ship.get_area(*self.pos, self.d)
+    
+class Ship(Entity):
+    MAX_SPEED = 2
+    SCOPE = 10
+    
+    SLOW_DOWN = 1
+    SPEED_UP = 2
+    TURN_LEFT = 3
+    TURN_RIGHT = 4
+    MOVES = [None, SPEED_UP, TURN_LEFT, TURN_RIGHT, SLOW_DOWN]
+    COMMANDS = {SLOW_DOWN: "SLOWER", SPEED_UP: "FASTER", TURN_LEFT: "PORT", TURN_RIGHT: "STARBOARD", None: "NONE"}
+    
+    areas = {}
+    
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.x, self.y = 0, 0
+        self.orientation = 0
+        self.speed = 0
+        self.stock = 0
+        self.owned = 0
+        
+        self.next_cell = None
+        self.next_pos = None
+        self.last_fire = None
+        self.last_mining = None
+        self.blocked_since = 0
+        self.same_traject_since = 0
+        self.last_action = ""
+        self.allies = []
+        self._moving_costs = {}
+        
+        self.objectives = ObjectivesQueue()
+        self.ennemies = ObjectivesQueue()
+        
+        self.objective = None
+        self.objectives_next = []
+        self.target_ennemy = None
+        
+        self.path = []
+        
+        self.distance = 0
+        self.alignment = 0
+        
+    def __repr__(self):
+        return f"<Ship {self.id}: pos=({self.x}, {self.y}), orientation={self.orientation}, speed={self.speed}, blocked={self.blocked_since}, last_fire={self.last_fire}, next_pos={self.next_pos}, area={self.area}>"
+
+    @classmethod
+    def abs_area(cls, x, y, d):
+        return [Grid.next_cell(x, y, d), (x, y), Grid.next_cell(x, y, Grid.add_directions(d, 3))]
+
+    @classmethod
+    def cache_area(cls, x, y):
+        for d in range(3):
+            area = [Grid.next_cell(x, y, d), (x, y), Grid.next_cell(x, y, d + 3)]
+            Ship.areas[(x, y, d)] = area
+            Ship.areas[(x, y, d + 3)] = list(reversed(area))
+            
+    @classmethod
+    def get_area(cls, x, y, d):
+        try:
+            return list(Ship.areas[(x, y, d)])
+        except KeyError:
+            return cls.abs_area(x, y, d)
+        
+    def update(self, x, y, *args):
+        previous_state = self.state()
+        previous_traject = self.traject()
+        
+        super().update(x, y)
+        self.orientation, self.speed, self.stock, self.owned = map(int, args)
+
+        self.objectives = ObjectivesQueue()
+        self.ennemies = ObjectivesQueue()
+        
+        self.objective = None
+        self.objectives_next = []
+        self.target_ennemy = None
+        
+        self.goto = None
+        self.path = []
+        
+        self.area = Ship.get_area(self.x, self.y, self.orientation)
+        self.prow, _, self.stern = self.area
+        
+        self.next_cell = self.get_next_cell()
+        self.next_pos = self.get_next_pos()
+        self.next_move = None
+        self.next_area = Ship.get_area(*self.next_pos, self.orientation)
+        
+        self.front = Grid.next_cell(*self.prow, self.orientation)
+        self.front_left = Grid.next_cell(*self.prow, Grid.add_directions(self.orientation, 1))
+        self.left = Grid.next_cell(*self.prow, Grid.add_directions(self.orientation, 2))
+        self.front_right = Grid.next_cell(*self.prow, Grid.add_directions(self.orientation, -1))
+        self.right = Grid.next_cell(*self.prow, Grid.add_directions(self.orientation, -2))
+        self.back_left = Grid.next_cell(*self.stern, Grid.add_directions(self.orientation, 1))
+        self.back_right = Grid.next_cell(*self.stern, Grid.add_directions(self.orientation, -1))
+        self.behind = Grid.next_cell(*self.stern, Grid.add_directions(self.orientation, 3))
+        
+        self._can_move = {}
+        
+        if self.traject() != previous_traject:
+            self.same_traject_since += 1
+        else:
+            self.same_traject_since = 0
+    
+        if self.state() == previous_state:
+            self.blocked_since += 1
+        else:
+            self.blocked_since = 0
+
+    def traject(self):
+        return (self.orientation, self.speed)
+
+    def state(self):
+        return (self.x, self.y, self.orientation, self.speed)
+
+    @classmethod
+    def get_pos_in(cls, current, speed, orientation, in_=1):
+        return Grid.next_cell(*current, orientation, repeat=speed * in_)
+
+    def get_next_pos(self, in_=1):
+        return self.get_pos_in(self.pos, self.speed, self.orientation, in_)
+    
+    def next_pos_proba(self, in_=1):
+
+        # guess next positions
+        positions = {0: [Position(self.pos, self.orientation, self.speed)]}
+        for i in range(in_):
+            positions[i + 1] = []
+            
+            for p in positions[i]:
+                
+                pos, d, speed = p.pos, p.d, p.speed
+                
+                # next pos with inertia
+                inertia = Grid.next_cell(*pos, d, repeat=speed)
+                
+                # wait, fire or mine
+                p = Position(inertia, d, speed, 30)
+                if self.moving_cost(*p.pos) >= 1000:
+                    p.weight = 10
+                    
+                # turn left
+                p = Position(inertia, Grid.add_directions(d, 1), speed)
+                if not self.moving_cost(*p.pos) >= 1000:
+                    positions[i + 1].append(p)
+                    
+                # turn right
+                p = Position(inertia, Grid.add_directions(d, -1), speed)
+                if not self.moving_cost(*p.pos) >= 1000:
+                    positions[i + 1].append(p)
+                    
+                # speed up
+                if speed < self.MAX_SPEED:
+                    p = Position(Grid.next_cell(*pos, d, repeat=speed + 1), d, speed + 1)
+                    if not self.moving_cost(*p.pos) >= 1000:
+                        positions[i + 1].append(p)
+                
+                # slow down
+                if speed > 1:
+                    p = Position(Grid.next_cell(*pos, d, repeat=speed - 1), d, speed - 1)
+                    if not self.moving_cost(*p.pos) >= 1000:
+                        positions[i + 1].append(p)
+        
+                # we voluntary ignore the case where a ship at speed 1 would slow down, 
+                # as it is not expected to be a standard behaviour for a ship
+        
+        # agregate
+        proba = {}
+        for i, plst in positions.items():
+            proba[i] = {}
+            weights = {}
+            total_weight = sum([p.weight for p in plst])
+            for p in plst:
+                for c in p.area:
+                    weights[c] = weights.get(c, 0) + p.weight
+            
+            for c in weights:
+                proba[i][c] = 100 * weights[c] // total_weight
+
+        return proba
+    
+    def guess_next_positions(self, in_=1):
+        proba = self.next_pos_proba(in_)
+        best = {}
+        for i in proba:
+            best[i] = max(proba[i].items(), key=lambda x: x[1])[0]
+        return best
+            
+    def get_next_cell(self, in_=1):
+        return Grid.next_cell(self.x, self.y, self.orientation, repeat=in_)
+
+    def in_current_direction(self, x, y):
+        return self.orientation == Grid.direction_to(*self.pos, x, y)
+
+    def moving_cost(self, x, y):
+        return self._moving_costs.get((x, y), 1000)
+    
+    def can_turn_left(self):
+        return (self._can_move[self.left] or grid.is_border(*self.left)) \
+            and (self._can_move[self.right] or grid.is_border(*self.right)) \
+            and (self._can_move[self.back_right] or grid.is_border(*self.back_right))
+    
+    def can_turn_right(self):
+        return (self._can_move[self.right] or grid.is_border(*self.right)) \
+               and (self._can_move[self.left] or grid.is_border(*self.left)) \
+               and (self._can_move[self.back_left] or grid.is_border(*self.back_left))
+    
+    def can_move_fwd(self):
+        return self._can_move[self.front] and not Grid.is_border(*self.prow)
+    
+    def can_move(self):
+        return self.can_move_fwd() or self.can_turn_left() or self.can_turn_left()
+    
+    def area_after_moving(self, move):
+        new_speed = self.speed
+        new_orientation = self.orientation
+        if move == Ship.SPEED_UP:
+            new_speed += 1
+        elif move == Ship.SLOW_DOWN:
+            new_speed -= 1
+        elif move == Ship.TURN_LEFT:
+            new_orientation = Grid.add_directions(self.orientation, 1)
+        elif move == Ship.TURN_RIGHT:
+            new_orientation = Grid.add_directions(self.orientation, -1)
+        
+        new_pos = self.get_next_cell(new_speed)
+        return self.get_area(*new_pos, new_orientation)
+    
+    def plan_next_move(self):
+        
+        blocked = False
+        if self.speed:
+            if any(self.front in (s.area if not s.speed else [s.area[1], s.area[0], s.prow]) for s in grid.ships if s is not self):
+                log("Blocked: speed 0")
+                blocked = True
+                self.speed = 0
+            
+        if self.path:
+            planned = self._follow_path(self.path)
+            
+        if self.path is None:
+            if self.can_move():
+                available = {}
+                if self.can_move_fwd():
+                    if self.speed:
+                        available[None] = 0
+                    else:
+                        available[Ship.SPEED_UP] = 0
+                if self.can_turn_left():
+                    available[Ship.TURN_LEFT] = 0
+                if self.can_turn_right():
+                    available[Ship.TURN_RIGHT] = 0
+                    
+                for m in available:
+                    new_area = self.area_after_moving(m)
+                    available[m] = abs(Grid.diff_directions(self.orientation, Grid.direction_to(*new_area[1], *self.goto))) + \
+                                   (2 if self.moving_cost(*new_area[1]) > 10 else 0)
+                    
+                planned = min(available.items(), key=lambda x: x[1])[0]
+                log(f"(!) broken: automove ({Ship.COMMANDS[planned]})")
+            else:
+                log(f"(!) broken: can not move")
+                planned = None
+        elif not self.path:
+            return False
+        
+        next_move = None
+        available_moves = [planned] + [m for m in Ship.MOVES if m != planned]
+        risk = Queue()
+        
+        for move in available_moves:
+                
+            new_area = self.area_after_moving(move)
+            
+            # takes inertia in account
+            if move == Ship.SPEED_UP or self.speed and move == None:
+                for c in self.get_area(*Grid.next_cell(*new_area[1], self.orientation, self.speed + int(move == Ship.SPEED_UP)), self.orientation):
+                    if not c in new_area:
+                        new_area.append(c)
+            
+            r = 0
+            
+            for i, c in enumerate(new_area):
+                mc = self.moving_cost(*c)
+                
+                if mc >= 1000 and (c in grid or i == 1):  # special: extra-grid cells are not consider as collisions since a part of the ship can go there
+                    countdown = grid.threat.get(c, 0)
+                    if countdown > 1 and mc < 2000:
+                        # cannonball will hit there after next turn
+                        r += 100
+                        
+                    else:
+                        if blocked and move in (Ship.SLOW_DOWN, None, Ship.SPEED_UP):
+                            r = 1000
+                        if i == 1:
+                            # the center of the ship is threaten
+                            r += 500
+                        else:
+                            r += 250
+            
+            if not r:
+                if move == planned:
+                    r = -1
+                elif self.speed <= 1 and move == Ship.SLOW_DOWN:
+                    r = 50 # we don't want to slow down except there is a danger
+                else:
+                    # distance from the prow to the current objective
+                    r = abs(Grid.diff_directions(self.orientation, Grid.direction_to(*new_area[1], *self.goto)))
+                    r += (2 if self.moving_cost(*new_area[1]) > 10 else 0)
+
+            risk.fput(move, r)
+            if r >= 100:
+                log(f"/!\ Danger: planned move <{Ship.COMMANDS[move]}> could lead to collision (risk={r}, area={new_area})")
+            elif r < 0:
+                next_move = move
+                log(f"Safe move: {Ship.COMMANDS[move]}")
+                break
+            else:
+                log(f"Available move <{Ship.COMMANDS[move]}> (risk={r}, area={new_area})")
+
+        else:
+            try:
+                next_move = risk.get()
+                log(f"* Go to the less risky: {Ship.COMMANDS[next_move]}")
+            except IndexError:
+                next_move = planned
+                log(f"* No collision-free move was found, go to the initial one: {Ship.COMMANDS[next_move]}")
+                
+        self.next_move = next_move
+        self.next_area = new_area
+        return True
+
+    def _follow_path(self, path):
+        
+        # flags represent direction changes or end of the path
+        last_flag = len(path) - 1
+        next_flag = next((i for i, n in enumerate(path) if n.orientation != self.orientation), last_flag)
+        afternext_flag = next((i for i, n in enumerate(path[next_flag:]) if n.orientation != path[next_flag].orientation), last_flag)
+        
+        if not self.speed:
+            diff = Grid.diff_directions(self.orientation, path[0].orientation)
+            
+            if diff > 0 and self.last_action == "STARBOARD" or diff < 0 and self.last_action == "PORT":
+                # special: avoid the starting hesitation
+                return Ship.SPEED_UP
+            
+            if diff and next_flag == 0:
+                # start, with a direction change
+                if diff > 0:
+                    return Ship.TURN_LEFT
+                elif diff < 0:
+                    return Ship.TURN_RIGHT
+                    
+            # start straight
+            else:
+                return Ship.SPEED_UP
+        
+        elif self.speed == self.MAX_SPEED:
+            
+            if next_flag <= self.speed:
+                if afternext_flag >= (next_flag + 2): # there is at least one straight cell after this drift
+                    # drift
+                    diff = Grid.diff_directions(self.orientation, path[next_flag].orientation)
+                    if diff > 0:
+                        return Ship.TURN_LEFT
+                    elif diff < 0:
+                        return Ship.TURN_RIGHT
+                else:
+                    return Ship.SLOW_DOWN
+                
+            if next_flag <= self.speed + 1:
+                # next direction change or target will be passed at current speed
+                return Ship.SLOW_DOWN
+
+        elif self.speed == 1:
+            
+            if next_flag <= 1:
+                diff = Grid.diff_directions(self.orientation, path[next_flag].orientation)
+                if diff > 0:
+                    return Ship.TURN_LEFT
+                elif diff < 0:
+                    return Ship.TURN_RIGHT
+            
+            elif next_flag >= 4:
+                return Ship.SPEED_UP
+            
+        return None
+           
+    def move(self):
+        if self.next_move == Ship.SPEED_UP:
+            self.speed_up()
+        elif self.next_move == Ship.SLOW_DOWN:
+            self.slow_down()
+        elif self.next_move == Ship.TURN_LEFT:
+            self.turn_left()
+        elif self.next_move == Ship.TURN_RIGHT:
+            self.turn_right()
+        else:
+            return False
+        return True
+           
+    def fire_at_will(self, *args, **kwargs):
+        return self._fire_at_will(*args, **kwargs)
+           
+    def _fire_at_will(self):
+        if not self.can_fire():
+            return False
+        
+        avoid = []
+        for ship in grid.owned_ships:
+            avoid += ship.next_area
+        
+        barrels = [b.pos for b in grid.barrels]
+        for bpos in barrels:
+            if any(Grid.manhattan(ship.pos, bpos) <= Grid.manhattan(e.pos, bpos) for ship in grid.owned_ships for e in grid.ennemy_ships):
+                avoid.append(bpos)
+        
+        for m in grid.mines:
+            if any((Grid.manhattan(s.next_pos, m.pos) <= 2 or \
+                    abs(Grid.diff_directions(self.orientation, Grid.direction_to(*s.next_pos, *m.pos))) <= 1)\
+                    for s in grid.owned_ships):
+                avoid.append(m.pos)
+        
+        all_shots = {}
+        
+        for target in grid.ennemy_ships:
+        
+            next_positions = target.next_pos_proba(4)
+            
+            # include avoid, mines, barrels, and other ennemies
+            for t in next_positions:
+                probas = next_positions[t]
+                mines_next = {}
+                
+                for c, proba in probas.items():
+                    if c in grid.next_to_mine:
+                        mpos = grid.next_to_mine[c].pos
+                        mines_next[mpos] = mines_next.get(mpos, 1) + proba
+                probas.update(mines_next)
+                
+                for c in probas:
+                    if c in barrels:
+                        probas[c] *= 2
+            
+            for t, probas in next_positions.items():
+                
+                shots = sorted(probas.items(), key=lambda x: x[1], reverse=True)
+                for c, proba in shots:
+                    if c in avoid:
+                        continue
+                    if proba < 20:
+                        continue
+                    dist = Grid.manhattan(self.prow, c)
+                    if dist > self.SCOPE:
+                        continue
+                    
+                    # time for the cannonball to reach this pos (including fire turn)
+                    delay = 1 + round(dist / 3)
+                    if delay != t:
+                        continue
+                    
+                    if not c in all_shots:
+                        all_shots[c] = (proba - t)
+                    else:
+                        all_shots[c] += (proba - t)
+                    
+        if all_shots:
+            best_shot = max(all_shots.items(), key=lambda x: x[1])[0]
+            log(f"[x] precise shoot: pos={best_shot}")
+            ship.fire(*best_shot)
+            return True
+                
+        return False
+        
+    def can_mine(self):
+        return self.last_mining is None or (current_turn - self.last_mining) >= 4
+        
+    def can_fire(self):
+        return self.last_fire is None or (current_turn - self.last_fire) >= 1
+            
+    def mine_maybe(self):
+        if self.can_mine():
+            if not any(Grid.manhattan(self.prow, ally.next_pos) <= 5 for ally in self.allies):
+                self.mine()
+                return True
+        return False
+            
+    # --- Basic commands
+    def _act(self, cmd, *args):
+        self.last_action = cmd
+        output = " ".join([cmd] + [str(a) for a in args])
+        log(f"ship {self.id}: {output}")
+        print(output)
+    
+    def auto_move(self, x, y):
+        self._act("MOVE", x, y)
+        
+    def speed_up(self):
+        self._act("FASTER")
+        
+    def slow_down(self):
+        self._act("SLOWER")    
+    
+    def turn_right(self):
+        self._act("STARBOARD")
+    
+    def turn_left(self):
+        self._act("PORT")
+        
+    def wait(self):
+        self._act("WAIT")
+        
+    def mine(self):
+        self.last_mining = current_turn
+        self._act("MINE")
+        
+    def fire(self, x, y):
+        self.last_fire = current_turn
+        self._act("FIRE", x, y)
+
+class Barrel(Entity):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.amount = 0
+
+        self.dispersal = 0
+        self.mine_threat = False
+        self.ennemy_near = False
+
+    def __repr__(self):
+        return f"<Barrel {self.id}: pos=({self.x}, {self.y}), amount={self.amount}>"
+        
+    def update(self, x, y, *args):
+        super().update(x, y)
+        self.amount = int(args[0])
+
+class Mine(Entity):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.ghost = False
+
+    def __repr__(self):
+        return f"<Mine {self.id}: pos=({self.x}, {self.y}), ghost={self.ghost}>"
+
+class Cannonball(Entity):
+
+    def update(self, x, y, *args):
+        super().update(x, y)
+        self.sender, self.countdown = int(args[0]), int(args[1])
+
+
+entities = {}
+map_entity = {"SHIP": Ship, 
+             "BARREL": Barrel,
+             "MINE": Mine,
+             "CANNONBALL": Cannonball}
+
+Grid.preload()
+grid = Grid()
+
+### *** Main Loop ***
+
+while True:
+    seen = []
+    current_turn += 1
+    
+    # <--- get input
+    my_ship_count, entity_count = int(input()), int(input())
+    ent_input = [input().split() for _ in range(entity_count)]
+    # --->
+
+    log(f"### TURN {current_turn}")
+    log(">> Load input")
+    # <--- load input
+    previous_ent, entities = grid.entities, {}
+    for e in ent_input:
+        ent_id, ent_type, *data = e
+        ent_id = int(ent_id)
+        entities[ent_id] = grid.entities.get(ent_id, map_entity[ent_type](ent_id))
+        entities[ent_id].update(*data)
+        
+    grid.load_entities(entities)
+    # --->
+    
+#     log(f"Owned Ships: {grid.owned_ships}")
+    log(f"Ennemy Ships: {grid.ennemy_ships}")
+    log(f"Barrels: {grid.barrels}")
+#     log(f"Mines: {grid.mines}")
+    log(f"Cannonballs: {grid.cannonballs}")
+    
+    max_it = 9000 // len(grid.owned_ships)
+
+    ### Acquire
+    log("# Acquiring")
+    
+    # main objective
+    while not all(s.objective for s in grid.owned_ships):
+        try:
+            acquired = sorted([(s, s.objectives.get()) for s in grid.owned_ships if not s.objective], key= lambda x: x[1].interest)
+            for s, o in acquired:
+                if not s.objective and not any(al.objective.target is o.target for al in s.allies if al.objective):
+                    s.objective = o
+            
+        except IndexError:
+            break
+        
+    # targetted ennemy
+    for s in grid.owned_ships:
+        s.target_ennemy = s.ennemies.get()
+        
+    ### Plan
+    log("# Planning")
+    
+    for ship in grid.owned_ships:
+        log(f"---- ship {ship.id} ---")
+        log(f"ship: {ship}")
+        
+        it_consumed = 0
+        
+        if ship.objective:
+            ship.goto = ship.objective.target.pos
+            
+        elif ship.target_ennemy:
+            if all(s.stock < ship.stock for s in grid.ships if not s is ship):
+                log("Best stock: runaway!")
+                ship.goto = grid.runaway_spot(ship)
+            else:
+                ship.goto = grid.shooting_spot(ship, ship.target_ennemy.target, current=ship.goto)
+        else:
+            log("ERROR: No target")
+            continue
+        
+        log(f"goto: {ship.goto}")
+        
+        ship.path, its = grid.path(ship.pos, 
+                              ship.orientation, 
+                              ship.goto, 
+                              moving_costs=ship._moving_costs, 
+                              inertia=ship.speed, 
+                              limit=(max_it - it_consumed))
+        it_consumed += its
+        
+        if ship.objective and ship.path and ship.path[-1] == ship.goto:
+            while ship.objectives and len(ship.path) < 15:
+                pos, d = ship.path[-1], ship.path[-1].orientation
+                
+                ship.objectives = ObjectivesQueue.re_eval(ship.objectives, pos, d)
+                current_obj = ship.objectives.get()
+                
+                ship.objectives_next.append(current_obj)
+            
+                new_path, its = grid.path(pos, d, 
+                                       current_obj.target.pos, 
+                                       moving_costs=ship._moving_costs,
+                                       limit=(max_it - it_consumed))
+                
+                it_consumed += its
+                
+                if new_path and new_path[-1] == current_obj.target.pos:
+                    ship.path += new_path
+                else:
+                    break
+ 
+        ship.plan_next_move()
+
+        log(f"obj: {ship.objective}; next: {ship.objectives_next}")
+        log(f"target: {ship.target_ennemy}")
+        log(f"path: {ship.path}")
+        log(f"next_move: {Ship.COMMANDS[ship.next_move]}")
+        
+    ### Process
+    log("# Processing")
+    
+    for ship in grid.owned_ships:
+        log(f"---- ship {ship.id} ---")
+        if not ship.objective and not ship.target_ennemy:
+            log("No target: wait")
+            ship.wait()
+        
+        if ship.move():
+            continue
+        
+        # no movement was required, can fire
+        if ship.fire_at_will():
+            continue
+        
+        # or mine
+        if ship.mine_maybe():
+            continue
+        
+        log("ERROR: Did not act, wait")
+        ship.wait()
+
+
+
+
+# Changelog:
+# 2019-04-17 
+# * if no available move, takes the less risky
+# * add the ennemy_near evaluation to the pre_eval method of GetBarrel and update it
+# * ignore probas < 20 in the moving cost calc (about ennemy next positions)
+# * get_shooting_spots: discurage direction changes
+# * increase shooting_spots distance from 5 to 7
+# * fire on mines, do not shoot mines that are in front of the ship
+# * increase the weight of a dead angle in GetBarrel.eval() from 18 to 36 <=> the weight of a distance of 6 needed to turn
+# * mines: increase moving_cost of neighbors if a cannon ball is going to hit the mine
+# * Improve the _follow_path algo
+# * anticipate the speed at 0 when blocked
+# * coeff 20 instead of 10 for the presence probability, making it impassable from 50% instead of 100%
+# *(disactivated)  moving cost takes now in account the order of play of the ships
+# * increase the max length of the path from 10 to 15 when seeking for next objectives
+# * minor improvement to automove
+# * Avoid shooting barrels unless ennemy is nearest
+# * include distance to ennemy in the eval of the shooting spot
+# * (disactivated because of below) avoid consecutives direction changes 
+# * path: direction change moving cost changed from 10 to 20 because of the slowing effect
+
+# 2019-04-18
+# * take in account the inertial_area in path computing when direction change
+# * improve the esquive algo by priorizing moves in direction of the target
+# * remove cache on 'next_pos_proba'
+# * improve the fire_at_will algo
+# * takes all ennemies in account to find the best shot
+
+# 2019-04-19
+# * path(): fix the special speed-up-inertia when start whith speed 0
+# * fix the grid.threat update
+# * add a moving cost at the prow of enemy ships to avoid a possible mine

+ 19 - 0
caribbean/testing_lines.py

@@ -0,0 +1,19 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+
+
+        ship = Ship(0)
+        ship.update(3,3,0,1,0,1)
+        ennemy = Ship(1)
+        ennemy.update(1,1,0,1,0,0)
+        barrel1 = Barrel(10)
+        barrel1.update(8,2,40,0,0,0)
+        barrel2 = Barrel(11)
+        barrel2.update(4,2,50,0,0,0)
+        mine = Mine(20)
+        mine.update(10, 2)
+        entities = {0: ship, 1: ennemy, 20: mine}
+#         entities = {0: ship, 1: ennemy, 10: barrel1, 11: barrel2, 20: mine}
+        seen = [0, 1]

+ 2 - 0
code_golf/chuck.py

@@ -0,0 +1,2 @@
+import re
+print(*['0'*(1+bool(a))+' '+'0'*len(a+b)for a,b in re.findall("(0+)|(1+)","".join(bin(ord(c))[2:].zfill(7)for c in input()))])

+ 4 - 0
code_golf/dont_panic.py

@@ -0,0 +1,4 @@
+i,s=int,lambda _=0:input().split()
+o=s()
+l=dict(map(s,[0]*i(o[7])))
+while 1:f,p,d=s();print(["WAIT","BLOCK"][(i(p)-i([o[4],l.get(f,0)][f<o[3]]))*("RNL".index(d[0])-1)<0])

+ 1 - 0
code_golf/temperatures.py

@@ -0,0 +1 @@
+input();print(min((abs(int(x)-.1),x)for x in input().split()&[0])[1])

+ 5 - 0
code_golf/thor.py

@@ -0,0 +1,5 @@
+X,Y,x,y=input().split()
+while 1:input();print("NS"[Y>y]*(y!=Y)+"WE"[X>x]*(x!=X));x+=(X>x)-(X<x);y+=(Y>y)-(Y<y)
+
+
+"NSWE"[y<Y:(y<Y)+1:2+(x>X)]

+ 273 - 0
code_mode/base_ai.py

@@ -0,0 +1,273 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import itertools
+import sys
+
+
+def log(x):
+    print(x, file=sys.stderr)
+
+
+# TODO: can I take ingredients not in order? If I can, find the shortest path trought all elements >> yes, but only one ingredient without a dish.
+# TODO: if partner on the way, wait if another path is 4+ cells longer
+# TODO: Should I tell wich I am doing to my partner?
+# TODO: When should I drop th plate on a table?
+# TODO: Check if a plate matching an order is already ready
+# TODO: don't take an order that is already taken by partner (how?)
+
+# Cells
+BLUEBERRIES_CRATE = "B"
+ICE_CREAM_CRATE = "I"
+WINDOW = "W"
+EMPTY_TABLE = "#"
+DISHWASHER = "D"
+FLOOR_CELL = "."
+
+# Items
+NONE = "NONE"
+DISH = "DISH"
+ICE_CREAM = "ICE_CREAM"
+BLUEBERRIES = "BLUEBERRIES"
+
+crate = {DISH: DISHWASHER,
+         ICE_CREAM: ICE_CREAM_CRATE,
+         BLUEBERRIES: BLUEBERRIES_CRATE,
+         WINDOW: WINDOW}
+
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.item = item
+        self.award = int(award)
+
+class Cook(Base):
+    def __init__(self, x, y, item):
+        
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+        self.order = ""
+        self.plan = []
+#         self.todo = []
+#         self.current_target = ""
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    def take_order(self, grid, order):
+        self.grid = grid
+        self.order = order
+#         self.todo = order.split('-')
+        self.update_plan()
+#         self.current_target = where[self.plan[0]]
+
+    def update_plan(self):
+        todo = self.order.split('-')
+        steps = [crate[t] for t in todo]
+        
+        # test different paths. DISH should be either first or second in the list, and window should be last.
+        cases = [c for c in itertools.permutations(steps)]
+        
+        # special: dish can not be after second position:
+        cases = [c for i, c in enumerate(cases) if not (i > 1 and c == DISH)]
+        
+        # window is always at the end:
+        cases += [WINDOW]
+        
+        # multply cases by all combinations of coordinates
+        exploded = [[self.grid.where_are(e) for e in case] for case in cases]
+        routes = sum([list(itertools.product(*r)) for r in exploded], [])
+        
+        # take the shortest path
+        shortest = min([r for r in routes], key=lambda r: len(self.grid.path_trough(self.pos, *r)))
+
+        self.plan = shortest
+        
+    def act(self):
+        
+        # take the first action in the plan
+        target = self.plan[0]
+        
+        #         path = self.grid.path_to(self.pos, target)
+        
+        # if can act this turn, consider the action as completed
+        if target in self.grid.neighbors(*self.pos):
+            target.pop(0)
+        
+        self.use(target)
+        
+        if not self.plan:
+            self.done()
+        
+    def done(self):
+        self.order = ""
+        self.plan = []
+#         self._objective = ""
+#         self.todo = []
+#         self.current_target = ""
+        
+#     @property
+#     def current_need(self):
+#         for t in self.todo:
+#             if not t in self.item:
+#                 return t
+#         return NONE
+    
+    def use(self, x, y, msg=""):
+        print("USE", x, y, msg)
+
+    def move(self, x, y):
+        print("MOVE", x, y)
+        
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+ 
+class Oven(Base):
+    def __init__(self, contents, timer):
+        self.contents = contents
+        self.timer = int(timer)
+
+class NoPathFound(Exception):
+    pass
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+class Grid(Base):
+    def __init__(self, cells):
+        self.cells = cells
+
+    def at(self, x, y):
+        return self.cells[y][x]
+    
+    def is_in(self, x, y):
+        try:
+            self.at(x, y)
+            return True
+        except IndexError:
+            return False
+
+    def flatten(self):
+        return [(x, y, c) for y, row in enumerate(self.cells) for x, c in enumerate(row)]
+        
+    def where_are(self, content):
+        return [(x, y) for x, y, c in self.flatten() if c == content]
+    
+    def is_passable(self, x, y):
+        return self.at(x, y) in (FLOOR_CELL, "0", "1")
+    
+    def closest(self, from_, content):
+        return min(self.where_are(content), key=lambda k: (abs(from_[0] - k[0]) + abs(from_[1] - k[1])))
+
+    def neighbors(self, x, y):
+        return [(xn, yn) for xn, yn in [(x - 1, y - 1), (x, y - 1), (x + 1, y - 1), \
+                                    (x - 1, y), (x + 1, y)  , \
+                                    (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)]
+                                if self.is_in(xn, yn)]
+
+    def path_to(self, origin, target):
+        nodes = []
+        origin = PathNode(*origin)
+        
+        heapq.heappush(nodes, (0, origin))
+
+        while nodes:
+            current = heapq.heappop(nodes)[1]
+            if current == target:
+                break
+
+            for x, y in self.neighbors(*current):
+
+                node = PathNode(x, y, current)
+                if not self.is_passable(*node):
+                    continue
+
+                node.cost = current.cost + 1
+                priority = node.cost + (abs(node[0] - target[0]) + abs(node[1] - target[1]))
+
+                heapq.heappush(nodes, (priority, node))
+        else:
+            raise NoPathFound("no path were found to the targetted location {}".format(target))
+
+        result = [target]
+        while current != origin:
+            result.append(tuple(current))
+            current = current.parent
+        result.append(origin)
+        result.reverse()
+
+        return result
+
+    def path_trough(self, origin, *steps):
+        return sum([self.path_to(origin, s) for s in steps], [])
+
+
+# input vars
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([list(input()) for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+while True:
+    turns_remaining = int(input())
+    player = Cook(*input().split())
+    log(f"*** player: {player}")
+    
+    partner = Cook(*input().split())
+    log(f"*** partner: {partner}")
+    
+    num_tables_with_items = int(input())  # the number of tables in the kitchen that currently hold an item
+    tables = [Table(*input().split()) for _ in range(num_tables_with_items)]
+    log(f"*** tables: {tables}")
+    
+    oven = Oven(*input().split())
+    log(f"*** oven: {oven}")
+    
+    num_customers = int(input())  # the number of customers currently waiting for food
+    customers = [Customer(*input().split()) for _ in range(num_customers)]
+    log(f"*** customers: {customers}")
+    
+    # if no current order, take the most beneficial
+    if not player.order:
+        queue = sorted(customers, reverse=True, key=lambda x: x.award)
+        # what is my partner doing?
+        player.take_order(grid, queue[0].item)
+        log(f'>>> new order taken: {player.order}')
+        
+    player.act()
+    #print("WAIT")
+        
+#     log(f'todo: {player.todo}')
+#     log(f'currently seeking: {player.current_need}')
+    
+#     if player.current_need == NONE:
+#         target = WINDOW
+#         log(f'current target: {target}')
+#         player.done()
+        
+#     else:
+#     target = where[player.current_need]
+#     log(f'current target: {target}')
+#             
+#     closest = grid.closest(player.pos, target)
+#     log(f"closest: {closest}")
+
+#     player.use(*closest)
+    

+ 34 - 0
code_mode/notes.md

@@ -0,0 +1,34 @@
+TODO: if partner on the way, wait if another path is 4+ cells longer
+x TODO: Should I tell wich I am doing to my partner?
+TODO: When should I drop th plate on a table?
+TODO: Check if a plate matching an order is already ready
+x TODO: don't take an order that is already taken by partner (how?)
+
+
+order = ["framboises", 
+         "fraises coupées",
+         "croissant"]
+         
+actions = [get,
+           get, cut
+           get, put in oven, wait, get] 
+
+
+
+Si pas de commande en cours: 
+   > prends une nouvelle commande
+   
+Si la commande est complète:
+   > va à la fenêtre
+   
+Si mains libres et un ingrédient a besoin d'être préparé:
+   > on va en priorité vers l'ingrédient
+   
+Si on a un ingrédient en main:
+    > On va le préparer
+    
+Si on a un élément préparé en main et pas d'assiette:
+    > on va chercher une assiette
+
+Sinon:
+    > On va chercher l'item le plus proche

+ 76 - 0
code_mode/pathfinding.py

@@ -0,0 +1,76 @@
+import heapq
+
+ grid.obstacles = [partner.pos]
+    
+    def is_passable(self, x, y):
+        return self.at(x, y) in (FLOOR_CELL, "0", "1") and not (x, y) in self.obstacles
+    
+    
+class NoPathFound(Exception):
+    pass
+
+class Node(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+class NodesHeap():
+    def __init__(self):
+        self._lst = []
+
+    def push(self, node, priority):
+        heapq.heappush(self._lst, (priority, node))
+
+    def pop(self):
+        return heapq.heappop(self._lst)[1]
+
+
+class Pathfinder():
+
+    @staticmethod
+    def a_star(grid, origin, target):
+        nodes = NodesHeap()
+        origin = Node(*origin)
+        nodes.push(origin, 0)
+
+        while nodes:
+            current = nodes.pop()
+            if current == target:
+                break
+
+            for x, y in grid.neighbors(*current):
+
+                node = Node(x, y, current)
+
+                # get the moving cost to this node
+                movingcost = grid.movingcost(*current, *node)
+                if movingcost < 0:
+                    continue
+
+                # cost of the node is the accumulated cost from origin
+                node.cost = current.cost + movingcost
+
+                # priority of the node is the sum of its cost and distance to target
+                # (the lower the better)
+                priority = node.cost + grid.geometry.manhattan(*node, *target)
+
+                # (?) would it be necessary to check if there is already a
+                # node with same coordinates and a lower priority?
+
+                # append to the checked nodes list
+                nodes.push(node, priority)
+        else:
+            # all the reachable nodes hve been checked, no way found to the target
+            raise NoPathFound("no path were found to the targetted location {}".format(target))
+
+        # build the result by going back up from target to origin
+        result = [target]
+        while current != origin:
+            result.append(tuple(current))
+            current = current.parent
+        result.append(origin)
+        result.reverse()
+
+        return result

+ 309 - 0
code_mode/script.py

@@ -0,0 +1,309 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+
+DEBUG = True
+def log(x):
+    if DEBUG:
+        print(x, file=sys.stderr)
+
+# Cells
+START_0 = '0'
+START_1 = '1'
+BLUEBERRIES_CRATE = "B"
+ICE_CREAM_CRATE = "I"
+STRAWBERRIES_CRATE = "S"
+CHOPPING_BOARD = "C"
+DOUGH_CRATE = "H"
+WINDOW = "W"
+EMPTY_TABLE = "#"
+DISHWASHER = "D"
+FLOOR_CELL = "."
+OVEN = "O"
+
+# Items
+NONE = "NONE"
+DISH = "DISH"
+ICE_CREAM = "ICE_CREAM"
+BLUEBERRIES = "BLUEBERRIES"
+STRAWBERRIES = "STRAWBERRIES"
+CHOPPED_STRAWBERRIES = "CHOPPED_STRAWBERRIES"
+DOUGH = "DOUGH"
+CROISSANT = "CROISSANT"
+
+
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.item = item
+        self.award = int(award)
+
+class Cook(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+        self.order = []
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    def update(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+        
+    def use(self, x, y, msg=""):
+        print("USE", x, y, msg)
+
+    def move(self, x, y):
+        print("MOVE", x, y)
+    
+    def wait(self):
+        print("WAIT")
+        
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+ 
+class Oven(Base):
+    def __init__(self, content, timer):
+        self.content = content
+        self.timer = int(timer)
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+class Grid(Base):
+    def __init__(self, cells):
+        self.cells = cells
+        self.w, self.h = len(cells[0]), len(cells)
+        self.add_costs = {}
+
+    def at(self, x, y):
+        return self.cells[y][x]
+
+    def flatten(self):
+        return [(x, y, c) for y, row in enumerate(self.cells) for x, c in enumerate(row)]
+        
+    @property
+    def coords(self):
+        return [(x, y) for y in range(self.h) for x in range(self.w)]
+        
+    def where_are(self, content):
+        return [(x, y) for x, y, c in self.flatten() if c == content]
+
+    @staticmethod
+    def distance(from_, to_):
+        return abs(from_[0] - to_[0]) + abs(from_[1] - to_[1])
+
+    def closest(self, from_, content):
+        return sorted([(c, Grid.distance(from_, c)) for c in self.where_are(content)], key=lambda k: k[1])[0]
+
+    def neighbors(self, x, y, diags=True):
+        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 passable(self, x, y):
+        return self.at(x, y) in (FLOOR_CELL, START_0, START_1)
+    
+    def cost(self, x, y):
+        return 10 + self.add_costs.get((x, y), 0)
+    
+    def path(self, origin, target, incl_start=False):
+        nodes = []
+        origin = PathNode(*origin)
+        targets = grid.neighbors(*target)
+        heapq.heappush(nodes, (0, origin))
+        
+        while nodes:
+            current = heapq.heappop(nodes)[1]
+
+            if current in targets:
+                path = []
+                next_ = current
+                while next_:
+                    if next_ != origin or incl_start:
+                        path.insert(0, next_)
+                    next_ = next_.parent
+                return path
+            
+            neighbors = self.neighbors(*current, False)
+
+            for x, y in neighbors:
+                
+                if not self.passable(x, y):
+                    continue
+
+                cost = current.cost + self.cost(x, y)
+                priority = cost + 10 * (abs(x - target[0]) + abs(y - target[1]))
+
+                node = PathNode(x, y, current)
+                node.cost = cost
+                heapq.heappush(nodes, (priority, node))
+        else:
+            return None
+
+# input vars
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([list(input()) for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+player = Cook(-1, -1, "")
+partner = Cook(-1, -1, "")
+oven = Oven(NONE, 0)
+
+
+location = {DISH: DISHWASHER,
+            ICE_CREAM: ICE_CREAM_CRATE,
+            BLUEBERRIES: BLUEBERRIES_CRATE,
+            STRAWBERRIES: STRAWBERRIES_CRATE,
+            CHOPPED_STRAWBERRIES: CHOPPING_BOARD,
+            CROISSANT: OVEN,
+            DOUGH: DOUGH_CRATE,
+            OVEN: OVEN,
+            WINDOW: WINDOW}
+special = list(location.values())
+
+class Task(Base):
+    def __init__(self, name, from_):
+        self.name = name
+        self.loc = location[name]
+        self.pos, self.dist = grid.closest(from_, self.loc)
+
+
+ingredients_for = {CHOPPED_STRAWBERRIES: STRAWBERRIES,
+               CROISSANT: DOUGH}
+needs_ingredients = list(ingredients_for.keys())
+
+
+preparation_for = {STRAWBERRIES: CHOPPED_STRAWBERRIES,
+                   DOUGH: CROISSANT}
+needs_preparation = list(preparation_for.keys())
+
+oven_time = {CROISSANT: 10}
+needs_oven = list(oven_time.keys())
+
+
+def whats_next(todo):
+    if not todo:
+        # no task left, target is the window
+        return Task(WINDOW, player.pos)
+    
+    # special case: one or more ingredients needs preparation
+    if any((t for t in todo if t.name in needs_ingredients)):
+        
+        # If cook has an ingredient in hands, he needs to prepare it
+        if player.item in needs_preparation:
+            return Task(preparation_for[player.item], player.pos)
+    
+        # If hands are free and an ingredient is needed, we go for it first except if it is already in the oven
+        if player.item == NONE and any((t for t in todo if t.name in needs_ingredients and not t.name in oven.content)):
+            return Task(next((ingredients_for[t.name] for t in todo if t.name in needs_ingredients)), player.pos)
+        
+        
+        
+#         
+#         
+#         # special case, one or more plates needs to be cooked
+#         if any((t for t in todo if t.name in needs_oven)):
+#             
+#             # if oven has ended, go grab the oven's content
+#             if oven.timer <=1 and any((t for t in todo if t.name in oven.content)):
+#                 return Task(OVEN, player.pos)
+#             
+#             # if oven is empty, go fill it
+#             if oven.content == NONE:
+#                 return Task(next((ingredients_for[t.name] for t in todo if t.name in needs_oven)), player.pos)
+
+
+        
+    if player.item != NONE and not DISH in player.item:
+        # cook has something in its hands and no dish, he have to take one
+        return next((t for t in todo if t.name == DISH))
+
+    # else, go for the closest task
+    tasks = sorted(todo, key= lambda x: x.dist)
+    return next(iter(tasks))
+
+
+while True:
+    turns_remaining = int(input())
+    player.update(*input().split())
+    log(f"*** player: {player}")
+    
+    partner.update(*input().split())
+    log(f"*** partner: {partner}")
+    
+    num_tables_with_items = int(input())  # the number of tables in the kitchen that currently hold an item
+    tables = [Table(*input().split()) for _ in range(num_tables_with_items)]
+    log(f"*** tables: {tables}")
+    
+    oven = Oven(*input().split())
+    log(f"*** oven: {oven}")
+    
+    num_customers = int(input())  # the number of customers currently waiting for food
+    customers = [Customer(*input().split()) for _ in range(num_customers)]
+    log(f"*** customers: {customers}")
+    
+    ### SCRIPT
+    
+    # if no current order, take the most beneficial
+    if not player.order:
+        queue = sorted(customers, reverse=True, key=lambda x: x.award)
+        player.order = queue[0].item.split('-')
+        log(f'>>> new order taken: {player.order}')
+
+    todo = [Task(t, player.pos) for t in player.order if t not in player.item]
+    log(f"todo: {todo}")
+
+    next_task = whats_next(todo)
+    log(f"next_task: {next_task}")
+
+    if next_task.name == WINDOW:
+        player.order = []
+    
+    # update grid movement costs following the probability of finding the partner here
+    partner_could_be_there = [(x, y) for x, y in grid.coords if grid.passable(x, y) and grid.distance(partner.pos, (x, y)) <= 4]
+    grid.add_costs = {}
+    for x, y in partner_could_be_there:
+        k1 = 2 if (x, y) == partner.pos else 1
+        # cell is next to a special cell, partner has more chance to stop there
+        k2 = 2 if any((c for c in grid.neighbors(x, y) if grid.at(*c) in special )) else 1
+            
+        grid.add_costs[(x, y)] = k1 * k2 * 3
+
+    log(grid.add_costs)
+    
+    path = grid.path(player.pos, next_task.pos)
+    log(path)
+    
+    if path is not None:
+        if len(path) > 0:
+            if len(path) > 4:
+                player.move(*path[3])
+            else:
+                player.move(*path[-1])
+        else:
+            player.use(*next_task.pos)
+    else:
+        player.use(*next_task.pos)

+ 148 - 0
code_mode/script_ok2.py

@@ -0,0 +1,148 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import sys
+
+def log(x):
+    print(x, file=sys.stderr)
+
+# Cells
+BLUEBERRIES_CRATE = "B"
+ICE_CREAM_CRATE = "I"
+WINDOW = "W"
+EMPTY_TABLE = "#"
+DISHWASHER = "D"
+FLOOR_CELL = "."
+
+# Items
+NONE = "NONE"
+DISH = "DISH"
+ICE_CREAM = "ICE_CREAM"
+BLUEBERRIES = "BLUEBERRIES"
+
+location = {DISH: DISHWASHER,
+            ICE_CREAM: ICE_CREAM_CRATE,
+            BLUEBERRIES: BLUEBERRIES_CRATE,
+            WINDOW: WINDOW}
+
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.item = item
+        self.award = int(award)
+
+class Cook(Base):
+    def __init__(self, x, y, item):
+        
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+        self.order = ""
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    def use(self, x, y, msg=""):
+        print("USE", x, y, msg)
+
+    def move(self, x, y):
+        print("MOVE", x, y)
+        
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+ 
+class Oven(Base):
+    def __init__(self, contents, timer):
+        self.contents = contents
+        self.timer = int(timer)
+
+class Grid(Base):
+    def __init__(self, cells):
+        self.cells = cells
+
+    def at(self, x, y):
+        return self.cells[y][x]
+
+    def flatten(self):
+        return [(x, y, c) for y, row in enumerate(self.cells) for x, c in enumerate(row)]
+        
+    def where_are(self, content):
+        return [(x, y) for x, y, c in self.flatten() if c == content]
+
+    @staticmethod
+    def distance(from_, to_):
+        return abs(from_[0] - to_[0]) + abs(from_[1] - to_[1])
+
+    def closest(self, from_, content):
+        return sorted([(c, Grid.distance(from_, c)) for c in self.where_are(content)], key=lambda k: k[1])[0]
+
+# input vars
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([list(input()) for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+class Task(Base):
+    def __init__(self, name, from_):
+        self.name = name
+        self.loc = location[name]
+        self.pos, self.dist = grid.closest(from_, self.loc)
+
+while True:
+    turns_remaining = int(input())
+    player = Cook(*input().split())
+    log(f"*** player: {player}")
+    
+    partner = Cook(*input().split())
+    log(f"*** partner: {partner}")
+    
+    num_tables_with_items = int(input())  # the number of tables in the kitchen that currently hold an item
+    tables = [Table(*input().split()) for _ in range(num_tables_with_items)]
+    log(f"*** tables: {tables}")
+    
+    oven = Oven(*input().split())
+    log(f"*** oven: {oven}")
+    
+    num_customers = int(input())  # the number of customers currently waiting for food
+    customers = [Customer(*input().split()) for _ in range(num_customers)]
+    log(f"*** customers: {customers}")
+    
+    ### SCRIPT
+    
+    # if no current order, take the most beneficial
+    if not player.order:
+        queue = sorted(customers, reverse=True, key=lambda x: x.award)
+        player.order = queue[0].item.split('-')
+        log(f'>>> new order taken: {player.order}')
+        
+    todo = [Task(t, player.pos) for t in player.order if t not in player.item]
+    log(f"todo: {todo}")
+    
+    if not todo:
+        # no task left, target is the window
+        next_task = Task(WINDOW, player.pos)
+        player.order, todo = [], []
+        
+    elif player.item != NONE and not DISH in player.item:
+        # cook has something in its hands and no dish, he have to take one
+        next_task = next((t for t in todo if t.name == "DISH"))
+        
+    else:
+        # else, go for the closest task
+        tasks = sorted(todo, key= lambda x: x.dist)
+        next_task = next(iter(tasks), None)
+            
+    log(f"next_task: {next_task}")
+    
+    player.use(*next_task.pos)

+ 200 - 0
code_mode/script_ok_1.py

@@ -0,0 +1,200 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import itertools
+import sys
+
+
+def log(x):
+    print(x, file=sys.stderr)
+
+
+
+# Cells
+BLUEBERRIES_CRATE = "B"
+ICE_CREAM_CRATE = "I"
+WINDOW = "W"
+EMPTY_TABLE = "#"
+DISHWASHER = "D"
+FLOOR_CELL = "."
+
+# Items
+NONE = "NONE"
+DISH = "DISH"
+ICE_CREAM = "ICE_CREAM"
+BLUEBERRIES = "BLUEBERRIES"
+
+location = {DISH: DISHWASHER,
+            ICE_CREAM: ICE_CREAM_CRATE,
+            BLUEBERRIES: BLUEBERRIES_CRATE,
+            WINDOW: WINDOW}
+
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.item = item
+        self.award = int(award)
+
+class Cook(Base):
+    def __init__(self, x, y, item):
+        
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+        self.order = ""
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    def use(self, x, y, msg=""):
+        print("USE", x, y, msg)
+
+    def move(self, x, y):
+        print("MOVE", x, y)
+        
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+ 
+class Oven(Base):
+    def __init__(self, contents, timer):
+        self.contents = contents
+        self.timer = int(timer)
+
+class NoPathFound(Exception):
+    pass
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+class Grid(Base):
+    def __init__(self, cells):
+        self.cells = cells
+
+    def at(self, x, y):
+        return self.cells[y][x]
+    
+    def is_in(self, x, y):
+        try:
+            self.at(x, y)
+            return True
+        except IndexError:
+            return False
+
+    def flatten(self):
+        return [(x, y, c) for y, row in enumerate(self.cells) for x, c in enumerate(row)]
+        
+    def where_are(self, content):
+        return [(x, y) for x, y, c in self.flatten() if c == content]
+    
+    def is_passable(self, x, y):
+        return self.at(x, y) in (FLOOR_CELL, "0", "1")
+    
+    def closest(self, from_, content):
+        return min(self.where_are(content), key=lambda k: (abs(from_[0] - k[0]) + abs(from_[1] - k[1])))
+
+    def neighbors(self, x, y):
+        return [(xn, yn) for xn, yn in [(x - 1, y - 1), (x, y - 1), (x + 1, y - 1), \
+                                    (x - 1, y), (x + 1, y)  , \
+                                    (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)]
+                                if self.is_in(xn, yn)]
+
+    def path_to(self, origin, target):
+        nodes = []
+        origin = PathNode(*origin)
+        
+        heapq.heappush(nodes, (0, origin))
+
+        while nodes:
+            current = heapq.heappop(nodes)[1]
+            if current == target:
+                break
+
+            for x, y in self.neighbors(*current):
+
+                node = PathNode(x, y, current)
+                if not self.is_passable(*node):
+                    continue
+
+                node.cost = current.cost + 1
+                priority = node.cost + (abs(node[0] - target[0]) + abs(node[1] - target[1]))
+
+                heapq.heappush(nodes, (priority, node))
+        else:
+            raise NoPathFound("no path were found to the targetted location {}".format(target))
+
+        result = [target]
+        while current != origin:
+            result.append(tuple(current))
+            current = current.parent
+        result.append(origin)
+        result.reverse()
+
+        return result
+
+
+
+# input vars
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([list(input()) for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+while True:
+    turns_remaining = int(input())
+    player = Cook(*input().split())
+    log(f"*** player: {player}")
+    
+    partner = Cook(*input().split())
+    log(f"*** partner: {partner}")
+    
+    num_tables_with_items = int(input())  # the number of tables in the kitchen that currently hold an item
+    tables = [Table(*input().split()) for _ in range(num_tables_with_items)]
+    log(f"*** tables: {tables}")
+    
+    oven = Oven(*input().split())
+    log(f"*** oven: {oven}")
+    
+    num_customers = int(input())  # the number of customers currently waiting for food
+    customers = [Customer(*input().split()) for _ in range(num_customers)]
+    log(f"*** customers: {customers}")
+    
+    ### SCRIPT
+    
+    # if no current order, take the most beneficial
+    if not player.order:
+        queue = sorted(customers, reverse=True, key=lambda x: x.award)
+        # what is my partner doing?
+        player.order = queue[0].item
+        log(f'>>> new order taken: {player.order}')
+        
+    todo = player.order.split('-')
+    log(f"todo: {todo}")
+    
+    next_task = next([t for t in todo if not t in player.item], WINDOW)
+    if next_task == WINDOW:
+        player.order, todo = [], []
+    log(f"next_task: {next_task}")
+    
+    target = location[next_task]
+    log(f"target: {target}")
+    
+    closest = grid.closest(player.pos, target)
+    log(f"closest: {closest}")
+    
+    player.use(*closest)

+ 205 - 0
code_mode/script_ok_basic.py

@@ -0,0 +1,205 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import itertools
+import sys
+
+
+def log(x):
+    print(x, file=sys.stderr)
+
+
+# TODO: can I take ingredients not in order? If I can, find the shortest path trought all elements >> yes, but only one ingredient without a dish.
+# TODO: if partner on the way, wait if another path is 4+ cells longer
+# TODO: Should I tell wich I am doing to my partner?
+# TODO: When should I drop th plate on a table?
+# TODO: Check if a plate matching an order is already ready
+# TODO: don't take an order that is already taken by partner (how?)
+
+# Cells
+BLUEBERRIES_CRATE = "B"
+ICE_CREAM_CRATE = "I"
+WINDOW = "W"
+EMPTY_TABLE = "#"
+DISHWASHER = "D"
+FLOOR_CELL = "."
+
+# Items
+NONE = "NONE"
+DISH = "DISH"
+ICE_CREAM = "ICE_CREAM"
+BLUEBERRIES = "BLUEBERRIES"
+
+location = {DISH: DISHWASHER,
+            ICE_CREAM: ICE_CREAM_CRATE,
+            BLUEBERRIES: BLUEBERRIES_CRATE,
+            WINDOW: WINDOW}
+
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.item = item
+        self.award = int(award)
+
+class Cook(Base):
+    def __init__(self, x, y, item):
+        
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+        self.order = ""
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    def use(self, x, y, msg=""):
+        print("USE", x, y, msg)
+
+    def move(self, x, y):
+        print("MOVE", x, y)
+        
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+ 
+class Oven(Base):
+    def __init__(self, contents, timer):
+        self.contents = contents
+        self.timer = int(timer)
+
+class NoPathFound(Exception):
+    pass
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+class Grid(Base):
+    def __init__(self, cells):
+        self.cells = cells
+        self.w, self.h = len(cells[0]), len(cells)
+
+    def at(self, x, y):
+        return self.cells[y][x]
+    
+    def is_in(self, x, y):
+        try:
+            self.at(x, y)
+            return True
+        except IndexError:
+            return False
+
+    def flatten(self):
+        return [(x, y, c) for y, row in enumerate(self.cells) for x, c in enumerate(row)]
+        
+    def where_are(self, content):
+        return [(x, y) for x, y, c in self.flatten() if c == content]
+    
+    def is_passable(self, x, y):
+        return self.at(x, y) in (FLOOR_CELL, "0", "1")
+    
+    def closest(self, from_, content):
+        return min(self.where_are(content), key=lambda k: (abs(from_[0] - k[0]) + abs(from_[1] - k[1])))
+
+    def neighbors(self, x, y):
+        return [(xn, yn) for xn, yn in [(x - 1, y - 1), (x, y - 1), (x + 1, y - 1), \
+                                    (x - 1, y), (x + 1, y)  , \
+                                    (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)]
+                                if 0 <= xn <= self.w and 0 <= yn <= self.h]
+
+    def path_to(self, origin, target):
+        nodes = []
+        origin = PathNode(*origin)
+        
+        heapq.heappush(nodes, (0, origin))
+
+        while nodes:
+            current = heapq.heappop(nodes)[1]
+            if current == target:
+                break
+
+            for x, y in self.neighbors(*current):
+
+                node = PathNode(x, y, current)
+                if not self.is_passable(*node):
+                    continue
+
+                node.cost = current.cost + 1
+                priority = node.cost + (abs(node[0] - target[0]) + abs(node[1] - target[1]))
+
+                heapq.heappush(nodes, (priority, node))
+        else:
+            raise NoPathFound("no path were found to the targetted location {}".format(target))
+
+        result = [target]
+        while current != origin:
+            result.append(tuple(current))
+            current = current.parent
+        result.append(origin)
+        result.reverse()
+
+        return result
+
+# input vars
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([list(input()) for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+while True:
+    turns_remaining = int(input())
+    player = Cook(*input().split())
+    log(f"*** player: {player}")
+    
+    partner = Cook(*input().split())
+    log(f"*** partner: {partner}")
+    
+    num_tables_with_items = int(input())  # the number of tables in the kitchen that currently hold an item
+    tables = [Table(*input().split()) for _ in range(num_tables_with_items)]
+    log(f"*** tables: {tables}")
+    
+    oven = Oven(*input().split())
+    log(f"*** oven: {oven}")
+    
+    num_customers = int(input())  # the number of customers currently waiting for food
+    customers = [Customer(*input().split()) for _ in range(num_customers)]
+    log(f"*** customers: {customers}")
+    
+    ### SCRIPT
+    
+    # if no current order, take the most beneficial
+    if not player.order:
+        queue = sorted(customers, reverse=True, key=lambda x: x.award)
+        # what is my partner doing?
+        player.order = queue[0].item
+        log(f'>>> new order taken: {player.order}')
+        
+    todo = player.order.split('-')
+    log(f"todo: {todo}")
+    
+    next_task = next([t for t in todo if not t in player.item], WINDOW)
+    if next_task == WINDOW:
+        player.order, todo = [], []
+    log(f"next_task: {next_task}")
+    
+    target = location[next_task]
+    log(f"target: {target}")
+    
+    closest = grid.closest(player.pos, target)
+    log(f"closest: {closest}")
+    
+    player.use(*closest)

+ 354 - 0
code_mode/script_refact.py

@@ -0,0 +1,354 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+
+DEBUG = True
+def log(x):
+    if DEBUG:
+        print(x, file=sys.stderr)
+
+# Cells
+START_0 = '0'
+START_1 = '1'
+BLUEBERRIES_CRATE = "B"
+ICE_CREAM_CRATE = "I"
+STRAWBERRIES_CRATE = "S"
+CHOPPING_BOARD = "C"
+DOUGH_CRATE = "H"
+WINDOW = "W"
+EMPTY_TABLE = "#"
+DISHWASHER = "D"
+FLOOR_CELL = "."
+OVEN = "O"
+
+# Items
+NONE = "NONE"
+DISH = "DISH"
+ICE_CREAM = "ICE_CREAM"
+BLUEBERRIES = "BLUEBERRIES"
+STRAWBERRIES = "STRAWBERRIES"
+CHOPPED_STRAWBERRIES = "CHOPPED_STRAWBERRIES"
+DOUGH = "DOUGH"
+CROISSANT = "CROISSANT"
+
+location = {DISH: DISHWASHER,
+            ICE_CREAM: ICE_CREAM_CRATE,
+            BLUEBERRIES: BLUEBERRIES_CRATE,
+            STRAWBERRIES: STRAWBERRIES_CRATE,
+            CHOPPED_STRAWBERRIES: CHOPPING_BOARD,
+            CROISSANT: OVEN,
+            DOUGH: DOUGH_CRATE,
+            OVEN: OVEN,
+            WINDOW: WINDOW}
+special = list(location.values())
+
+preq = {CHOPPED_STRAWBERRIES: STRAWBERRIES,
+        CROISSANT: DOUGH}
+needs_preparation = {STRAWBERRIES: CHOPPED_STRAWBERRIES,
+                     DOUGH: CROISSANT}
+
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.item = item
+        self.award = int(award)
+
+class Cook(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+        self.order = []
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    def update(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+        
+    def use(self, x, y, msg=""):
+        print("USE", x, y, msg)
+
+    def move(self, x, y):
+        print("MOVE", x, y)
+    
+    def wait(self):
+        print("WAIT")
+        
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+ 
+class Oven(Base):
+    def __init__(self, contents, timer):
+        self.contents = contents
+        self.timer = int(timer)
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+class Grid(Base):
+    def __init__(self, cells):
+        self.cells = cells
+        self.w, self.h = len(cells[0]), len(cells)
+        self.add_costs = {}
+
+    def at(self, x, y):
+        return self.cells[y][x]
+
+    def flatten(self):
+        return [(x, y, c) for y, row in enumerate(self.cells) for x, c in enumerate(row)]
+        
+    @property
+    def coords(self):
+        return [(x, y) for y in range(self.h) for x in range(self.w)]
+        
+    def where_are(self, content):
+        return [(x, y) for x, y, c in self.flatten() if c == content]
+
+    @staticmethod
+    def distance(from_, to_):
+        return abs(from_[0] - to_[0]) + abs(from_[1] - to_[1])
+
+    def closest(self, from_, content):
+        return sorted([(c, Grid.distance(from_, c)) for c in self.where_are(content)], key=lambda k: k[1])[0]
+
+    def neighbors(self, x, y, diags=True):
+        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 passable(self, x, y):
+        return self.at(x, y) in (FLOOR_CELL, START_0, START_1)
+    
+    def cost(self, x, y):
+        return 10 + self.add_costs.get((x, y), 0)
+    
+    def path(self, origin, target, incl_start=False):
+        nodes = []
+        origin = PathNode(*origin)
+        targets = grid.neighbors(*target)
+        heapq.heappush(nodes, (0, origin))
+        
+        while nodes:
+            current = heapq.heappop(nodes)[1]
+
+            if current in targets:
+                path = []
+                next_ = current
+                while next_:
+                    if next_ != origin or incl_start:
+                        path.insert(0, next_)
+                    next_ = next_.parent
+                return path
+            
+            neighbors = self.neighbors(*current, False)
+
+            for x, y in neighbors:
+                
+                if not self.passable(x, y):
+                    continue
+
+                cost = current.cost + self.cost(x, y)
+                priority = cost + 10 * (abs(x - target[0]) + abs(y - target[1]))
+
+                node = PathNode(x, y, current)
+                node.cost = cost
+                heapq.heappush(nodes, (priority, node))
+        else:
+            return None
+
+# input vars
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([list(input()) for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+class Task(Base):
+    def __init__(self, name, from_):
+        self.name = name
+        self.loc = location[name]
+        self.pos, self.dist = grid.closest(from_, self.loc)
+
+
+player = Cook(-1, -1, "")
+partner = Cook(-1, -1, "")
+
+class Action(Base):
+    name = ""
+    needs_free_hands = False
+    where = ""
+
+NOT_STARTED = 0
+RUNNING = 1
+ENDED = 2
+
+        
+class Preparation(Base):
+    name = ""
+    steps = []
+    oven_time = 0
+        
+    def __init__(self):
+        self.pending = self.steps
+        self.cooked = False
+    
+    @property
+    def complete(self):
+        return not self.pending
+    
+    @property
+    def available(self):
+        return not self.oven_time or self.cooked
+    
+    def whats_next(self):
+        return self.pending.pop(0)
+    
+class Bluberries(Preparation):
+    name = BLUEBERRIES
+    steps = [BLUEBERRIES_CRATE]
+        
+class Icecream(Preparation):
+    name = ICE_CREAM
+    steps = [ICE_CREAM_CRATE]
+        
+class ChoppedStrawberries(Preparation):
+    name = CHOPPED_STRAWBERRIES
+    steps = [STRAWBERRIES_CRATE, CHOPPING_BOARD]
+        
+class Croissant(Preparation):
+    name = CROISSANT
+    steps = [DOUGH_CRATE, OVEN]
+    oven_time = 10
+
+preparations = {BLUEBERRIES: Bluberries,
+                ICE_CREAM: Icecream,
+                CHOPPED_STRAWBERRIES: ChoppedStrawberries,
+                CROISSANT: Croissant}
+
+class Order():
+    def __init__(self, order):
+        self.order = [preparations[o] for o in order]
+        
+    def complete(self):
+        return all(k for k in self.order in player.item)
+    
+    def update(self, player_item):
+        for p in self.order:
+            if p.name in player_item:
+                p.complete = True
+    
+    def on_dish(self):
+        return DISH in player.item
+    
+    def pending(self):
+        if self.complete:
+            return [WINDOW]
+        
+        
+        
+        return [p.pending[0] for p in self.pending if p.available and not p.complete]
+
+
+while True:
+    turns_remaining = int(input())
+    player.update(*input().split())
+    log(f"*** player: {player}")
+    
+    partner.update(*input().split())
+    log(f"*** partner: {partner}")
+    
+    num_tables_with_items = int(input())  # the number of tables in the kitchen that currently hold an item
+    tables = [Table(*input().split()) for _ in range(num_tables_with_items)]
+    log(f"*** tables: {tables}")
+    
+    oven = Oven(*input().split())
+    log(f"*** oven: {oven}")
+    
+    num_customers = int(input())  # the number of customers currently waiting for food
+    customers = [Customer(*input().split()) for _ in range(num_customers)]
+    log(f"*** customers: {customers}")
+    
+    ### SCRIPT
+    
+    # if no current order, take the most beneficial
+    if not player.order:
+        queue = sorted(customers, reverse=True, key=lambda x: x.award)
+        player.order = queue[0].item.split('-')
+        log(f'>>> new order taken: {player.order}')
+
+    todo = [Task(t, player.pos) for t in player.order if t not in player.item]
+    log(f"todo: {todo}")
+
+    if not todo:
+        # no task left, target is the window
+        next_task = Task(WINDOW, player.pos)
+        player.order, todo = [], []
+    
+    elif player.item in needs_preparation:
+        # cook has an ingredient in hands, he needs to prepare it
+        next_task = Task(needs_preparation[player.item], player.pos)
+    
+    elif any((t.name in preq for t in todo)) and not DISH in player.item:
+        # hands shall be free to handle ingredients, we go for it first if possible
+        next_task = Task(next((preq[t.name] for t in todo if t.name in preq)), player.pos)
+    
+    elif oven.timer < 1:
+        # oven has dinged
+        next_task = Task(OVEN, player.pos)
+    
+    elif player.item != NONE and not DISH in player.item:
+        # cook has something in its hands and no dish, he have to take one
+        next_task = next((t for t in todo if t.name == DISH))
+    
+    else:
+        # else, go for the closest task
+        tasks = sorted(todo, key= lambda x: x.dist)
+        next_task = next(iter(tasks), None)
+ 
+    log(f"next_task: {next_task}")
+    
+    # update grid movement costs following the probability of finding the partner here
+    partner_could_be_there = [(x, y) for x, y in grid.coords if grid.passable(x, y) and grid.distance(partner.pos, (x, y)) <= 4]
+    grid.add_costs = {}
+    for x, y in partner_could_be_there:
+        k1 = 2 if (x, y) == partner.pos else 1
+        # cell is next to a special cell, partner has more chance to stop there
+        k2 = 2 if any((c for c in grid.neighbors(x, y) if grid.at(*c) in special )) else 1
+            
+        grid.add_costs[(x, y)] = k1 * k2 * 3
+
+    log(grid.add_costs)
+    
+    path = grid.path(player.pos, next_task.pos)
+    log(path)
+    
+    if path is not None:
+        if len(path) > 0:
+            if len(path) > 4:
+                player.move(*path[3])
+            else:
+                player.move(*path[-1])
+        else:
+            player.use(*next_task.pos)
+    else:
+        player.use(*next_task.pos)

+ 241 - 0
code_mode/solution_wood.py

@@ -0,0 +1,241 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+
+DEBUG = False
+def log(x):
+    if DEBUG:
+        print(x, file=sys.stderr)
+
+# Cells
+START_0 = '0'
+START_1 = '1'
+BLUEBERRIES_CRATE = "B"
+ICE_CREAM_CRATE = "I"
+WINDOW = "W"
+EMPTY_TABLE = "#"
+DISHWASHER = "D"
+FLOOR_CELL = "."
+
+# Items
+NONE = "NONE"
+DISH = "DISH"
+ICE_CREAM = "ICE_CREAM"
+BLUEBERRIES = "BLUEBERRIES"
+
+location = {DISH: DISHWASHER,
+            ICE_CREAM: ICE_CREAM_CRATE,
+            BLUEBERRIES: BLUEBERRIES_CRATE,
+            WINDOW: WINDOW}
+
+special = (BLUEBERRIES_CRATE, ICE_CREAM_CRATE, WINDOW, DISHWASHER)
+
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.item = item
+        self.award = int(award)
+
+class Cook(Base):
+    def __init__(self, x, y, item):
+        
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+        self.order = ""
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    def use(self, x, y, msg=""):
+        print("USE", x, y, msg)
+
+    def move(self, x, y):
+        print("MOVE", x, y)
+    
+    def wait(self):
+        print("WAIT")
+        
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = item
+ 
+class Oven(Base):
+    def __init__(self, contents, timer):
+        self.contents = contents
+        self.timer = int(timer)
+
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+class Grid(Base):
+    def __init__(self, cells):
+        self.cells = cells
+        self.w, self.h = len(cells[0]), len(cells)
+        self.add_costs = {}
+
+    def at(self, x, y):
+        return self.cells[y][x]
+
+    def flatten(self):
+        return [(x, y, c) for y, row in enumerate(self.cells) for x, c in enumerate(row)]
+        
+    @property
+    def coords(self):
+        return [(x, y) for y in range(self.h) for x in range(self.w)]
+        
+    def where_are(self, content):
+        return [(x, y) for x, y, c in self.flatten() if c == content]
+
+    @staticmethod
+    def distance(from_, to_):
+        return abs(from_[0] - to_[0]) + abs(from_[1] - to_[1])
+
+    def closest(self, from_, content):
+        return sorted([(c, Grid.distance(from_, c)) for c in self.where_are(content)], key=lambda k: k[1])[0]
+
+    def 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 passable(self, x, y):
+        return self.at(x, y) in (FLOOR_CELL, START_0, START_1)
+    
+    def cost(self, x, y):
+        return 10 + self.add_costs.get((x, y), 0)
+    
+    def path(self, origin, target, incl_start=False):
+        nodes = []
+        origin = PathNode(*origin)
+        targets = grid.neighbors(*target, diags=True)
+        heapq.heappush(nodes, (0, origin))
+        
+        while nodes:
+            current = heapq.heappop(nodes)[1]
+
+            if current in targets:
+                path = []
+                next_ = current
+                while next_:
+                    if next_ != origin or incl_start:
+                        path.insert(0, next_)
+                    next_ = next_.parent
+                return path
+            
+            neighbors = self.neighbors(*current, False)
+
+            for x, y in neighbors:
+                
+                if not self.passable(x, y):
+                    continue
+
+                cost = current.cost + self.cost(x, y)
+                priority = cost + 10 * (abs(x - target[0]) + abs(y - target[1]))
+
+                node = PathNode(x, y, current)
+                node.cost = cost
+                heapq.heappush(nodes, (priority, node))
+        else:
+            return None
+
+# input vars
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([list(input()) for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+class Task(Base):
+    def __init__(self, name, from_):
+        self.name = name
+        self.loc = location[name]
+        self.pos, self.dist = grid.closest(from_, self.loc)
+
+while True:
+    turns_remaining = int(input())
+    player = Cook(*input().split())
+    log(f"*** player: {player}")
+    
+    partner = Cook(*input().split())
+    log(f"*** partner: {partner}")
+    
+    num_tables_with_items = int(input())  # the number of tables in the kitchen that currently hold an item
+    tables = [Table(*input().split()) for _ in range(num_tables_with_items)]
+    log(f"*** tables: {tables}")
+    
+    oven = Oven(*input().split())
+    log(f"*** oven: {oven}")
+    
+    num_customers = int(input())  # the number of customers currently waiting for food
+    customers = [Customer(*input().split()) for _ in range(num_customers)]
+    log(f"*** customers: {customers}")
+    
+    ### SCRIPT
+    
+    # if no current order, take the most beneficial
+    if not player.order:
+        queue = sorted(customers, reverse=True, key=lambda x: x.award)
+        player.order = queue[0].item.split('-')
+        log(f'>>> new order taken: {player.order}')
+        
+    todo = [Task(t, player.pos) for t in player.order if t not in player.item]
+    log(f"todo: {todo}")
+    
+    if not todo:
+        # no task left, target is the window
+        next_task = Task(WINDOW, player.pos)
+        player.order, todo = [], []
+        
+    elif player.item != NONE and not DISH in player.item:
+        # cook has something in its hands and no dish, he have to take one
+        next_task = next((t for t in todo if t.name == "DISH"))
+        
+    else:
+        # else, go for the closest task
+        tasks = sorted(todo, key= lambda x: x.dist)
+        next_task = next(iter(tasks), None)
+            
+    log(f"next_task: {next_task}")
+    
+    # update grid movement costs following the probability of finding the partner here
+    partner_could_be_there = [(x, y) for x, y in grid.coords if grid.passable(x, y) and grid.distance(partner.pos, (x, y)) <= 4]
+    grid.add_costs = {}
+    for x, y in partner_could_be_there:
+        k1 = 2 if (x, y) == partner.pos else 1
+        # cell is next to a special cell, partner has more chance to stop there
+        k2 = 2 if any((c for c in grid.neighbors(x, y) if grid.at(*c) in special )) else 1
+            
+        grid.add_costs[(x, y)] = k1 * k2 * 3
+
+    log(grid.add_costs)
+    
+    path = grid.path(player.pos, next_task.pos)
+    log(path)
+    
+    if path is not None:
+        if len(path) > 0:
+            if len(path) > 4:
+                player.move(*path[3])
+            else:
+                player.move(*path[-1])
+        else:
+            player.use(*next_task.pos)
+    else:
+        player.use(*next_task.pos)

+ 19 - 0
darts.py

@@ -0,0 +1,19 @@
+'''
+> https://www.codingame.com/ide/puzzle/darts
+@author: olivier.massot, 2019
+'''
+
+size = int(input())
+radius = size // 2
+
+players = [input() for _ in range(int(input()))]
+scores = {p: [] for p in players}
+
+for _ in range(int(input())):
+    name_, x, y = input().split()
+    x, y = int(x), int(y)
+    score = 5 * (((abs(x) + abs(y)) <= radius) + ((x**2 + y**2) <= radius**2) + (abs(x) <= radius and abs(y) <= radius))
+    scores[name_].append(score)
+
+for s, _, p in sorted([(sum(scores[p]), i, p) for i, p in enumerate(players)], key=lambda x: (x[0], -1*x[1]), reverse=True):
+    print(f"{p} {s}")

+ 36 - 0
dead_men_shot.py

@@ -0,0 +1,36 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+shape = [tuple([int(j) for j in input().split()]) for _ in range(int(input()))]
+shots = [tuple([int(j) for j in input().split()]) for _ in range(int(input()))]
+
+xmin, xmax = min([c[0] for c in shape]), max([c[0] for c in shape])
+edges = list(zip(shape[:-1], shape[1:])) + [(shape[-1], shape[0])]
+oedges = [sorted(e, key=lambda x: x[0]) for e in edges]
+
+def is_in(xs, ys):
+    if not xmin <= xs <= xmax:
+        return False
+    
+    fx = []
+    for start, end in oedges:
+        # vertical edge
+        if start[0] == end[0]:
+            if xs == start[0] and start[1] <= ys <= end[1]:
+                return True
+            else:
+                continue
+        if start[0] <= xs <= end[0]:
+            a = (end[1] - start[1]) / (end[0] - start[0])
+            b = -1 * a * end[0] + end[1]
+            fx.append(a * xs + b)
+    
+    fx = sorted(list(set(fx)))
+    for y0, y1 in zip(fx[:-1:2], fx[1::2]):
+        if y0 <= ys <= y1:
+            return True
+    return False
+
+for s in shots:
+    print("hit" if is_in(*s) else "miss")

+ 1293 - 0
i_n_f/script.py

@@ -0,0 +1,1293 @@
+'''
+>> https://www.codingame.com/ide/173171838252e7c6fd6f3ff9cb8169431a08eec1
+@author: olivier.massot, may 2019
+'''
+import heapq
+import sys
+import time
+
+# TODO
+# * take units and towers in account when computing the threat
+# ? when priority needs a lvl3 unit, find a way to spare
+# * resurrect the strategic value: number of cells depending of the cell
+# * consider defending also cells not owned
+# * make a first turn of moves for units deep inside the territory, to avoid unecessary training and computing
+# x reduce interest of defend a pivot it has no movable cell around
+# ! ensure lvl3 units are working!
+
+debug = True
+t0 = time.time()
+
+def log(*msg):
+    if debug:
+        print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr)
+
+# OWNER
+ME = 0
+OPPONENT = 1
+
+# BUILDING TYPE
+HQ = 0
+    
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+class Queue(Base):
+    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 InterestQueue(Queue):
+    def __add__(self, other):
+        self.items += other.items
+        return self
+    
+    def put(self, item):
+        heapq.heappush(self.items, item)
+        
+    def get(self):
+        return heapq.heappop(self.items)
+
+class BasePosition(Base):
+    def __init__(self, cell, *args, **kwargs):
+        self.interest = 0
+        self.cell = cell
+        
+    @property
+    def x(self):
+        return self.cell.x
+        
+    @property
+    def y(self):
+        return self.cell.y
+        
+    @property
+    def pos(self):
+        return self.cell.pos
+        
+    def __lt__(self, other):
+        return self.interest < other.interest
+
+    def eval(self):
+        raise NotImplementedError
+        
+        
+class Position(BasePosition):
+    def __init__(self, cell):
+        super().__init__(cell)
+        
+        self.possession = 0
+        self.threat = 0
+        self.pivot = 0
+        self.union = 0
+        self.depth = 0
+        self.hq = 0
+        self.tower = 0
+        self.mine = 0
+        self.dist_to_goal = 0
+        
+        self.min_level = 1
+        
+    def pre_eval(self):
+        
+        # *** Eval interest: the lower the better
+        
+        self.interest = 0
+        self.min_level = 1
+        
+        # eval possession
+        if self.cell.active_owned:
+            self.possession = 1
+            
+        elif self.cell.active_opponent:
+            self.possession = -1
+
+        # eval threat
+        self.threat = 0
+        if self.cell.active_owned:
+            self.threat = self.cell.threat
+
+        self.covers = sum([grid[n].owned for n in self.cell.neighbors])
+        
+        # eval pivot
+        self.pivot = self.cell.pivot_value
+        self.next_to_pivot = any(grid[n].pivot_value for n in self.cell.neighbors)
+        
+        # cell is not easily reachable
+        self.protected = len([n for n in grid.neighbors(*self.cell.pos, True) if not grid[n].movable]) == 6
+
+        # distance to the ennemy HQ
+        self.dist_to_goal = Grid.manhattan(self.cell.pos, opponent.hq.pos)
+        
+        # distance to the own HQ
+        self.dist_to_hq = Grid.manhattan(self.cell.pos, player.hq.pos)
+        
+        # distance to the center of the map
+        self.dist_to_center = Grid.manhattan(self.cell.pos, (6,6))
+        
+        # priorize adjacent cells
+        self.union = len([n for n in self.cell.neighbors if grid[n].active_owned])
+        
+        # favorize dispersion
+        self.concentration = len([n for n in self.cell.neighbors if grid[n].unit and grid[n].unit.owned])
+        
+        self.deadends = len([n for n in self.cell.neighbors if not grid[n].movable])
+        
+        # include 'depthmap'
+        self.depth = self.cell.depth
+        
+        self.under_tower = self.cell.under_tower
+        self.overlaps_tower = sum([grid[n].under_tower for n in self.cell.neighbors])
+        
+        # priorize mines or HQ
+        if self.cell.building:
+            self.hq = int(self.cell.building.type_ == Building.HQ)
+            self.tower = int(self.cell.building.type_ == Building.TOWER)
+            self.mine = int(self.cell.building.type_ == Building.MINE)
+        
+        # *** Min level to go there
+        if self.cell.unit and self.cell.unit.opponents:
+            self.min_level = min([self.cell.unit.level + 1, 3])
+
+        if self.cell.under_tower:
+            self.min_level = 3
+
+    def eval(self):
+        self.pre_eval()
+        self.interest = 3 * self.depth + self.dist_to_goal + 2 * self.concentration + 2 * self.deadends
+
+    def __repr__(self):
+        detail = [self.possession, self.threat, self.pivot, self.dist_to_goal,
+                  self.union, self.concentration, self.depth, self.hq, self.tower, self.mine]
+        return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
+        
+        
+class Defend(Position):
+    def __init__(self, cell):
+        super().__init__(cell)
+    
+    def __repr__(self):
+        detail = [self.threat, self.covers, self.pivot, 
+                  self.dist_to_hq, self.cell.danger, self.cell.critical, 
+                  self.under_tower, self.overlaps_tower]
+        return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
+    
+    def eval(self):
+        self.pre_eval()
+        self.interest = 20 \
+                        - 2 * self.covers \
+                        - (8 * self.pivot if not self.protected else 4 * self.pivot) \
+                        - 80 * self.cell.critical \
+                        - 25 * self.cell.danger \
+                        + 25 * self.cell.under_tower \
+                        + 10 * self.overlaps_tower \
+                        + self.dist_to_hq
+        
+class Secure(Position):
+    def __init__(self, cell, emergency = False):
+        super().__init__(cell)
+        self.emergency = emergency
+    
+    def __repr__(self):
+        detail = [self.threat, self.covers, self.pivot,  
+                  self.under_tower, self.overlaps_tower]
+        return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
+    
+    def eval(self):
+        self.pre_eval()
+        self.interest = 30 \
+                        - 2 * self.threat \
+                        - 2 * self.covers \
+                        - (8 * self.pivot if not self.protected else 2 * self.pivot) \
+                        + 25 * self.cell.under_tower \
+                        + 10 * self.overlaps_tower \
+                        + self.dist_to_center
+        
+class Attack(Position):
+    def eval(self):
+        self.pre_eval()
+        self.interest = 15 * self.possession \
+                        - 5 * self.pivot \
+                        - 3 * self.next_to_pivot \
+                        - 2 * self.union \
+                        + 4 * self.dist_to_goal \
+                        - 25 * self.tower \
+                        - 15 * self.mine \
+                        - 100 * self.hq \
+                        - 50 * self.cell.critical \
+                        - 20 * self.cell.danger \
+                        + 10 * (self.min_level - 1) \
+                        - 15 * (self.cell.pos in sum(grid.divide, []))
+        
+class Colonize(Position):
+    def eval(self):
+        self.pre_eval()
+        self.interest = 15 * self.possession \
+                        - 5 * self.next_to_pivot \
+                        - 2 * self.union \
+                        + 3 * self.concentration \
+                        + 2 * self.deadends \
+                        + 4 * self.depth \
+                        + 2 * self.dist_to_goal \
+                        - max([0, 3 - self.dist_to_hq]) \
+                        + 2 * self.dist_to_center
+                        
+        
+class MinePosition(BasePosition):
+    def __init__(self, target):
+        super().__init__(target)
+    
+    def eval(self):
+        # the lower the better
+        self.interest -= self.cell.depth
+
+class BaseLoc(Base):
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+        
+    @property
+    def pos(self):
+        return self.x, self.y
+
+class MineSite(BaseLoc):
+    def __init__(self, x, y):
+        super().__init__(x, y)
+
+class BaseOwnedLoc(BaseLoc):
+    def __init__(self, x, y, owner):
+        super().__init__(x, y)
+        self.owner = owner
+
+    @property
+    def owned(self):
+        return self.owner == ME
+
+    @property
+    def opponents(self):
+        return self.owner == OPPONENT
+
+class Building(BaseOwnedLoc):
+    HQ = 0
+    MINE = 1
+    TOWER = 2
+    
+    cost = {0: 0, 1: 20, 2: 15}
+    maintenance = {0: 0, 1: 0, 2: 0}
+    
+    def __init__(self, owner, type_, x, y):
+        super().__init__(x, y, owner)
+        self.type_ = type_
+
+    @property
+    def hq(self):
+        return self.type_ == Building.HQ
+
+
+
+class Unit(BaseOwnedLoc):
+    cost = {1: 10, 2: 20, 3: 30}
+    maintenance = {1: 1, 2: 4, 3: 20}
+    def __init__(self, owner, id_, level, x, y):
+        super().__init__(x, y, owner)
+        self.id_ = id_
+        self.level = level
+        
+        self.has_moved = False
+
+class Player(Base):
+    def __init__(self, id_):
+        self.id_ = id_
+        self.gold = 0
+        self.income = 0
+        self.units = []
+        self.buildings = []
+        self.hq = None
+        
+        self.spent = 0
+        self.new_charges = 0
+        self.to_spare = 0
+
+    def update(self, gold, income, units, buildings):
+        self.gold = gold
+        self.income = income
+        self.units = [u for u in units if u.owner == self.id_]
+        self.buildings = [b for b in buildings if b.owner == self.id_]
+        self.hq = next((b for b in self.buildings if b.type_ == HQ))
+
+        self.spent = 0
+        self.new_charges = 0
+
+    @property
+    def current_gold(self):
+        return self.gold - self.spent
+
+    @property
+    def current_income(self):
+        return self.income - self.new_charges
+
+    def training_capacity(self):
+        return min([(self.gold - self.spent) // Unit.cost[1], (self.income - self.new_charges) // Unit.maintenance[1]])
+
+    def can_afford(self, lvl):
+        return (self.gold - self.spent) >= Unit.cost[lvl] and (self.income - self.new_charges) >= Unit.maintenance[lvl]
+
+    def max_affordable(self):
+        for lvl in range(3, 0, -1):
+            if self.can_afford(lvl):
+                return lvl
+        return 0
+
+    def can_act(self):
+        return self.training_capacity() > 0 or self.gold >= 15 or any(not unit.has_moved for unit in self.units)
+
+class Cell(Base):
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+        self._content = "#"
+        self.neighbors = []
+        self.unit = None
+        self.building = None
+        self.mine_site = None
+        
+        self.under_tower = False
+        self.depth = 0
+        self.pivot_for = []
+        self.pivot_value = 0
+        self.critical = False
+        self.danger = False
+        self.threat = 0
+        self.threat_by = None
+        
+    @property
+    def pos(self):
+        return self.x, self.y
+        
+    @property
+    def raw_val(self):
+        return self._content
+
+    def update(self, content, unit = None, building = None):
+        self._content = content
+        self.unit = unit
+        self.building = building
+        
+        self.under_tower = False
+        self.depth = 0
+        self.pivot_for = []
+        self.pivot_value = 0
+        self.threat = 0
+        self.threat_by = None
+        self.critical = False
+        self.danger = False
+
+    @property
+    def movable(self):
+        return self._content != "#"
+    
+    @property
+    def owned(self):
+        return self._content.lower() == "o"
+    
+    @property
+    def opponents(self):
+        return self._content.lower() == "x"
+    
+    @property
+    def owner(self):
+        if self.owned:
+            return ME
+        elif self.opponents:
+            return OPPONENT
+        else:
+            return None
+
+        
+    @property
+    def headquarter(self):
+        return self.pos in Grid.hqs
+    
+    @property
+    def occupied(self):
+        return self.unit or self.building
+    
+    @property
+    def active(self):
+        return self._content.isupper()
+    
+    @property
+    def active_owned(self):
+        return self._content == "O"
+    
+    @property
+    def active_opponent(self):
+        return self._content == "X"
+    
+    def is_active_owner(self, player_id):
+        if player_id == ME:
+            return self.active_owned
+        elif player_id == OPPONENT:
+            return self.active_opponent
+        else:
+            return False
+    
+    def owned_unit(self):
+        if self.unit and self.unit.owned:
+            return self.unit
+    
+    def owned_building(self):
+        if self.building and self.building.owned:
+            return self.building
+    
+    def take_possession(self):
+        self._content = "O"
+    
+    def desactivate(self):
+        self._content = self._content.lower()
+    
+    def get_unit_level(self):
+        return self.unit.level if self.unit else 0
+    
+class Node(Base):
+    def __init__(self, pos, path=[]):
+        self.pos = pos
+        self.path = path
+        
+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):
+    dim = 12
+    hqs = [(0,0), (11,11)]
+    
+    def __init__(self, mines_sites = []):
+        
+        self.cells = {(x, y): Cell(x, y) for x in range(Grid.dim) for y in range(Grid.dim)}
+        for pos, cell in self.cells.items():
+            cell.neighbors = [p for p in self.neighbors(*pos) if p in self.cells]
+            
+        self.units = []
+        self.buildings = []
+        for m in mines_sites:
+            self.cells[(m.x, m.y)].mine_site = m
+        self.threat_level = 0
+        
+    def print_grid(self):
+        return "\n".join(["".join([c for c in row]) for row in self.grid])
+    
+    @property
+    def pos(self):
+        return self.x, self.y
+    
+    @property
+    def grid(self):
+        return [[self.cells[(x, y)].raw_val for x in range(Grid.dim)] for y in range(Grid.dim)]
+
+    def __getitem__(self, key):
+        return self.cells[key]
+
+    def update(self, grid, buildings, units):
+        buildings_ix = {(b.x, b.y): b for b in buildings}
+        units_ix= {(u.x, u.y): u for u in units}
+        
+        self.buildings = list(buildings)
+        self.units = list(units)
+        
+        for y, row in enumerate(grid):
+            for x, c in enumerate(row):
+                self.cells[(x, y)].update(c, 
+                                          units_ix.get((x, y), None), 
+                                          buildings_ix.get((x, y), None))
+
+        log(" * update pivots")
+#         self.update_pivots()
+        self.update_propagation(ME)
+        self.update_propagation(OPPONENT)
+        
+        log(" * update threats")
+        self.update_threats()
+        
+        self.update_state()
+
+        self.update_divide_and_conquer()
+
+    def update_state(self):
+        self.update_tower_areas()
+        self.update_frontlines()
+        self.update_depth_map()
+
+    @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 = [(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 < Grid.dim and 0 <= y < Grid.dim]
+    
+    @classmethod
+    def zone(cls, center, radius):
+        return [(x, y) for x in range(0, cls.dim) for y in range(0, cls.dim) if cls.manhattan(center, (x, y)) <= radius]      
+    
+    def zone_set(self, center, radius):
+        zone = set(center)
+        for _ in range(radius):
+            for p in zone:
+                zone |= self.neighbors(*p)
+
+    @staticmethod
+    def closest(from_, in_):
+        return min(in_, key=lambda x: Grid.manhattan(from_, x))
+    
+    def get_hq(self, player_id):
+        return next((b for b in self.buildings if b.owner == player_id and b.hq))
+    
+    def update_tower_areas(self):
+        for b in self.buildings:
+            if b.type_ == Building.TOWER:
+                self.cells[b.pos].under_tower = True
+                for n in self.cells[b.pos].neighbors:
+                    self.cells[n].under_tower = True
+    
+    def update_frontlines(self):
+        # update the current frontlines
+        self.frontline = []
+        self.frontex = []
+        
+        for cell in self.cells.values():
+            if cell.active_owned:
+                if any(self.cells[c].movable and not self.cells[c].active_owned
+                       for c in cell.neighbors):
+                    self.frontline.append(cell)
+
+    def update_depth_map(self):
+        buffer = [c.pos for c in self.frontline]
+        for p in buffer:
+            self.cells[p].depth = 1
+            
+        next_buffer = []
+        while buffer:
+            for p in buffer:
+                for n in self.cells[p].neighbors:
+                    if self.cells[n].active_owned and not self.cells[n].depth:
+                        self.cells[n].depth = self.cells[p].depth + 1
+                        next_buffer.append(n)
+                    
+            buffer = list(next_buffer)
+            next_buffer = []
+    
+    def _active_owned(self, pos, player_id):
+        try:
+            return self.cells[pos].is_active_owner(player_id)
+        except KeyError:
+            return False
+        
+    def update_propagation(self, player_id):
+        start = self.get_hq(player_id).pos
+        lvl = 0
+        propagation = {start: (lvl, [])}
+        
+        pivots_cells = []
+        for x, y in self.cells:
+            if (x, y) != start and self._active_owned((x, y), player_id):
+                
+                around = [(x, y - 1), (x + 1, y - 1), (x + 1, y), (x + 1, y + 1),
+                             (x, y + 1), (x - 1, y + 1), (x - 1, y), (x - 1, y - 1)]
+                owned = [self._active_owned(p, player_id) for p in around]
+                changes = [x for x in zip(owned, owned[1:]) if x == (True, False)]
+                
+                if len(changes) > 1:
+                    pivots_cells.append((x, y))
+        pivots = {p: [] for p in pivots_cells}
+        
+        buffer = [start]
+        while buffer:
+            new_buffer = []
+            lvl += 1
+            for pos in buffer:
+                for n in self.neighbors(*pos):
+                    if self._active_owned(n, player_id):
+                        if not n in propagation:
+                            propagation[n] = (lvl, [pos])
+                            new_buffer.append(n)
+                        else:
+                            # already visited
+                            if propagation[pos][1] != [n] and propagation[n][0] >= propagation[pos][0]:
+                                propagation[n][1].append(pos)
+            
+            buffer = new_buffer
+        
+        self.propagation = propagation
+
+        children = {}
+        for p, data in self.propagation.items():
+            _, parents = data
+            for parent in parents:
+                if not parent in children:
+                    children[parent] = []
+                children[parent].append(p)
+        
+        for pivot in pivots:
+            buffer = set(children.get(pivot, []))
+            
+            while buffer:
+                new_buffer = set()
+                 
+                for child in buffer:
+                    new_buffer |= set(children.get(child, []))
+               
+                pivots[pivot] += list(buffer)
+                buffer = new_buffer
+            
+        # cleaning 'false children'
+        for pivot, children in pivots.items():
+            invalid = []
+            for child in children:
+                parents = self.propagation[child][1]
+                if any((p != pivot and p not in children) or p in invalid for p in parents):
+                    invalid.append(child)
+            for p in invalid:
+                children.remove(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
+            self.cells[pivot].pivot_value = sum([1 + grid[p].get_unit_level() for p in pivot_for])
+            
+    def update_divide_and_conquer(self):
+        segs = []
+        for c in self.frontline:
+            for n in c.neighbors:
+                if n in self.cells and self.cells[n].active_opponent:
+                    seg = []
+                    x, y = c.pos
+                    xn, yn = n
+                    
+                    if xn > x:
+                        dx = 1
+                    elif xn < x:
+                        dx = -1
+                    else:
+                        dx = 0
+                    
+                    if yn > y:
+                        dy = 1
+                    elif yn < y:
+                        dy = -1
+                    else:
+                        dy = 0
+        
+                    x, y = x + dx, y + dy
+                    while (x, y) in self.cells and self.cells[(x, y)].active_opponent:
+                        seg.append((x, y))
+                        x, y = x + dx, y + dy
+                        
+                    if seg:
+                        segs.append(seg)
+            
+        self.divide = [s for s in segs if len(s) <= 4]
+        log(self.divide)
+            
+
+    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]
+        
+        ennemy_frontier = [c for c in self.cells.values() if c.active_opponent \
+                           and any(self.cells[n].movable and not self.cells[n].opponents for n in c.neighbors)]
+
+        for cell in self.cells.values():
+            if cell.owned:
+                closest, dist = min([(o, Grid.manhattan(cell.pos, o.pos)) for o in ennemy_frontier], key=lambda x: x[1])
+                if cell.unit:
+                    dist += cell.unit.level
+                if cell.under_tower:
+                    dist += 2
+                    
+                if dist < self.threat_level:
+                    cell.threat = self.threat_level - dist
+                    cell.threat_by = closest
+        
+        hqcell = grid[player.hq.pos]
+        self.emergency = hqcell.threat > 0
+        
+        self.update_threat_path()
+            
+    def update_threat_path(self):
+        
+        hqcell = grid[player.hq.pos]
+        if not hqcell.threat > 0:
+            return
+        closest_threat = hqcell.threat_by
+        log(f"* {closest_threat.pos} threats HQ")
+        
+        path  = self.path(closest_threat.pos, player.hq.pos)
+        if path:
+            log(f"* Path to HQ: {path}")
+            extended_path = set()
+            for p in path:
+                extended_path |= set(self.neighbors(*p))
+            extended_path -= set(path)
+            
+            for p in path:
+                self.cells[p].critical = True
+            for p in extended_path:
+                self.cells[p].danger = True
+    
+    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 influence_zone(self, player_id):
+        owned, neighbors = {p for p, c in self.cells.items() if c.owner == player_id and c.active}, set()
+        for p in owned:
+            neighbors |= set(self.neighbors(*p))
+        return (self.cells[p] for p in (owned | neighbors) if self.cells[p].movable)
+               
+    def training_places(self):
+        return (c for c in self.influence_zone(ME) if self.can_move(c.pos))
+    
+    def get_next_training(self, max_level=3):
+        q = InterestQueue()
+        for cell in self.training_places():
+            q.put(Position(cell))
+        if not q:
+            return None
+        
+        action = q.get()
+    
+        if max_level < 3: 
+            while action.cell.under_tower:
+                try:
+                    action = q.get()
+                except IndexError:
+                    return None
+        level = 1
+        for ennemy in opponent.units:
+            if Grid.manhattan(action.cell.pos, ennemy.pos) < 3:
+                level = min(ennemy.level + 1, max_level)
+                break
+                
+        action.level = level
+        return action
+    
+    def can_move(self, pos, level=1):
+        cell = self.cells[pos]
+        can_move = True
+        
+        can_move &= cell.movable
+        can_move &= not cell.owned_unit()
+        can_move &= not cell.owned_building()
+        if level != 3:
+            can_move &= (cell.unit is None or cell.unit.level < level)
+            can_move &= cell.owned or not cell.under_tower
+        return can_move
+    
+    def moving_zone(self, unit):
+        return (self.cells[p] for p in self.cells[unit.pos].neighbors 
+                if self.can_move(p, unit.level))
+        
+    def get_next_move(self, unit):
+        q = InterestQueue()
+        for cell in self.moving_zone(unit):
+            o = Position(cell)
+            o.eval()
+            q.put(o)
+        if not q:
+            return None
+        objective = q.get()
+        return objective
+    
+    def building_zone(self, type_):
+        if type_ == Building.MINE:
+            return [cell for cell in self.cells.values() if cell.mine_site and cell.depth > 3]
+        else:
+            return []
+
+    def get_building_site(self, type_):
+        q = InterestQueue()
+        for cell in self.building_zone(type_):
+            q.put(MinePosition(cell))
+        if not q:
+            return None
+        return q.get()
+
+    def remove_unit_from(self, cell):
+        opponent.units.remove(cell.unit)
+        self.units.remove(cell.unit)
+        cell.unit = None
+
+    def cost_for_new_mine(self):
+        return Building.cost[Building.MINE] + 4 * len([b for b in player.buildings if b.type_ == Building.MINE])
+
+    def apply(self, action):
+        if type(action) is Move:
+            old_cell, new_cell = self.cells[action.unit.pos], action.cell
+            
+            if new_cell.unit:
+                if new_cell.unit.owned:
+                    log("ERROR: impossible move (owned unit here)")
+                    return
+                if action.unit.level < 3 and new_cell.unit.level >= action.unit.level:
+                    log("ERROR: impossible move (unit here, level too low)")
+                    return
+                # cell is occupied by an opponent's unit with an inferior level
+                self.remove_unit_from(new_cell)
+            
+            if new_cell.building and new_cell.building.type_ == Building.TOWER:
+                if new_cell.owned:
+                    log("ERROR: impossible move (allied tower here)")
+                if action.unit.level < 3:
+                    log("ERROR: impossible move (tower here, level too low)")
+                opponent.buildings.remove(new_cell.building)
+                self.buildings.remove(new_cell.building)
+                new_cell.building = None
+                
+            old_cell.unit = None
+            action.unit.x, action.unit.y = new_cell.pos
+            new_cell.unit = action.unit
+            action.unit.has_moved = True
+            
+            if not new_cell.owned:
+                if new_cell.opponents:
+                    for p in new_cell.pivot_for:
+                        self.cells[p].desactivate()
+                        if self.cells[p].unit and self.cells[p].unit.opponents:
+                            self.remove_unit_from(self.cells[p])
+                new_cell.take_possession()
+            
+                self.update_state()
+        
+        elif type(action) is Train:
+            new_cell = action.cell
+            unit = Unit(ME, None, action.level, *new_cell.pos)
+            unit.has_moved = True
+            
+            player.spent += Unit.cost[action.level]
+            player.new_charges += Unit.maintenance[action.level]
+            
+            if new_cell.unit:
+                if new_cell.unit.owned:
+                    log("ERROR: impossible training")
+                    return
+                if unit.level < 3 and new_cell.unit.level >= unit.level:
+                    log("ERROR: impossible training")
+                    return
+                # cell is occupied by an opponent's unit with an inferior level
+                self.remove_unit_from(new_cell)
+            
+            if new_cell.building and new_cell.building.opponents and new_cell.building.type_ == Building.TOWER:
+                if unit.level < 3:
+                    log("ERROR: impossible training")
+                opponent.buildings.remove(new_cell.building)
+                self.buildings.remove(new_cell.building)
+                new_cell.building = None
+            
+            new_cell.unit = unit
+            
+            if not new_cell.owned:
+                if new_cell.opponents:
+                    for p in new_cell.pivot_for:
+                        self.cells[p].desactivate()
+                        if self.cells[p].unit and self.cells[p].unit.opponents:
+                            self.remove_unit_from(self.cells[p])
+                new_cell.take_possession()
+                
+                self.update_state()
+            
+        elif type(action) is BuildTower:
+            new_cell = action.cell
+            building = Building(ME, Building.TOWER, *new_cell.pos)
+            new_cell.building = building
+            player.buildings.append(building)
+            
+            player.spent += Building.cost[Building.TOWER]
+            
+            if building.type_ == Building.TOWER:
+                new_cell.under_tower = True
+                for n in new_cell.neighbors:
+                    self.cells[n].under_tower = True
+
+            self.update_state()
+            if self.emergency:
+                self.update_threat_path()
+
+        elif type(action) is BuildMine:
+            
+            player.spent += self.cost_for_new_mine()
+            
+            new_cell = action.cell
+            building = Building(ME, Building.MINE, *new_cell.pos)
+            new_cell.building = building
+            player.buildings.append(building)
+
+class Action(Base):
+    def command(self):
+        raise NotImplementedError()
+    
+class Wait(Action):
+    def command(self):
+        return "WAIT"
+
+class Move(Action):
+    def __init__(self, unit, cell):
+        self.unit = unit
+        self.cell = cell
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.unit.id_} {self.cell.pos}>"
+
+    def command(self):
+        return f"MOVE {self.unit.id_} {self.cell.x} {self.cell.y}"
+    
+class Train(Action):
+    def __init__(self, level, cell):
+        self.level = level
+        self.cell = cell
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.level} {self.cell.pos}>"
+    
+    def command(self):
+        return f"TRAIN {self.level} {self.cell.x} {self.cell.y}"
+
+class Build(Action):
+    str_types = {1: "MINE", 2: "TOWER"}
+    def __init__(self, type_, cell):
+        self.type_ = type_
+        self.cell = cell
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.str_types[self.type_]} {self.cell.pos}>"
+    
+    def command(self):
+        return f"BUILD {self.str_types[self.type_]} {self.cell.x} {self.cell.y}"
+
+class BuildTower(Build):
+    def __init__(self, cell):
+        super().__init__(Building.TOWER, cell)
+
+class BuildMine(Build):
+    def __init__(self, cell):
+        super().__init__(Building.MINE, cell)
+        
+        
+# ******** MAIN *************
+
+test = False
+
+if test:
+    mines_input = []
+else:
+    mines_input = [input() for _ in range(int(input()))]
+
+mines_sites = [MineSite(*[int(j) for j in item.split()]) for item in mines_input]
+# log(f"* mines: {mines_sites}")
+
+grid = Grid(mines_sites)
+player = Player(ME)
+opponent = Player(OPPONENT)
+
+
+def cmd_wait():
+    return "WAIT"
+
+def get_input():
+    if test:
+        gold, income = 20, 1
+        opponent_gold, opponent_income = 20, 1
+        new_grid_input = ['O..#########', '...###...###', '...###....##', '#..##.....##', '...#......##', '#.........##', '##.........#', '##......#...', '##.....##..#', '##....###...', '###...###...', '#########..X']
+        buildings_input = ['0 0 0 0', '1 0 11 11']
+        units_input = []
+        
+    else:
+        gold, income = int(input()), int(input())
+        opponent_gold, opponent_income = int(input()), int(input())
+        new_grid_input = [input() for _ in range(12)]
+        buildings_input = [input() for _ in range(int(input()))]
+        units_input = [input() for _ in range(int(input()))]
+    
+#         log(gold, income, opponent_gold, opponent_income)
+#         log(new_grid_input)
+#         log(buildings_input)
+#         log(units_input)
+    
+    return gold, income, opponent_gold, opponent_income, new_grid_input, buildings_input, units_input
+
+
+while True:
+    
+    
+    # <--- get and parse input 
+    gold, income, opponent_gold, opponent_income, new_grid_input, buildings_input, units_input = get_input()
+
+    new_grid = [list(row) for row in new_grid_input]
+    
+    buildings = [Building(*[int(j) for j in item.split()]) for item in buildings_input]
+#     log(f"* buildings: {buildings}")    
+    
+    units = [Unit(*[int(j) for j in item.split()]) for item in units_input]
+#     log(f"* units: {units}")   
+    # --->
+    
+    log("## Start ##")
+    log("* Update")
+    
+    # <--- update
+    player.update(gold, income, units, buildings)
+#     log(f"player: {player}")
+    
+    opponent.update(opponent_gold, opponent_income, units, buildings)
+#     log(f"opponent: {opponent}")
+    
+    grid.update(new_grid, buildings, units)
+#     log(f"grid:\n{grid.print_grid()}")
+    # --->
+    
+    commands = []
+    
+    # start
+    log(f"Threat level: {grid.threat_level}")
+    if grid.emergency:
+        log("<!> HQ is threaten!")
+    
+    todo = []
+    played, abandonned = [], []
+    acted = True
+    defs, max_def = 0, 8
+    to_spare = len([c for c in grid.cells.values() if c.owned])
+    
+    while player.can_act():
+        
+        if acted:
+            # only re-eval if an action occured
+            q = InterestQueue()
+            for cell in grid.influence_zone(ME):
+                if cell.movable and not cell in abandonned and not cell in played:
+                    if cell.owned and cell.threat > 0 and cell.pos != player.hq.pos:
+                        if defs > max_def:
+                            continue
+                        if grid.emergency:
+                            p = Defend(cell)
+                        else:
+                            p = Secure(cell)
+                    elif cell.opponents:
+                        p = Attack(cell)
+                    elif not cell.owned:
+                        p = Colonize(cell)
+                    else:
+                        continue
+                    p.eval()
+                    q.put(p)
+            
+        if not q:
+            break
+        
+        acted = False
+        
+        objective = q.get()
+            
+        if type(objective) is Secure:
+            
+            if (player.current_gold - to_spare) > Building.cost[Building.TOWER] \
+               and not objective.cell.mine_site \
+               and not objective.cell.building:
+                action = BuildTower(objective.cell)
+                defs += 1
+            else:
+                abandonned.append(objective.cell)
+                continue
+                
+        elif type(objective) is Defend:
+            
+            if player.current_gold > Building.cost[Building.TOWER] \
+               and not objective.cell.mine_site \
+               and not objective.cell.building:
+                action = BuildTower(objective.cell)
+                defs += 1
+                
+            elif objective.cell.unit and objective.cell.unit.owned:
+                # already a unit here: stay on place
+                objective.cell.unit.has_moved = True
+                continue
+                
+            elif player.can_afford(1):
+                action = Train(1, objective.cell)
+                defs += 1
+                
+            else:
+                near_unit = next((grid[n].unit for n in objective.cell.neighbors 
+                                  if grid[n].unit 
+                                  and grid[n].unit.owned 
+                                  and grid[n].unit.level == 1
+                                  and not grid[n].unit.has_moved), 
+                                 None)
+                if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
+                    action = Move(near_unit, objective.cell)
+                    defs += 1
+                else:
+                    abandonned.append(objective.cell)
+                    continue
+            
+        elif type(objective) in (Colonize, Attack):
+            
+            near_units = [grid[n].unit for n in objective.cell.neighbors if grid[n].unit \
+                                                                             and grid[n].unit.owned \
+                                                                             and not grid[n].unit.has_moved \
+                                                                             and grid[n].unit.level == objective.min_level]
+            
+            if len(near_units) == 1:
+                near_unit = near_units[0]
+            elif len(near_units) > 1:
+                near_unit = min(near_units, key=lambda u: len([u.pos == o.cell.pos for o in q.items]))
+            else:
+                near_unit = None
+                
+            if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
+                action = Move(near_unit, objective.cell)
+            else:
+                if objective.min_level > player.max_affordable() or player.gold < (to_spare + Unit.cost[objective.min_level]):
+                    abandonned.append(objective.cell)
+                    continue
+                
+                action = Train(objective.min_level, objective.cell)
+        
+        log(f"priority: {objective}")
+        
+        # a unit occupy the cell
+        already_there = action.cell.unit
+        if already_there and already_there.owned:
+            if already_there.has_moved:
+                log("cell is occupied: abandon")
+                abandonned.append(objective.cell)
+                continue
+            log(f"* needs to evacuate unit {already_there.id_} before")
+            evacuate = grid.get_next_move(already_there)
+            if evacuate:
+                evac_action = Move(already_there, evacuate.cell)
+                log(f"forced: {evac_action}")
+                grid.apply(evac_action)
+                todo.append(evac_action)
+            else:
+                log(f"<!> No move available for unit {already_there.id_}")
+                abandonned.append(objective.cell)
+                continue
+        
+        acted = True
+        log(f"> Apply action {action}")
+        played.append(action.cell)
+        grid.apply(action)
+        todo.append(action)
+        
+    # units which did not move
+    for unit in player.units:
+        if not unit.has_moved:
+            objective = grid.get_next_move(unit)
+            if objective:
+                action = Move(unit, objective.cell)
+                log(f"default: {action}")
+                grid.apply(action)
+                todo.append(action)
+            else:
+                log(f"<!> No move available for unit {unit.id_}")
+                unit.has_moved = True
+                
+    # can build mines?
+    if player.current_gold > grid.cost_for_new_mine():
+        site = grid.get_building_site(Building.MINE)
+        if site:
+            action = BuildMine(site.cell)
+            if action:
+                log(f"default: {action}")
+                grid.apply(action)
+                todo.append(action)
+        
+    log(f"* todo: {todo}")
+    
+    commands = [action.command() for action in todo]
+
+    if not commands:
+        log("nothing to do: wait")
+        commands = ["WAIT"]
+        
+    log(f"* commands: {commands}")
+    print(";".join(commands))

+ 171 - 0
i_n_f/temp.py

@@ -0,0 +1,171 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+from collections import Counter
+import itertools
+import time
+
+
+class Node():
+    def __init__(self, pos, path=[]):
+        self.pos = pos
+        self.path = path
+
+class Grid():
+    dim = 12
+    owned = 3
+    def __init__(self):
+        
+        self.cells = [(x, y) for x in range(Grid.dim) for y in range(Grid.dim)]
+
+    def print_grid(self):
+        grid = [["" for _ in range(Grid.dim)] for _ in range(Grid.dim)]
+        for x, y in self.cells:
+            grid[y][x] = f"({x}, {y})" if self._active_owned((x, y), 0) else "______"
+        return "\n".join(["".join([c for c in row]) for row in grid])
+    
+    @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 = [(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 < Grid.dim and 0 <= y < Grid.dim]
+        
+    def oriented_neighbors(self, x, y, orient):
+        return [(x + 1, y), (x, y + 1)] if orient > 0 else [(x - 1, y), (x, y - 1)]
+    
+    def update_frontlines(self):
+        self.frontline = []
+        
+        for p in self.cells:
+            if self._active_owned(p, 0):
+                if any(not self._active_owned(n, 0) for n in self.neighbors(*p)):
+#                     cell.update_threat()
+                    self.frontline.append(p)
+    
+    def _active_owned(self, pos, player_id):
+        return pos in [(0,0), (1,0), (2,0), (3,0), (4, 0),
+                       (0,1), (1,1), (2,1), 
+                       (0,2), (1,2), (2,2), 
+                       (0,3), (1,3), (2,3), 
+                                     (2,4), 
+                              (1,5), (2,5),
+                              (1,6), (2,6),
+                              (1,7), (2,7),
+                              (1,8), (2,8)]
+        
+    def __active_owned(self, pos, player_id):
+        return pos[0] < 12 and pos[1] < 12
+
+    def update_pivot_for(self, player_id):
+#         start = self.get_hq(player_id).pos
+        start = (0,0)
+        orient = 1 if start == (0, 0) else -1
+        start_node = Node(start)
+        
+        buffer = [start_node]
+        nodes = {start_node}
+        ignored = set()
+        
+        while buffer:
+            new_buffer = []
+            for node in buffer:
+                neighbors = [p for p in self.neighbors(*node.pos) if self._active_owned(p, player_id)]
+                if len(neighbors) == 4:
+                    ignored.add(node.pos)
+                    continue
+                for n in neighbors:
+                    if not n in node.path:
+                        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)
+        print(paths_to)
+    
+        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)
+        
+        occurrences = Counter(sum(sum(paths_to.values(), []), []))
+
+        while ignored:
+            new_ignored = set()
+            for p in ignored:
+                occured_neighbors = [occurrences[n] for n in self.neighbors(*p) if n in occurrences]
+                if not occured_neighbors:
+                    new_ignored.add[p]
+                    continue
+                occurrences[p] = 2 * sum(occured_neighbors) // len(occured_neighbors)
+            ignored = new_ignored
+        
+        print(occurrences)
+
+        return pivots
+
+
+    def __update_pivot_for(self, player_id):
+#         start = self.get_hq(player_id).pos
+        start = (0,0)
+        orient = 1
+        buffer = [(None, start)]
+        bounds = []
+        
+        while buffer:
+            new_buffer = []
+            for o, p in buffer:
+                for n in self.oriented_neighbors(*p, orient):
+                    if self._active_owned(n, player_id) and not (p, n) in bounds and n != o:
+                        bounds.append((p, n))
+                        new_buffer.append((p, n))
+            buffer = new_buffer
+        
+        print(bounds)
+        
+        parents_number = Counter()
+        for parent, child in bounds:
+            parents_number[child] += 1
+        print(parents_number)
+            
+
+        pivots = set()
+        for parent, child in bounds:
+            if parent == start:
+                continue
+            if parents_number[child] == 1:
+                pivots |= {parent}
+                
+        
+        return pivots
+    
+grid = Grid()
+Grid.owned = 5
+print(grid.print_grid())
+
+t0 = time.time()
+a = grid.update_pivot_for(0)
+print(a)
+print(time.time() - t0)
+

+ 417 - 0
labyrinth/script.py

@@ -0,0 +1,417 @@
+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}>"
+
+    def ancestors(self):
+        ancestors = []
+        p = self.parent
+        while p:
+            ancestors.insert(0, p)
+            p = p.parent
+        return ancestors
+
+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, "#") in passable for p in sight) and not any(w in sight for w in waypoints):
+                    neighbors[waypoint].append((other, Grid.dist(waypoint, 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 n, dist in neighbors[current]:
+                if n in current.ancestors():
+                    continue
+
+                cost = current.cost + dist
+                priority = cost + Grid.dist(n, target)
+                
+                node = PathNode(*n, 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 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
+            
+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)
+    

+ 341 - 0
labyrinth/test.py

@@ -0,0 +1,341 @@
+'''
+
+@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}>"
+
+    def ancestors(self):
+        ancestors = []
+        p = self.parent
+        while p:
+            ancestors.insert(0, p)
+            p = p.parent
+        return ancestors
+            
+            
+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 print_nodes(self, node):
+        
+        def _get_parent(node):
+            parents = []
+            if node.parent:
+                parents.insert(0, node.parent)
+                parents = _get_parent(node.parent) + parents
+            return parents
+        
+        path_to = _get_parent(node)
+        print(node, path_to)
+        
+    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(w in sight for w in waypoints):
+                    neighbors[waypoint].append((other, Grid.dist(waypoint, other)))
+        
+        while nodes:
+            current = nodes.get()
+            
+#             self.print_nodes(current)
+            
+            if current == target:
+                path = []
+                previous = current
+                while previous:
+                    if previous != start:
+                        path.insert(0, previous)
+                    previous = previous.parent
+                return path
+
+            for n, dist in neighbors[current]:
+                if n in current.ancestors():
+                    continue
+
+                cost = current.cost + dist
+#                 cost = current.cost + 1
+                priority = cost + Grid.dist(n, target)
+                
+                node = PathNode(*n, 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(grid.corners))
+
+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)
+

+ 741 - 0
last_crusade/script.py

@@ -0,0 +1,741 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+import time
+
+
+# TODO: what about the PIVOT_BACK in requirements?
+# TODO: do not use a rock for interception if it is behind indiana
+# 
+
+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"
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.trajectory = None
+        self.crash_on = None
+
+class RockThreat(Rock):
+    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
+        
+    def useless_pivot(self, pivot):
+        return self.pivoted[pivot] == self.type
+        
+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 Trajectory(list):
+    def __init__(self, *args):
+        super().__init__(args)
+        self.stops = []
+        self.interceptions = []
+
+    def elide(self, at):
+        new_t = Trajectory(*self[:at])
+        new_t.stops = self.stops
+        return new_t
+    
+    def __repr__(self):
+        return "<{},stops={},intercept:{}>".format(list(self), self.stops, self.interceptions)
+    
+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 = []
+        self.deadend = False
+    
+    def __lt__(self, other):
+        return self.pos < other.pos
+    
+    def __repr__(self):
+        return f"<{self.pos}, pivot:{self.pivot}, cost:{self.cost}, round:{self.round}, require:{self.require}, deadend:{self.deadend}>"
+
+class DeadEnd(Exception): 
+    pass
+class Collision(Exception): 
+    pass
+
+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 += reversed(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 collide(self, pos1, pos2, pivot=PIVOT_UNCHANGED):
+        if pos1.coords != pos2.coords:
+            return False
+        if pos1.coords == self.exit:
+            return False
+        if pos1 == pos2:
+            return True
+        piece = self.get_cell(pos1.coords)
+        if pivot:
+            piece = turn(piece, pivot)
+        if type(piece) in [Piece4, Piece5]:
+            return piece.apply(pos1.face) == pos2.face
+        if not piece.open_on(pos1.face) or not piece.open_on(pos2.face):
+            return False
+        return True
+    
+    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 interception_path(self, rock, target):
+        subject = rock
+        nodes = Queue()
+        origin = PathNode(subject.pos)
+        nodes.put(origin, 0)
+        
+        its, break_on = 0, 2000
+        
+        while nodes:
+            current = nodes.get()
+            
+            its += 1
+            if its > break_on:
+                log("interception pathfinding broken")
+                return None
+            
+            for at_ in range(len(target.trajectory)):
+                if self.collide(current.pos, target.trajectory[at_]):
+                    path = []
+                    previous = current.parent
+                    while previous:
+                        if previous != origin:
+                            path.insert(0, previous)
+                        previous = previous.parent
+                    return path, at_
+
+            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)
+            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
+                    
+                    priority = node.cost
+                    nodes.put(node, priority)
+
+        return []
+    
+    
+    def update_rocks_moves(self):
+        
+        for rock in self.rocks:
+            trajectory = Trajectory(*self.trajectory(rock))
+            if trajectory:
+                rock.crash_on = self.after(trajectory[-1])
+            
+            for i, pos in enumerate(trajectory):
+                piece = self.cells[pos.coords]
+                if piece.locked or i == 0:
+                    continue
+                
+                # stops
+                for pivot in [PIVOT_LEFT, PIVOT_RIGHT, PIVOT_BACK]:
+                    if piece.useless_pivot(pivot):
+                        continue
+                    pos_after = self.after(pos, pivot)
+                    if not pos_after or not self.get_cell(pos_after.coords).open_on(pos_after.face):
+                        stop = (i, pivot)
+                        trajectory.stops.append(stop)
+                        break
+            
+            rock.trajectory = trajectory
+        
+        self.apply_collisions()
+        
+        for rock in self.rocks:
+            # find an interception possibility
+            for other in self.rocks:
+                if rock is other:
+                    continue
+                res = self.interception_path(other, rock)
+                if res:
+                    path, at_ = res
+                    if any(self.collide(self.indi.pos, n.pos) for n in path):
+                        continue
+                    rock.trajectory.interceptions += [(n.pos, n.pivot, at_) for n in path if n.pivot]
+        
+#         log([rock.trajectory for rock in self.rocks])
+        log([rock.crash_on for rock in self.rocks])
+          
+    def apply_collisions(self):
+        
+        real_rocks = [rock for rock in self.rocks if type(rock) is Rock]
+        if not real_rocks:
+            return
+        
+        for turn in range(max([len(rock.trajectory) for rock in real_rocks])):
+            
+            for i in range(len(real_rocks)):
+                for j in range(i+1, len(real_rocks)):
+                    try:
+                        if self.collide(real_rocks[i].trajectory[turn], real_rocks[j].trajectory[turn]):
+                            log("Two rocks collide at ", real_rocks[i].trajectory[turn])
+                            self.rocks[i].trajectory = self.rocks[i].trajectory.elide(turn + 1)
+                            self.rocks[j].trajectory = self.rocks[j].trajectory.elide(turn + 1)
+                    except IndexError:
+                        pass
+        
+    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 = []
+        current = 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 if with_start else path[1:]
+    
+    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 = set()
+            
+            deadend = False
+            try:
+                for rock in self.rocks:
+                    
+                    for round_ in (current.round +1, current.round):
+                        if len(rock.trajectory) > round_ and self.collide(rock.trajectory[round_], next_pos):
+                            alternatives = [Requirement(rock.trajectory[i], pivot, i) for i, pivot in rock.trajectory.stops if 0 < i <= round_]
+        
+                            if alternatives: 
+                                require = alternatives[-1]
+                                requirements.add(require)
+                                break
+                            
+                            else:
+                                interceptions = [Requirement(pos, pivot, i) for pos, pivot, i in rock.trajectory.interceptions if 0 < i <= round_]
+                                if interceptions:
+                                    requirements |= set(interceptions)
+                            
+                                elif type(rock) == Rock: #unstoppable rock
+                                    log("node is deadend: ", next_pos.coords)
+                                    raise DeadEnd
+                        
+            except DeadEnd:
+                deadend = True
+        
+            for pivot, rotations in PIVOTS.items():
+                if rotations > 0:
+                    if next_cell.locked:
+                        continue
+                    elif next_cell.useless_pivot(pivot):
+                        # useless rotation
+                        continue
+                    
+                if current is origin and rotations > 1:
+                    continue
+                
+                collision = False
+                for rock in self.rocks:
+                    if not type(rock) is Rock:
+                        continue
+                    if rock.crash_on and current.round + 1 == len(rock.trajectory) and self.collide(rock.crash_on, next_pos, pivot):
+                        log(f"pivoting from {pivot} would lead to a collision at {next_pos}")
+                        collision = True
+                        break
+                
+                pivoted = turn(next_cell, pivot)
+                if pivoted.open_on(next_pos.face):
+                    node = PathNode(next_pos, current, pivot)
+                    node.cost = current.cost + rotations + len(requirements) + 3 * deadend + 5 * collision
+                    node.round = current.round + 1
+                    node.require = requirements
+                    node.deadend = deadend
+                    
+                    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):
+        self.coords = coords[:2]
+        self.pivot = pivot
+
+    def command(self):
+        return "{} {} {}".format(*self.coords, COMMAND.get(self.pivot, "Invalid"))
+
+    def __repr__(self):
+        return f"<{self.command()}>"
+
+    def __eq__(self, other):
+        return self.coords == other.coords and self.pivot == other.pivot
+        
+    def __hash__(self):
+        return 0
+
+class Requirement(Action):
+    def __init__(self, coords, pivot, last_chance_at):
+        super().__init__(coords, pivot)
+        self.last_chance_at = last_chance_at
+
+    def command(self):
+        return "{} {} {}".format(*self.coords, COMMAND.get(self.pivot, "Invalid"))
+
+    def __repr__(self):
+        return f"<REQ {self.command()} before {self.last_chance_at}>"
+
+# 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 = [None for _ in range(len(path))]
+        
+        # basic plan
+        for i, node in enumerate(path):
+            if node.pivot in [PIVOT_LEFT, PIVOT_RIGHT]:
+                plan[i] = Action(node.pos, node.pivot)
+            elif node.pivot == PIVOT_BACK:
+                plan[i] = Action(node.pos, PIVOT_RIGHT)
+
+        log("first plan:", plan)
+
+        # add requirements
+        requirements = set()
+        for i, node in enumerate(path):
+            if node.pivot == PIVOT_BACK:
+                action = Requirement(node.pos, PIVOT_RIGHT, i)
+                requirements.add(action)
+            for action in node.require:
+                requirements.add(action)
+        
+        log(requirements)
+        
+        for action in sorted(requirements, key=lambda x: x.last_chance_at):
+            for j in range(action.last_chance_at - 1, -1, -1):
+                if j < len(plan) and plan[j] is None:
+                    plan[j] = action
+                    break
+            else:
+                log("! action required could not be inserted: ", action)
+        
+        log("completed:", plan)
+    else:
+        log("no path, use previous plan")
+
+    try:
+        action = plan.pop(0)
+        if not next((type(a) is Requirement for a in plan if not a is None), False):
+            while plan and not action:
+                action = plan.pop(0)
+            
+        if action:
+            grid.apply(action)
+            print(action.command())
+        else:
+            print("WAIT")
+    except IndexError:
+        print("WAIT")

+ 63 - 0
max_rect.py

@@ -0,0 +1,63 @@
+'''
+> https://www.codingame.com/ide/1850615487c96262f26bd2d508cb46b18cc23557
+@author: olivier.massot, 2019
+'''
+import sys
+
+# w, h = [int(i) for i in input().split()]
+# rows = [[int(x) for x in  input().split()] for _ in range(h)]
+
+w,h=2,2
+
+rows = [[99,0],
+        [0,-1],
+        [0,2],
+        ]
+
+def max_subarray_1d(row):
+    max_so_far, max_ending_here = 0, row[0]
+    for v in row[1:]:
+        max_ending_here = max(v, max_ending_here + v)
+        max_so_far = max(max_so_far, max_ending_here)
+    return max_so_far
+
+def max_subarray_2d(rows):
+    w, h = len(rows[0]), len(rows)
+
+    # Modify the array's elements to now hold the sum  
+    # of all the numbers that are above that element in its column
+    summed=[[*row] for row in rows]
+    for y in range(1, h):
+        for x in range(w):
+            summed[y][x] += summed[y-1][x]
+
+    ans = 0
+    for y0 in range(h-1):
+        for y1 in range(y0+1, h):
+            sums = [summed[y1][x] - summed[y0][x] for x in range(w)]
+            ans = max(ans, max_subarray_1d(sums))
+            
+    for y in range(h):
+        ans = max(ans, max_subarray_1d(summed[y]))
+        ans = max(ans, max_subarray_1d(rows[y]))
+    ans=max(ans, sum((sum(row) for row in rows)))
+    return ans
+
+if all(v >= 0 for row in rows for v in row):
+    print("special: all positives", file=sys.stderr)
+    print(sum((sum(row) for row in rows)))
+elif all(v <= 0 for row in rows for v in row):
+    print("special: all negatives", file=sys.stderr)
+    print(max(max(row) for row in rows))
+elif len(rows[0]) == 1:
+    print("special: one column", file=sys.stderr)
+    maxsub = max_subarray_1d([row[0] for row in rows])
+    print(maxsub)
+elif len(rows) == 1:
+    print("special: one row", file=sys.stderr)
+    maxsub = max_subarray_1d(rows[0])
+    print(maxsub)
+     
+else:
+    maxsub = max_subarray_2d(rows)
+    print(maxsub)

+ 53 - 0
max_rect2.py

@@ -0,0 +1,53 @@
+'''
+> https://www.codingame.com/ide/1850615487c96262f26bd2d508cb46b18cc23557
+@author: olivier.massot, 2019
+'''
+import sys
+from copy import deepcopy
+
+# w, h = [int(i) for i in input().split()]
+# rows = [[int(x) for x in  input().split()] for _ in range(h)]
+
+w,h=2,2
+
+rows = [[-1,99],
+        [0,2]
+        ]
+
+def findMaxSubmatrix(matrix, height, width):
+    nrows = len(matrix)
+    ncols = len(matrix[0])           
+    cumulative_sum = deepcopy(matrix)
+
+    for r in range(nrows):
+        for c in range(ncols):
+            if r == 0 and c == 0:
+                cumulative_sum[r][c] = matrix[r][c]
+            elif r == 0:
+                cumulative_sum[r][c] = cumulative_sum[r][c-1] + matrix[r][c]
+            elif c == 0:
+                cumulative_sum[r][c] = cumulative_sum[r-1][c] + matrix[r][c]
+            else:
+                cumulative_sum[r][c] = cumulative_sum[r-1][c] + cumulative_sum[r][c-1] - cumulative_sum[r-1][c-1] + matrix[r][c]
+
+    best = 0
+
+    for r1 in range(nrows):
+        for c1 in range(ncols):
+            r2 = r1 + height - 1
+            c2 = c1 + width - 1
+            if r2 >= nrows or c2 >= ncols:
+                continue
+            if r1 == 0 and c1 == 0:
+                sub_sum = cumulative_sum[r2][c2]
+            elif r1 == 0:
+                sub_sum = cumulative_sum[r2][c2] - cumulative_sum[r2][c1-1]
+            elif c1 == 0:
+                sub_sum = cumulative_sum[r2][c2] - cumulative_sum[r1-1][c2]
+            else:
+                sub_sum = cumulative_sum[r2][c2] - cumulative_sum[r1-1][c2] - cumulative_sum[r2][c1-1] + cumulative_sum[r1-1][c1-1]
+            if best < sub_sum:
+                best = sub_sum
+    return best
+
+print(findMaxSubmatrix(rows,h,w))

+ 90 - 0
neighbors_sum_grids.py

@@ -0,0 +1,90 @@
+'''
+@author: olivier.massot, 2019
+'''
+import itertools
+import sys
+import time
+
+def log(*msg):
+    print(*msg, file=sys.stderr)
+t0 = time.time()
+
+# matrix = [[int(j) for j in input().split()] for _ in range(5)]
+matrix = [[21, 14, 10, 0, 0], [0, 4, 1, 6, 0], [0, 3, 5, 11, 13], [15, 0, 0, 0, 0], [23, 24, 0, 20, 25]]
+# matrix = [[19, 15, 11, 0, 23], [0, 0, 0, 0, 13], [0, 0, 1, 0, 22], [0, 0, 0, 0, 16], [14, 12, 21, 0, 25]]
+# matrix = [[23, 22, 8, 0, 18], [0, 0, 0, 5, 0], [0, 0, 12, 0, 0], [0, 10, 0, 11, 9], [0, 0, 0, 0, 0]]
+# matrix = [[0, 12, 0, 0, 0], [0, 0, 17, 0, 7], [0, 4, 3, 0, 18], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
+log(matrix)
+
+grid = {(x, y): v for y, row in enumerate(matrix) for x, v in enumerate(row)}
+
+crd = [(x, y) for x in range(5) for y in range(5)]
+ocrd = sorted(crd, key=lambda c: c[0] + c[1])
+
+def get(grid, x, y):
+    if x < 0 or y < 0:
+        raise IndexError
+    return grid[y][x]
+
+def get_neighbors(x, y, d=5):
+    return [(x+dx, y+dy) for dx in range(-1, 2) for dy in range(-1,2) \
+            if (dx or dy) and 0 <= x + dx < d and 0 <= y + dy < d ]
+
+neighbors = {c: get_neighbors(*c) for c in crd}
+
+log("\n".join([" ".join([str(j).rjust(2) for j in row]) for row in matrix]))
+
+def validate(grid, unused, start):
+    x0, y0 = start
+    scan = [(x, y) for x in range(x0 - 1, x0 + 2) for y in range(y0 - 1, y0 + 2) if (x, y) in grid]
+
+    for c in scan:
+        v = grid[c]
+            
+        if v <= 2:
+            continue
+        
+        neighs = [grid.get(n) for n in get_neighbors(*c, d=len(grid))]
+        neighs = [x for x in neighs if x is not None]
+        if 0 in neighs:
+            return True
+        
+        if not any(n1 + n2 == v for n1, n2 in itertools.combinations(neighs, 2)):
+            return False
+        
+    return True
+        
+def test_candidates(grid):
+    
+    try:
+        x, y = next(c for c, v in grid.items() if v == 0)
+    except StopIteration:
+        return grid
+    
+    unused = [j for j in range(25,0,-1) if not j in grid.values()]
+    
+    for j in unused:
+        new_grid = dict(grid)
+        new_grid[(x, y)] = j
+        
+        valid = validate(new_grid, unused, (x, y))
+        
+        if valid:
+            r = test_candidates(new_grid)
+            if r:
+                return r
+
+dim = 5
+
+new_grid = {c: v for c, v in grid.items() if c[0] in range(dim) and c[1] in range(dim)}
+print(new_grid)
+r = test_candidates(new_grid)
+print(r)
+
+m = [[r[(x, y)] for x in range(dim)] for y in range(dim)]
+log("\n".join([" ".join([str(j).rjust(2) for j in row]) for row in m]))
+
+log(time.time() - t0)
+for row in m:
+    print(" ".join(map(str, row)))
+

+ 219 - 0
optimization/cig.py

@@ -0,0 +1,219 @@
+import heapq
+import sys
+
+
+def log(*msg):
+    print(*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}>"
+
+h, w, n = [int(input()) for _ in range(3)]
+log("INIT: ", h, w, n)
+
+LEFT = "E"
+RIGHT = "A"
+DOWN = "D"
+UP = "C"
+STAY = "B"
+ACTIONS = [LEFT, RIGHT, DOWN, UP, STAY]
+MOVES = [UP, RIGHT, DOWN, LEFT] # WARNING: order matters here
+
+class Grid():
+    UNKNOWN = 0
+    WALL = 1
+    GROUND = 2
+    
+    VECT = {UP: (0,-1), RIGHT: (1,0), DOWN: (0,1), LEFT: (-1,0)}
+    INV = {UP: DOWN, RIGHT: LEFT, LEFT: RIGHT, DOWN: UP, STAY: STAY}
+    
+    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.player = []
+        self.ghosts = []
+        self.deadends = []
+        
+    def update(self, player, ghosts, available_moves):
+        self.player = player
+        self.ghosts = ghosts
+        self.available_moves = available_moves
+        
+        for p in [self.player] + self.ghosts:
+            self.cells[p] = Grid.GROUND
+            
+        for m in MOVES:
+            p = grid.pos_after(self.player, m)
+            self.cells[p] = Grid.GROUND if m in available_moves else Grid.WALL
+
+        self.propagate()
+
+
+    def _repr_cell(self, p):
+        if p == self.player:
+            return "P"
+        elif p in self.ghosts:
+            return "G"
+        elif self.cells[p] == Grid.WALL:
+            return "x"
+        elif self.cells[p] == Grid.GROUND:
+            return "_"
+        else:
+            return "."
+        
+    def graph(self):
+        return "\n".join(["".join([self._repr_cell((x, y)) for x in range(self.w)]) for y in range(self.h) if abs(y - self.player[1]) < 15])
+
+    def set_visited(self, p):
+        self.cells[p] = Grid.GROUND
+        
+    def set_wall(self, p):
+        self.cells[p] = Grid.WALL
+
+    @staticmethod
+    def pos_after(current, move):
+        x, y = current
+        dx, dy = Grid.VECT[move]
+        return x + dx, y + dy
+
+    @staticmethod
+    def dist(p1, p2):
+        xa, ya = p1
+        xb, yb = p2
+        return abs(xa - xb) + abs(ya - yb) 
+
+    def 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 path(self, start, target):
+        nodes = Queue()
+        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:
+                if (x, y) == current.parent:
+                    continue
+                
+                if self.cells[(x, y)] == Grid.WALL:
+                    continue
+
+                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 propagate(self):
+        self.propagation = {}
+        for m in self.available_moves:
+            self.propagation[m] = 0
+            visited= set()
+            start = self.pos_after(self.player, m)
+            
+            buffer = [start]
+            visited |= set(buffer)
+            
+            while buffer:
+                new_buffer = []
+                if any(p in self.ghosts for p in buffer):
+                    break
+                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.player and \
+                           not self.cells[n] == Grid.WALL:
+                            new_buffer.append(n)
+                            visited.add(n)
+                            self.propagation[m] += 2 if self.cells[n] == Grid.UNKNOWN else 1
+                buffer = new_buffer
+            
+grid = Grid(w, h)
+turn = 0
+previous = STAY
+
+while True:
+    turn += 1
+    available_moves = [m for m in MOVES if input() == "_"]
+    log(available_moves)
+    
+    l = [tuple([int(j) for j in input().split()]) for _ in range(n)]
+    *ghosts, player= l
+    grid.update(player, ghosts, available_moves)
+    log(player, ghosts)
+    
+    log(grid.graph())
+    log(grid.propagation)
+    
+    q = Queue()
+    for m in available_moves:
+        visited = 20*(m == grid.INV[previous])
+        
+        danger = sum([grid.dist(grid.pos_after(player, m), e) for e in ghosts])
+        danger = 100 * danger // (len(ghosts) * (w + h))
+        
+        perspective = grid.propagation[m]
+        perspective = 100 * perspective // (max(grid.propagation.values()) or 1)
+        
+        interest = visited - danger - perspective
+        
+        log(m, visited, danger, perspective, interest)
+        q.fput(m, interest)
+        
+    move = q.get()
+    previous = move
+    print(move)
+

+ 364 - 0
optimization/test.txt

@@ -0,0 +1,364 @@
+35 28 5
+
+# _ # _
+[[11, 15], [16, 15], [11, 17], [16, 17], [13, 25]]
+----
+
+
+# _ # _
+[[12, 15], [15, 15], [11, 17], [16, 17], [14, 25]]
+----
+
+
+_ _ # _
+[[13, 15], [14, 15], [11, 17], [16, 17], [15, 25]]
+----
+
+# _ # _
+[[14, 15], [14, 14], [11, 17], [16, 17], [16, 25]]
+----
+
+# _ # _
+[[14, 14], [14, 13], [11, 17], [16, 17], [17, 25]]
+----
+
+# _ _ _
+[[14, 13], [13, 13], [11, 17], [16, 17], [18, 25]]
+----
+
+# _ # _
+[[15, 13], [12, 13], [11, 17], [16, 17], [19, 25]]
+----
+
+# _ # _
+[[15, 12], [12, 12], [11, 17], [16, 17], [20, 25]]
+----
+
+_ # _ _
+[[15, 11], [12, 11], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 10], [12, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[16, 10], [11, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[17, 10], [10, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 10], [9, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 9], [9, 9], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 8], [9, 8], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 7], [9, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[19, 7], [8, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[20, 7], [7, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 7], [6, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 6], [6, 6], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 5], [6, 5], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 4], [6, 4], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 3], [6, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[20, 3], [7, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[19, 3], [8, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 3], [9, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[17, 3], [10, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[16, 3], [11, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 3], [12, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 4], [12, 4], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 5], [12, 5], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 6], [12, 6], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 7], [12, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[16, 7], [11, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[17, 7], [10, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 7], [9, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[19, 7], [8, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[20, 7], [7, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 7], [6, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 6], [6, 6], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 5], [6, 5], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 4], [6, 4], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 3], [6, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[22, 3], [5, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[23, 3], [4, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 3], [3, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[25, 3], [2, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 3], [1, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 4], [1, 4], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 5], [1, 5], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 6], [1, 6], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 7], [1, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 8], [2, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 9], [3, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 10], [4, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[25, 10], [5, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 10], [6, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[23, 10], [6, 8], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[22, 10], [6, 9], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 10], [6, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 11], [6, 11], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 12], [6, 12], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 13], [6, 13], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 14], [6, 14], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 15], [6, 15], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 16], [6, 16], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 17], [6, 17], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 18], [6, 18], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 19], [6, 19], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 20], [6, 20], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 21], [6, 21], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 22], [6, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[22, 22], [5, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[23, 22], [4, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 22], [3, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[25, 22], [2, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 22], [1, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 23], [1, 23], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 24], [1, 24], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 25], [1, 25], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[25, 25], [2, 25], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 25], [3, 25], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 26], [3, 26], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 27], [3, 27], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 28], [3, 28], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[23, 28], [4, 28], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[22, 28], [5, 28], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 28], [6, 28], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 27], [6, 27], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 26], [6, 26], [11, 17], [16, 17], [21, 25]]
+
+Sortie standard :

+ 372 - 0
optimization/test_A.txt

@@ -0,0 +1,372 @@
+35 28 5
+
+# _ # _
+[[11, 15]
+[16, 15]
+[11, 17]
+[16, 17]
+[13, 25]]
+----
+
+
+# _ # _
+[[12, 15]
+[15, 15]
+[11, 17]
+[16, 17]
+[14, 25]]
+----
+
+
+_ _ # _
+[[13, 15], [14, 15], [11, 17], [16, 17], [15, 25]]
+----
+
+# _ # _
+[[14, 15], [14, 14], [11, 17], [16, 17], [16, 25]]
+----
+
+# _ # _
+[[14, 14], [14, 13], [11, 17], [16, 17], [17, 25]]
+----
+
+# _ _ _
+[[14, 13], [13, 13], [11, 17], [16, 17], [18, 25]]
+----
+
+# _ # _
+[[15, 13], [12, 13], [11, 17], [16, 17], [19, 25]]
+----
+
+# _ # _
+[[15, 12], [12, 12], [11, 17], [16, 17], [20, 25]]
+----
+
+_ # _ _
+[[15, 11], [12, 11], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 10], [12, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[16, 10], [11, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[17, 10], [10, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 10], [9, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 9], [9, 9], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 8], [9, 8], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 7], [9, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[19, 7], [8, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[20, 7], [7, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 7], [6, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 6], [6, 6], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 5], [6, 5], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 4], [6, 4], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 3], [6, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[20, 3], [7, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[19, 3], [8, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 3], [9, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[17, 3], [10, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[16, 3], [11, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 3], [12, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 4], [12, 4], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 5], [12, 5], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 6], [12, 6], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[15, 7], [12, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[16, 7], [11, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[17, 7], [10, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[18, 7], [9, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[19, 7], [8, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[20, 7], [7, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 7], [6, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 6], [6, 6], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 5], [6, 5], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 4], [6, 4], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 3], [6, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[22, 3], [5, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[23, 3], [4, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 3], [3, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[25, 3], [2, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 3], [1, 3], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 4], [1, 4], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 5], [1, 5], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 6], [1, 6], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 7], [1, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 8], [2, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 9], [3, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 10], [4, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[25, 10], [5, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 10], [6, 7], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[23, 10], [6, 8], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[22, 10], [6, 9], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 10], [6, 10], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 11], [6, 11], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 12], [6, 12], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 13], [6, 13], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 14], [6, 14], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 15], [6, 15], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 16], [6, 16], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 17], [6, 17], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 18], [6, 18], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 19], [6, 19], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 20], [6, 20], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 21], [6, 21], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 22], [6, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[22, 22], [5, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[23, 22], [4, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 22], [3, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[25, 22], [2, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 22], [1, 22], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 23], [1, 23], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 24], [1, 24], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[26, 25], [1, 25], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[25, 25], [2, 25], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 25], [3, 25], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 26], [3, 26], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 27], [3, 27], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[24, 28], [3, 28], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[23, 28], [4, 28], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[22, 28], [5, 28], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 28], [6, 28], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 27], [6, 27], [11, 17], [16, 17], [21, 25]]
+----
+
+_ # _ _
+[[21, 26], [6, 26], [11, 17], [16, 17], [21, 25]]
+
+Sortie standard :

+ 22 - 0
optimization/test_B.txt

@@ -0,0 +1,22 @@
+35 28 5
+# _ # _
+[[11, 15], [16, 15], [11, 17], [16, 17], [13, 25]]
+
+0197# _ # _
+[[12, 15], [15, 15], [11, 17], [16, 17], [13, 25]]
+
+0297# _ # _
+[[13, 15], [14, 15], [11, 17], [16, 17], [13, 25]]
+
+0397# _ # _
+[[14, 15], [14, 14], [11, 17], [16, 17], [13, 25]]
+
+0497# _ # _
+[[14, 14], [14, 13], [11, 17], [16, 17], [13, 25]]
+
+0597# _ # _
+[[14, 13], [13, 13], [11, 17], [16, 17], [13, 25]]
+
+0697# _ # _
+[[15, 13], [12, 13], [11, 17], [16, 17], [13, 25]]
+

+ 121 - 0
optimization/x.txt

@@ -0,0 +1,121 @@
+
+# _ # _
+
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...........A....B..................
+...................................
+...........C....D..................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+.............E.....................
+...................................
+...................................
+
+# _ # _
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+............A..B...................
+...................................
+...........C....D..................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+..............E....................
+...................................
+...................................
+
+# _ # _
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+.............AB....................
+...................................
+...........C....D..................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...............E...................
+...................................
+...................................
+
+# _ # _
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+..............B....................
+..............A....................
+...................................
+...........C....D..................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+...................................
+................E..................
+...................................
+...................................

+ 16 - 0
organic_compound.py

@@ -0,0 +1,16 @@
+'''
+> https://www.codingame.com/ide/puzzle/organic-compounds
+@author: olivier.massot, 2019
+'''
+import re
+
+compounds = [re.findall("\(\d\)|CH\d|0", input().replace("   ", "0")) for i in range(int(input()))]
+values = [[int(re.search(r"\d", c)[0]) for c in row] for row in compounds]
+
+def getval(x, y):
+    return values[y][x] if 0 <= y < len(values) and 0 <= x < len(values[y]) else 0
+    
+valid = all((sum([getval(x, y), getval(x - 1, y), getval(x + 1, y), getval(x, y - 1), getval(x, y + 1)]) == 4 \
+             for y, row in enumerate(compounds) for x, v in enumerate(row) if v[0] == 'C'))
+    
+print("VALID" if valid else "INVALID")

+ 104 - 0
puzzle_rook.py

@@ -0,0 +1,104 @@
+'''
+    https://www.codingame.com/ide/18267536aa85810c92273263bca743a4f379f708
+@author: olinox14, 2019
+'''
+import sys
+
+def log(*msg):
+    print(*msg, file=sys.stderr)
+    
+rows = [list(input()) for _ in range(int(input()))]
+cols = [[r[i] for r in rows] for i in range(len(rows))]
+
+# for y, r in enumerate(rows):
+#     log("".join(r))
+
+class XInterval():
+    def __init__(self, y, xstart, xend=-1):
+        self.y = y
+        self.xstart = xstart
+        self.xend = xend
+        self.cells = []
+        
+    def __repr__(self):
+        return f"<XI({self.xstart}, {self.y})-({self.xend}, {self.y})>"
+    
+    def __len__(self):
+        return self.xend - self.xstart
+    
+    def update(self):
+        self.cells = [(x, self.y) for x in range(self.xstart, self.xend)]
+    
+class YInterval():
+    def __init__(self, x, ystart, yend=-1):
+        self.x = x
+        self.ystart = ystart
+        self.yend = yend
+        self.cells = []
+        
+    def __repr__(self):
+        return f"<YI({self.x}, {self.ystart})-({self.x}, {self.yend})>"
+    
+    def __len__(self):
+        return self.yend - self.ystart
+    
+    def update(self):
+        self.cells = [(self.x, y) for y in range(self.ystart, self.yend)]
+
+xintervals = []
+for y, row in enumerate(rows):
+    current = XInterval(y, 0)
+    
+    for x, c in enumerate(row):
+        if c == "X":
+            if x > current.xstart:
+                current.xend = x
+                current.update()
+                xintervals.append(current)
+            current = XInterval(y, x + 1)
+    if current.xstart < len(row):
+        current.xend = len(row)
+        current.update()
+        xintervals.append(current)
+
+yintervals = []
+for x, col in enumerate(cols):
+    current = YInterval(x, 0)
+    
+    for y, c in enumerate(col):
+        if c == "X":
+            if y > current.ystart:
+                current.yend = y
+                current.update()
+                yintervals.append(current)
+            current = YInterval(x, y + 1)
+    if current.ystart < len(col):
+        current.yend = len(col)
+        current.update()
+        yintervals.append(current)
+
+graph = [[any(c in yi.cells for c in xi.cells) for xi in xintervals] for yi in yintervals]
+
+def bpm(graph, u, matchR, seen): 
+
+    for v in range(len(xintervals)): 
+        if graph[u][v] and seen[v] == False: 
+            seen[v] = True 
+            if matchR[v] == -1 or bpm(graph, matchR[v], matchR, seen): 
+                matchR[v] = u 
+                return True
+    return False
+    
+def count_rooks(graph):
+    matchR = [-1 for _ in xintervals]
+    result = 0 
+    for i in range(len(yintervals)): 
+        seen = [False for _ in xintervals]
+        if bpm(graph, i, matchR, seen): 
+            result += 1
+            
+    return result 
+
+print(count_rooks(graph))
+
+

+ 69 - 0
quadrilaterals.py

@@ -0,0 +1,69 @@
+'''
+> https://www.codingame.com/ide/puzzle/nature-of-quadrilaterals
+@author: olivier.massot, 2019
+'''
+import math
+import sys
+
+
+class Vertex():
+    def __init__(self, name_, x, y):
+        self.name = name_
+        self.x = int(x)
+        self.y = int(y)
+        self.pos = (self.x, self.y)
+    
+    def __repr__(self):
+        return f"{self.name}{self.pos}"
+
+class Side():
+    def __init__(self, v1, v2):
+        self.v1 = v1
+        self.v2 = v2
+        self.sqlength = (v2.x - v1.x)**2 + (v2.y - v1.y)**2
+        
+        self.a = (v2.x - v1.x) / (v2.y - v1.y) if v2.y != v1.y else (v2.x - v1.x) * math.inf
+        self.b = -1 * self.a * v2.x + v2.y
+
+    def ortho_to(self, s):
+        if abs(self.a) == math.inf and s.a == 0:
+            return True
+        elif self.a == 0 and abs(s.a) == math.inf: 
+            return True
+        elif self.a * s.a == -1:
+            return True
+        return False
+    
+class Quadri():
+    def __init__(self, *vertices):
+        self.vertices = [Vertex(*v) for v in vertices]
+        self.name = "".join([v.name for v in self.vertices])
+        self.sides = [Side(v1, v2) for v1, v2 in list(zip(self.vertices, self.vertices[1:])) + [(self.vertices[-1], self.vertices[0])]]
+    
+    def __repr__(self):
+        return f"<Q:{self.vertices}>"
+
+    def nature(self):
+        opposites_parallels = (abs(self.sides[0].a) == abs(self.sides[2].a) and \
+                               (abs(self.sides[1].a) == abs(self.sides[3].a)))
+        same_length_sides = all([s.sqlength == self.sides[0].sqlength for s in self.sides[1:]])
+        right_angles = self.sides[0].ortho_to(self.sides[1]) and self.sides[1].ortho_to(self.sides[2]) and self.sides[2].ortho_to(self.sides[3])
+        
+        if right_angles and same_length_sides:
+            return "square"
+        elif right_angles:
+            return "rectangle"
+        elif same_length_sides:
+            return "rhombus"
+        elif opposites_parallels:
+            return "parallelogram"
+        else:
+            return "quadrilateral"
+        
+input_ = [input().split() for _ in range(int(input()))]
+quadris = [Quadri(*zip(l[::3], l[1::3], l[2::3])) for l in input_]
+
+print(quadris, file=sys.stderr)
+    
+for q in quadris:
+    print(f"{q.name} is a {q.nature()}.")

+ 24 - 0
robbery_optim.py

@@ -0,0 +1,24 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import sys
+
+# houses = [int(input()) for _ in range(int(input()))]
+houses = [1, 15, 10, 13, 16]
+
+print(houses, file=sys.stderr)
+        
+to_rob = []
+
+def max_independent_weight(weights):
+    
+    def with_and_without(i):
+        if i < 0 or i >= len(weights):
+            return 0, 0
+        right = with_and_without(i + 1)
+        return weights[i] + right[1], max(right)
+               
+    return max(with_and_without(0))
+
+print(max_independent_weight(houses))

+ 46 - 0
succession.py

@@ -0,0 +1,46 @@
+'''
+  > https://www.codingame.com/ide/puzzle/order-of-succession
+  @author: olivier.massot, 2019
+'''
+
+class Person():
+    def __init__(self, name, parent, birth, death, religion, gender):
+        self.name = name
+        self.parent_name = parent
+        self.birth = int(birth)
+        self.death = death
+        self.religion = religion
+        self.gender = gender
+        
+        self.alive = (self.death == "-")
+        self.catholic = (self.religion == "Catholic")
+        self.male = -1 if self.gender == "M" else 0
+        self.parent = None
+        self.children = []
+        
+    def __repr__(self):
+        return self.name
+        
+family = {p.name: p for p in [Person(*input().split()) for _ in range(int(input()))]}
+first_ancestor = None
+
+for p in family.values():
+    p.parent = family.get(p.parent_name, None)
+    if not p.parent:
+        first_ancestor = p
+    p.children = sorted([c for c in family.values() if c.parent_name == p.name], key=lambda x: (x.male, x.birth))
+
+def walk(root):
+    succession = []
+    def add(root):
+        if root.alive and not root.catholic:
+            succession.append(root)
+        for child in root.children:
+            add(child)
+    add(root)
+    return succession
+    
+succession = walk(first_ancestor)
+for p in succession:
+    print(p.name)
+

+ 20 - 0
ternary.py

@@ -0,0 +1,20 @@
+n = int(input())
+s = n // abs(n) if n else 1
+n = abs(n)
+ks = []
+mod = 0
+
+while n:
+    q, r = n // 3, n % 3
+    if (r + mod) > 1:
+        ks.insert(0, s*(r + mod - 3))
+        mod = 1
+    else:
+        ks.insert(0, s*(r + mod))
+        mod = 0
+    n = q
+if mod:
+    ks.insert(0, s)
+if not ks:
+    ks = [0]
+print("".join(["T", "0", "1"][i + 1] for i in ks))

+ 22 - 0
travelling_saleman.py

@@ -0,0 +1,22 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import math
+
+def distance(c0, c1):
+    return math.sqrt((c0[0] - c1[0])**2 + (c0[1] - c1[1])**2)
+
+cities = [tuple([int(j) for j in input().split()]) for _ in range(int(input()))]
+
+dist, start = 0, cities.pop(0)
+
+current = start
+while cities:
+    cities.sort(key=lambda c: distance(c, current))
+    next_step = cities.pop(0)
+    dist += distance(next_step, current)
+    current = next_step
+
+dist += distance(next_step, start)
+print(round(dist))

+ 16 - 0
triforce.py

@@ -0,0 +1,16 @@
+'''
+> https://www.codingame.com/ide/puzzle/may-the-triforce-be-with-you
+@author: olivier.massot, 2019
+'''
+n = int(input())
+
+lines = '\n'.join([' ' * (2 * n - i - 1) + \
+         '*' * ((1 + 2 * i) % (2*n)) + \
+         ' ' * ((1 + (2 * (2 * n - (i + 1)))) if i >= n else 0)  + \
+         '*' * (((1 + 2 * i) % (2*n)) if i >= n else 0) \
+         for i in range(2 * n)])
+
+print('.' + lines[1:])
+
+
+

+ 21 - 0
xml_mdf.py

@@ -0,0 +1,21 @@
+'''
+> https://www.codingame.com/ide/puzzle/xml-mdf-2016
+@author: olivier.massot, 2019
+'''
+
+s = "ab-bcd-d-c-ae-e"
+
+opened, depth, score = True, 0, {}
+
+for c in s:
+    if c == '-':
+        opened = False
+    elif opened:
+        depth += 1
+    else:
+        score[c] = score.get(c, 0) + 1 / depth
+        depth -= 1
+        opened = True
+
+print(score)
+print(max(score, key=score.get))