olinox 6 years ago
commit
101f2ad2d5
6 changed files with 2797 additions and 0 deletions
  1. 8 0
      .gitignore
  2. 801 0
      carribean/script.py
  3. 682 0
      code_a_la_mode/cook.py
  4. 596 0
      code_a_la_mode/cook_svg.py
  5. 601 0
      code_a_la_mode/cook_svg2.py
  6. 109 0
      code_a_la_mode/recurse_try.py

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+*.pyc
+.project
+.pydevproject
+.settings/
+temp/
+test/
+output/
+*.log

+ 801 - 0
carribean/script.py

@@ -0,0 +1,801 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+
+# TODO:
+# * add an esquive manoeuvre / try to avoid cannonballs
+# * separate the main loop in two: planning, then acting
+# * consider targeting rum barrels if an ennemy is nearer
+# * compute first and second target instead of only one to anticipate the next move
+# * if an enemy is near a mine, shoot the mine instead of the ship
+
+debug = True
+
+def log(*msg):
+    if debug:
+        print(*msg, file=sys.stderr)
+
+current_turn = 0
+
+class DidNotAct(Exception):
+    pass
+
+class Base():
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+
+class Position(Base):
+    def __init__(self, x, y):
+        self.pos = (x, y)
+
+class ShootingSpot(Position):
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.interest = 0
+
+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):
+    def __init__(self):
+        self.w = 23
+        self.h = 21
+        
+        self._neighbors = {}
+        for x in range(-1, self.w + 1):
+            for y in range(-1, self.h + 1):
+                self.cache_neighbors(x, y)
+ 
+        self.load_entities({})
+        
+    def __contains__(self, key):
+        return 0 <= key[0] < self.w and 0 <= key[1] < self.h
+
+    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.pos in [e.pos for e in entities.values() if type(e) is Mine]:
+                    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 = []
+        
+        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)
+                
+        
+    def at(self, x, y):
+        try:
+            return self.index[(x, y)]
+        except KeyError:
+            return None
+        
+    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 pre_evaluate_barrels_interest(self):
+        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))
+            
+    def evaluate_barrels_interest(self, ship):
+        for b in self.barrels:
+            b.distance = Grid.manhattan(ship.next_pos, b.pos)
+            b.alignement = abs(Grid.diff_directions(Grid.direction_to(*ship.prow, *b.pos), ship.orientation))
+            b.about_to_be_picked = any(b.pos in s.next_area for s in self.ennemy_ships)
+                    
+    def evaluate_ennemies_interest(self, ship):
+        for s in self.ennemy_ships:
+            s.distance = Grid.manhattan(ship.next_pos, s.next_pos)
+            s.alignement = abs(self.diff_directions(self.direction_to(*ship.prow, *s.next_pos), ship.orientation))
+    
+    def pre_update_moving_costs(self):
+        self.moving_costs = {}
+        
+        for x in range(-1, self.w + 1):
+            for y in range(-1, self.h + 1):
+                if x in (0, self.w) or y in (0, self.h):
+                    self.moving_costs[(x, y)] = 15 # borders are a little more expensive
+                elif x in (-1, self.w + 1) or y in (-1, self.h + 1):
+                    self.moving_costs[(x, y)] = 1000 # out of the map
+                else:
+                    self.moving_costs[(x, y)] = 10 # base moving cost
+        
+        for m in self.mines:
+            for n in self.neighbors(*m.pos):
+                self.moving_costs[n] += 30
+        for m in self.mines:
+            self.moving_costs[m.pos] += 1000
+        for c in self.cannonballs:
+            self.moving_costs[c.pos] += (100 + (5 - c.countdown) * 200)
+        
+    def update_moving_costs(self, ship):
+        for s in self.ships:
+            if s is ship:
+                continue
+            dist = self.manhattan(ship.pos, s.pos)
+            if dist > 8:
+                continue
+            for c in self.neighbors(*s.pos):
+                self.moving_costs[c] += 100 * abs(3 - s.speed)
+            for c in self.zone(s.next_pos, 4):
+                self.moving_costs[c] += 20
+    
+    def shooting_spot(self, ship, targetted_ship):
+        self.shooting_spots = []
+        for x, y in self.zone(targetted_ship.next_pos, 10):
+            if self.moving_costs[(x, y)] > 10:
+                continue
+            if self.manhattan((x, y), targetted_ship.next_pos) < 2:
+                continue
+            
+            spot = ShootingSpot(x, y)
+            
+            spot.interest -= self.moving_costs[(x, y)]
+            
+            # avoid cells too close from borders
+            if not (3 <= x <= (self.w - 3) and 3 <= y < (self.h - 3)):
+                spot.interest -= 10
+            
+            # priorize spots at distance 5 from active ship
+            spot.interest += 10 * abs(5 - self.manhattan((x, y), ship.pos))
+            
+            self.shooting_spots.append(spot)
+
+        return max(self.shooting_spots, key= lambda x: x.interest)
+
+    # 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) 
+
+    def zone(self, center, radius):
+        buffer = frozenset([center])
+        for _ in range(0, radius):
+            current = buffer
+            for x, y in current:
+                buffer |= frozenset(self.neighbors(x, y))
+        return [c for c in buffer if 0 <= c[0] < self.w and 0 <= c[1] < self.h]       
+
+    @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 diff_directions(d1, d2):
+        d = d2 - d1
+        if d <= -3:
+            d += 6
+        elif d > 3:
+            d -= 6
+        return d
+    
+    @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))
+    
+    def cache_neighbors(self, xc, yc):
+        self._neighbors[(xc, yc)] = [(x, y) for x, y in Grid.abs_neighbors(xc, yc) if 0 <= x < self.w and 0 <= y < self.h]
+
+    def neighbors(self, x, y):
+        try:
+            return self._neighbors[(x, y)]
+        except KeyError:
+            self.cache_neighbors(x, y)
+            return self._neighbors[(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, origin, orient0, target, incl_start=False, limit=10000):
+        nodes = []
+        break_on, iteration = limit, 0
+        
+        origin = PathNode(*origin)
+        origin.orientation = orient0
+        heapq.heappush(nodes, (0, origin))
+        neighbors = []
+
+        while nodes:
+            current = heapq.heappop(nodes)[1]
+
+            if current == target:
+                path = []
+                previous = current
+                while previous:
+                    if previous != origin or incl_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
+                
+                iteration += 1
+                if break_on > 0 and iteration >= break_on:
+                    return None
+                
+                moving_cost = self.moving_costs[x, y]
+                if moving_cost >= 1000:
+                    continue
+                
+                d = Grid.direction_to(*current, x, y)
+                diff = abs(Grid.diff_directions(current.orientation, d))
+                if diff > 1:
+                    # change direction one degree at a time
+                    continue
+                    
+                cost = current.cost + moving_cost + diff * 10
+                if diff != 0 and any(self.moving_costs[c] >= 1000 for c in neighbors):
+                    # a direction change here is dangerous
+                    cost += 50
+                
+                priority = cost + 10 * Grid.manhattan((x, y), target)
+
+                node = PathNode(x, y, current)
+                node.cost = cost
+                node.orientation = d
+                heapq.heappush(nodes, (priority, node))
+        else:
+            return None
+
+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)
+    
+class Ship(Entity):
+    MAX_SPEED = 2
+    SCOPE = 10
+    
+    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.target = None
+        
+        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}>"
+
+    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.target = None
+        
+        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_area = Ship.get_area(*self.next_pos, self.orientation)
+        
+        self.mobility_zone = list(set(self.area + self.next_area))
+            
+        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_area(cls, x, y, orientation):
+        dx, dy = Grid.directions(y)[((orientation + 3) % 6)]
+        stern = (x + dx, y + dy)
+        
+        dx, dy = Grid.directions(y)[orientation]
+        prow = (x + dx, y + dy)
+        
+        return [prow, (x, y), stern]
+
+    def get_next_pos(self, in_=1):
+        x, y = self.x, self.y
+        for _ in range(in_):
+            for _ in range(self.speed):
+                dx, dy = Grid.directions(y)[self.orientation]
+                x, y = x + dx, y + dy
+        return x, y
+    
+    def get_next_cell(self, in_=1):
+        x, y = self.x, self.y
+        for _ in range(in_):
+            dx, dy = Grid.directions(y)[self.orientation]
+            x, y = x + dx, y + dy
+        return x, y
+
+    def in_current_direction(self, x, y):
+        return self.orientation == Grid.direction_to(*self.pos, x, y)
+
+    @property
+    def interest(self):
+        return 7 * self.distance + 3 * self.alignment - 20 * self.blocked_since - 10 * self.same_traject_since
+    
+    def acquire(self, target):
+        self.target = target
+        if type(target) is Barrel:
+            target.aimed = True
+
+    def move(self, *args, **kwargs):
+        try:
+            self._move(*args, **kwargs)
+            return True
+        except DidNotAct:
+            return False
+
+    def _move(self, path, avoid=[]):
+        
+        if path is None:
+            log(f"(!) broken: automove to {goto}")
+            ship.auto_move(*goto)
+            return
+        elif not path:
+            raise DidNotAct()
+        
+        # <--- special: avoid blocking situations
+        if current_turn > 1 and self.blocked_since >= 1:
+            dx, dy = Grid.directions(self.y)[((self.orientation + 1) % 6)]
+            if grid.moving_costs[self.x + dx, self.y + dy] <= 50:
+                self.turn_left()
+            else:
+                self.turn_right()
+            return
+        # --->
+        
+        # speed shall be at 1 when arriving on the "flag"
+        next_flag = next((i for i, n in enumerate(path) if n.orientation != self.orientation), None)
+        if next_flag is None:
+            next_flag = len(path)
+        
+        if next_flag < (2 * self.speed):
+            # the end of the path or a direction change is coming
+            diff = Grid.diff_directions(self.orientation, path[0].orientation)
+            
+            # <--- special: avoid the left/right hesitation when stopped
+            if diff and not self.speed and self.last_action in ["STARBOARD",  "PORT"]:
+                self.speed_up()
+                return
+            # --->
+            
+            if diff > 0:
+                self.turn_left()
+                return
+                
+            elif diff < 0:
+                self.turn_right()
+                return
+        
+            elif self.speed > 1:
+                self.slow_down()
+                return
+        
+        else:
+            if not self.speed or (next_flag > (2 * self.speed + 1) and self.speed < self.MAX_SPEED):
+                # long path and no direction change coming: speed up
+                self.speed_up()
+                return
+        
+        raise DidNotAct()
+           
+    def fire_at_will(self, *args, **kwargs):
+        try:
+            self._fire_at_will(*args, **kwargs)
+            return True
+        except DidNotAct:
+            return False
+           
+    def _fire_at_will(self, target, allies = []):
+        if not self.can_fire():
+            raise DidNotAct()
+        
+        log("** fire at will")
+        
+        next_positions = [target.get_next_pos(i) for i in range(1, 3)]
+        log(f"ennemy next pos: {next_positions}")
+        
+        avoid = []
+        if not self in allies:
+            allies.append(self)
+        for ally in allies:
+            avoid += ally.mobility_zone
+
+        log(f"avoid: {avoid}")
+
+        for i, p in enumerate(next_positions):
+            turn = i + 1
+            
+            if p in avoid:
+                continue
+            
+            dist_p = Grid.manhattan(self.prow, p)
+            
+            if dist_p > self.SCOPE:
+                continue
+            
+            if (1 + round(dist_p / 3)) == turn:
+                log(f"Precision: {p}, {dist_p}, {turn}")
+                ship.fire(*p)
+                return
+        
+        next_pos = next_positions[0]
+        dist_p = Grid.manhattan(self.prow, next_pos)
+        if dist_p <= self.SCOPE:
+            ship.fire(*p)
+            return
+        
+        raise DidNotAct()
+            
+    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
+            
+    # --- Basic commands
+    def auto_move(self, x, y):
+        self.last_action = "MOVE"
+        print(f"MOVE {x} {y}")
+        
+    def speed_up(self):
+        self.last_action = "FASTER"
+        print("FASTER")
+        
+    def slow_down(self):
+        self.last_action = "SLOWER"
+        print("SLOWER")    
+    
+    def turn_right(self):
+        self.last_action = "STARBOARD"
+        print("STARBOARD")
+    
+    def turn_left(self):
+        self.last_action = "PORT"
+        print("PORT")
+        
+    def wait(self):
+        self.last_action = "WAIT"
+        print("WAIT")
+        
+    def mine(self):
+        self.last_mining = current_turn
+        self.last_action = "MINE"
+        print("MINE")
+        
+    def fire(self, x, y):
+        self.last_fire = current_turn
+        self.last_action = "FIRE"
+        print(f"FIRE {x} {y}")
+
+class Barrel(Entity):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.amount = 0
+        self.distance = 0
+        self.dispersal = 0
+        self.alignement = False
+        self.mine_threat = 0
+        self.about_to_be_picked = False
+        self.aimed = 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])
+#         self.aimed = False
+        
+    @property
+    def interest(self):
+        # the lower the better
+        return 7 * self.distance \
+               + 3 * self.dispersal \
+               + self.mine_threat ** 2 \
+               + 7 * self.alignement \
+               - 100 * self.about_to_be_picked
+
+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])
+
+
+
+class Action(Base):
+    def __init__(self, *args):
+        self.args = args
+        
+    def resolve(self):
+        raise NotImplementedError
+
+class Move(Action):
+    pass
+
+class Fire(Action):
+    pass
+
+class TurnLeft(Action):
+    pass
+
+class TurnRight(Action):
+    pass
+
+class SpeedUp(Action):
+    pass
+
+class SlowDown(Action):
+    pass
+
+
+entities = {}
+map_entity = {"SHIP": Ship, 
+             "BARREL": Barrel,
+             "MINE": Mine,
+             "CANNONBALL": Cannonball}
+
+grid = Grid()
+
+
+while True:
+    seen = []
+    current_turn += 1
+    
+    # <--- get input
+    if 1:
+        my_ship_count, entity_count = int(input()), int(input())
+        for _ in range(entity_count):
+            ent_id, ent_type, *data = input().split()
+            ent_id = int(ent_id)
+            seen.append(ent_id)
+               
+            if not ent_id in entities:
+                entities[ent_id] = map_entity[ent_type](ent_id)
+                   
+            ent = entities[ent_id]
+            ent.update(*data)
+         
+        entities = {k: v for k, v in entities.items() if k in seen}
+    # --->
+    # <--- test input
+    else:
+        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]
+    # --->
+    
+    # log(entities)
+    grid.load_entities(entities)
+    
+    log(f"### turn {current_turn}")
+#     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}")
+
+    grid.pre_update_moving_costs()
+    grid.pre_evaluate_barrels_interest()
+
+    for ship in grid.owned_ships:
+        log(f"---- ship {ship.id} ---")
+        log(f"ship: {ship}")
+        grid.update_moving_costs(ship)
+        allies = [s for s in grid.owned_ships if s is not ship]
+        
+        target = None
+        
+        if grid.barrels:
+            grid.evaluate_barrels_interest(ship)
+            log("barrels interest: {}".format({b.pos: f"{b.interest} ({b.distance}/{b.dispersal}/{b.mine_threat}/{b.alignement})" for b in grid.barrels}))
+        if grid.ennemy_ships:
+            grid.evaluate_ennemies_interest(ship)
+            log("ennemies interest: {}".format({s.pos: f"{s.interest} ({s.distance}/{s.alignement}/{s.blocked_since}/{s.same_traject_since})" for s in grid.ennemy_ships}))
+
+        allies_targets = [a.target for a in allies]
+        
+        targetted_barrel = next((b for b in sorted(grid.barrels, key=lambda x: x.interest) if not b in allies_targets and not b.pos in ship.next_area), None)
+        targetted_ennemy = next((s for s in sorted(grid.ennemy_ships, key=lambda x: x.interest)), None)
+    
+        if not targetted_barrel and not targetted_ennemy:
+            log("(!) No target, wait")
+            ship.wait()
+            continue
+    
+        target = targetted_barrel or targetted_ennemy
+    
+        log(f"target ({target.__class__.__name__}): {target}")
+        ship.acquire(target)
+        
+        if type(target) is Ship:
+            goto = grid.shooting_spot(ship, target).pos
+        else:
+            goto = target.pos
+        log(f"goto: {goto}")
+        
+        path = grid.path(ship.next_pos, ship.orientation, goto, limit=6000 // len(grid.owned_ships))
+        log(f"path: {path}")
+        
+        if ship.move(path):
+            continue
+            
+        if ship.fire_at_will(targetted_ennemy, allies=grid.owned_ships):
+            continue
+        
+        log("ERROR: Did not act, wait")
+        ship.wait()

+ 682 - 0
code_a_la_mode/cook.py

@@ -0,0 +1,682 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+
+
+DEBUG = True
+
+
+def log(x):
+    if DEBUG: print(x, file=sys.stderr)
+
+
+# -- Base class
+class Base():
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+
+# --- locations
+class Location(Base):
+    name = ""
+    passable = False
+
+class SpecialLocation(Location):
+    pass
+
+class DishWasher(SpecialLocation):
+    pass
+
+class IcecreamCrate(SpecialLocation):
+    pass
+
+class BlueberriesCrate(SpecialLocation):
+    pass
+
+class StrawberriesCrate(SpecialLocation):
+    pass
+
+class DoughCrate(SpecialLocation):
+    pass
+
+class ChoppingBoard(SpecialLocation):
+    pass
+
+class Oven(SpecialLocation):
+    def __init__(self):
+        self._content = None
+        self._timer = 0
+
+    @property
+    def content(self):
+        return self._content
+    
+    @content.setter
+    def content(self, content):
+        self._content = match[content]
+        
+    @property
+    def timer(self):
+        return self._timer
+    
+    @timer.setter
+    def timer(self, timer):
+        self._timer = int(timer)
+        
+    def update(self, content, timer):
+        self.content = content
+        self.timer = timer
+
+oven = Oven()
+
+
+class Window(SpecialLocation):
+    pass
+
+class Start(Location):
+    passable = True
+
+class Start0(Start): pass
+class Start1(Start): pass
+
+class FloorCell(Location):
+    passable = True
+
+class EmptyTable(Location):
+    pass
+
+locations = [DishWasher, IcecreamCrate, BlueberriesCrate, StrawberriesCrate, DoughCrate, ChoppingBoard, Oven, Window, Start0, Start1, FloorCell, EmptyTable]
+special_locations = [l for l in locations if l is SpecialLocation]
+tables = []
+
+# -- Grid
+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 is content]
+
+    @staticmethod
+    def distance(from_, to_):
+        return abs(from_[0] - to_[0]) + abs(from_[1] - to_[1])
+
+    def items(self):
+        return [c for row in self.cells for c in row]
+
+    def closest_in(self, from_, coords):
+        return sorted([(c, Grid.distance(from_, c)) for c in coords], key=lambda k: k[1])[0]
+
+    def closest(self, from_, content):
+        return self.closest_in(from_, self.where_are(content))
+
+    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).passable and self.cost(x, y) < 50
+
+    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))
+        break_on = 10000
+        iteration = 0
+
+        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:
+                iteration += 1
+                if iteration >= break_on:
+                    return None
+
+                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
+
+## -- objects
+
+
+
+class Recipe(Base):
+    name = ""
+    location = None
+    cooking_time = 0
+    requirements = []
+    needs_free_hands = False
+    
+    def available(self):
+        return []
+    
+    def get(self):
+        pass
+        
+class FinalProduct(Recipe):
+    needs_dish = True
+    
+class Ingredient(Recipe):
+    needs_free_hands = True    
+
+
+
+class Dish(Recipe):
+    name = "DISH"
+    location = DishWasher
+
+class IceCream(FinalProduct):
+    name = "ICE_CREAM"
+    location = IcecreamCrate
+
+class Blueberries(FinalProduct):
+    name = "BLUEBERRIES"
+    location = BlueberriesCrate
+    
+class Strawberries(Ingredient):
+    name = "STRAWBERRIES"
+    location = StrawberriesCrate
+    needs_free_hands = True
+    
+class ChoppedStrawberries(FinalProduct):
+    name = "CHOPPED_STRAWBERRIES"
+    location = ChoppingBoard
+    requirements = [Strawberries]
+    
+class Dough(Ingredient):
+    name = "DOUGH"
+    location = DoughCrate
+    needs_free_hands = True
+
+class Croissant(FinalProduct):
+    name = "CROISSANT"
+    requirements = [Dough]
+    location = Oven
+    cooking_time = 10
+    
+class ChoppedDough(Ingredient):
+    name = "CHOPPED_DOUGH"
+    requirements = [Dough]
+    location = ChoppingBoard
+    
+class RawTart(Ingredient):
+    name = "RAW_TART"
+    requirements = [ChoppedDough]
+    location = BlueberriesCrate
+    
+class Tart(FinalProduct):
+    name = "TART"
+    requirements = [RawTart]
+    location = Oven
+    cooking_time = 10
+
+# -- Other 
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.order = [match[i] for i in item.split('-')]
+        self.award = int(award)
+
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = [match[i] for i in item.split('-')]
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+
+# --- Actions
+
+class Action(Base):
+    needs_dish = False
+    needs_free_hands = False
+     
+    def __init__(self, location):
+        self.location = location
+     
+    def locate(self, player):
+        self.pos, self.dist = grid.closest(player.pos, self.location)
+ 
+class GetAction(Action):
+ 
+    def __init__(self, subject):
+        self.subject = subject
+        self.location = self.subject.location
+     
+    def locate(self, player):
+        from_crate = grid.where_are(self.location)
+        on_tables = [t.pos for t in tables if [self.subject] == t.item]
+        self.pos, self.dist = grid.closest_in(player.pos, from_crate + on_tables)
+ 
+class GetDessert(GetAction):
+    needs_dish = True
+ 
+class GetIngredient(GetAction):
+    needs_free_hands = True
+ 
+class GetDish(GetAction):
+    def __init__(self):
+        super().__init__(Dish)
+ 
+class Transform(Action):
+    pass
+ 
+class Cook(Action):
+    def __init__(self):
+        self.location = Oven
+ 
+class GetCookedDessert(GetDessert):
+    def __init__(self, subject, ready_in=0):
+        super().__init__(subject)
+        self.location = Oven
+        self.ready_in = ready_in
+
+class Deliver(Action):
+    def __init__(self):
+        self.location = Window
+ 
+class DropHanded(Action):
+    def __init__(self):
+        self.subject = None
+        self.location = EmptyTable
+     
+    def locate(self, player):
+        free_tables = [c for c in grid.where_are(self.location) if not c in [(t.x, t.y) for t in tables]]
+        self.pos, self.dist = grid.closest_in(player.pos, free_tables)
+
+class GetOnTable(GetAction):
+    def __init__(self, tables):
+        self._tables = tables
+    
+    def locate(self, player):
+        self.pos, self.dist = grid.closest_in(player.pos, [t.pos for t in self._tables])
+
+class GetIngredientOnTable(GetOnTable):
+    needs_free_hands = True
+
+class GetDishOnTable(GetOnTable):
+    pass
+
+
+
+class Cooker(Base):
+    def __init__(self):
+        self._x = -1
+        self._y = -1
+        self._in_hand = []
+        self.order = []
+
+    @property
+    def x(self):
+        return self._x
+
+    @x.setter
+    def x(self, x):
+        self._x = int(x)
+
+    @property
+    def y(self):
+        return self._y
+
+    @y.setter
+    def y(self, y):
+        self._y = int(y)
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    @property
+    def in_hand(self):
+        return self._in_hand
+
+    @in_hand.setter
+    def in_hand(self, item):
+        self._in_hand = [x for x in [match[i] for i in item.split('-')] if x is not None]
+
+    def take_order(self, order):
+        self.order = order
+
+    def order_fullfilled(self):
+        self.order = []
+
+    def update(self, x, y, item):
+        self.x = x
+        self.y = y
+        self.in_hand = item
+
+    @property
+    def hands_free(self):
+        return len(self._in_hand) == 0
+
+    @property
+    def hands_full(self):
+        return any(recipe.needs_free_hands for recipe in self._in_hand)
+
+    @property
+    def dish_handed(self):
+        return Dish in self.in_hand
+
+    def eval_orders(self, customers):
+        waiting = sorted(customers, reverse=True, key=lambda x: x.award)
+        self.take_order(waiting[0].order)
+        
+    def get_recipe(self, recipe):
+        
+        on_tables = [t for t in tables if t.item == [recipe]]
+        if on_tables:
+            if issubclass(recipe, Ingredient):
+                return GetIngredientOnTable(on_tables)
+            else:
+                return GetOnTable(on_tables)
+        
+        elif recipe.cooking_time and (recipe == oven.content or (any((req == oven.content) for req in recipe.requirements) and oven.timer < 3)):
+            return GetCookedDessert(recipe)
+        
+        else:
+            # check requirements
+            
+            if recipe.requirements:
+                
+                if all((req in self.in_hand) for req in recipe.requirements):
+                    if recipe.cooking_time:
+                        return Cook()
+                    else:
+                        return Transform(recipe.location)
+                
+                else:
+                    
+                    for req in recipe.requirements:
+                        if not req in self.in_hand and not req == oven.content:
+                            return self.get_recipe(req)
+                    
+            else:
+                # no requirements, simple get
+                if issubclass(recipe, Ingredient):
+                    return GetIngredient(recipe)
+                elif issubclass(recipe, Dish):
+                    return GetDish()
+                else:
+                    return GetAction(recipe)
+            
+    def todo(self):
+        todo = []
+        
+        # nothing left to do: deliver
+        if all((i in self.in_hand) for i in self.order):
+            todo = [Deliver()]
+        
+        store = None
+        for table in tables:
+            if Dish in table.item and all((i in self.order and not i in self.in_hand) for i in table.item):
+                store = table
+        
+        for item in self.order:
+
+            if item in self.in_hand or (store and item in store.item):
+                # already done
+                continue
+            
+            action = self.get_recipe( item )
+            if action:
+                todo.append( action )
+        
+        if store:
+            todo.append(GetDishOnTable([store]))
+        
+        for action in todo:
+            action.locate(self)
+        
+        return todo
+
+
+    def act(self, action):
+        if isinstance(action, Deliver) and action.pos in grid.neighbors(*self.pos):
+            # we're about to deliver, clean the order
+            self.order = []
+        
+        elif (action.needs_free_hands and not self.hands_free) or (isinstance(action, GetAction) and self.hands_full):
+            # cannot act, needs to drop
+            action = DropHanded()
+            action.locate(self)
+            log(f"Need to drop before: {action.pos}")
+            
+        self.use(*action.pos)
+
+    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")
+
+# --- constants
+match = {
+            '0': Start0,
+            '1': Start1,
+            'B': BlueberriesCrate,
+            'I': IcecreamCrate,
+            'S': StrawberriesCrate,
+            'C': ChoppingBoard,
+            'H': DoughCrate,
+            'W': Window,
+            '#': EmptyTable,
+            'D': DishWasher,
+            '.': FloorCell,
+            'O': Oven,
+            'NONE': None,
+            'DISH': Dish,
+            'ICE_CREAM': IceCream,
+            'BLUEBERRIES': Blueberries,
+            'STRAWBERRIES': Strawberries,
+            'CHOPPED_STRAWBERRIES': ChoppedStrawberries,
+            'DOUGH': Dough,
+            'CROISSANT': Croissant,
+            'CHOPPED_DOUGH': ChoppedDough,
+            'RAW_TART': RawTart,
+            'TART': Tart,
+        }
+
+# --- input vars
+
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([[match[c] for c in input()] for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+player = Cooker()
+partner = Cooker()
+oven = next((i for i in grid.items() if type(i) is Oven), Oven())
+
+path = []
+blocked_since = 0
+previous_partner_pos = None
+partner_did_not_moved_since = 0
+
+while True:
+    # <--- turn input
+    turns_remaining = int(input())
+
+    player.update(*input().split())
+    log(f"*** player: {player}")
+
+    partner.update(*input().split())
+    log(f"*** partner: {partner}")
+    if partner.pos != previous_partner_pos:
+        previous_partner_pos = partner.pos
+        partner_did_not_moved_since = 0
+    else:
+        partner_did_not_moved_since += 1
+        log(f"partner did not moved since: {partner_did_not_moved_since}")
+        
+
+    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.update(*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 awarded
+    if not player.order or not player.order in [c.order for c in customers]:
+        player.eval_orders(customers)
+        log('>>> new order taken')
+
+    log(f"order: {player.order}")
+
+    todo = player.todo()
+    log(f"todo: {todo}")
+    
+
+    priority = [
+            # order fulfilled: deliver
+            next((a for a in todo if type(a) is Deliver), None),
+            
+            # cook has a dessert in its hands and no dish, he have to take one
+            next((a for a in todo if isinstance(a, GetDish) and any(r for r in player.in_hand if issubclass(r, FinalProduct))), None),
+            
+            # something to take out from the oven!
+            next((a for a in todo if type(a) is GetCookedDessert and a.ready_in < 3), None),
+            
+            # If cook has an ingredient in hands, he needs to prepare it
+            next((a for a in todo if type(a) is Transform), None),
+            
+            # If cook has an cook_ingredient in hands, he needs to cook it
+            next((a for a in todo if type(a) is Cook and not oven.content), None),
+            
+            # If hands are free and an ingredient is needed, we go for it first
+            next((a for a in todo if a.needs_free_hands and player.hands_free), None),
+            
+            # there is a dish waiting on a table
+            next((a for a in todo if type(a) is GetDishOnTable), None),
+            
+            # get the closest action
+            next((a for a in sorted(todo, key=lambda x: x.dist) if (type(a) != GetCookedDessert or a.ready_in < 3)), None),
+            
+            # wait for the oven to finish
+            next((a for a in todo if type(a) is GetCookedDessert), None),
+            
+        ]
+    
+    log(f"priorities: {priority}")
+    priority = [p for p in priority if not p is None]
+    
+    if not priority:
+        log("nothing to do??")
+        player.wait()
+        continue
+    
+    next_task = priority[0]
+    
+    log(f"next_task: {next_task}")
+ 
+    # <--- Update moving costs
+    # 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 + (5*partner_did_not_moved_since)) 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 isinstance(grid.at(*c), SpecialLocation))) else 1
+        grid.add_costs[(x, y)] = k1 * k2 * 3
+    log(f"add_costs: {grid.add_costs}")
+    # --->
+ 
+    # <--- compute shortest path
+    previous_path = path
+    path = grid.path(player.pos, next_task.pos)
+    log(f"path: {path}")
+    # --->
+    
+    # <-- try to avoid blocking situations
+    if path and path == previous_path:
+        log(f">> path did not change, blocked? turn: {blocked_since}")
+        blocked_since += 1
+    else:
+        blocked_since = 0
+
+    if blocked_since > 1:
+        aside = next((c for c in grid.neighbors(*player.pos) if grid.passable(*c) and not c == partner.pos), None)
+        if aside:
+            blocked_since = 0
+            log("step aside")
+            player.move(*aside)
+            continue
+        else:
+            log("ouch, cannot move aside")
+        
+    # --->
+ 
+    if path is not None:
+        if len(path) > 0:
+            if len(path) > 4:
+                player.move(*path[3])
+            else:
+                player.move(*path[-1])
+        else:
+            player.act(next_task)
+    else:
+        player.act(next_task)

+ 596 - 0
code_a_la_mode/cook_svg.py

@@ -0,0 +1,596 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+
+DEBUG = True
+
+
+def log(x):
+    if DEBUG: print(x, file=sys.stderr)
+
+
+# -- Base class
+class Base():
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+
+# --- locations
+class Location(Base):
+    name = ""
+    passable = False
+
+class SpecialLocation(Location):
+    pass
+
+class DishWasher(SpecialLocation):
+    pass
+
+class IcecreamCrate(SpecialLocation):
+    pass
+
+class BlueberriesCrate(SpecialLocation):
+    pass
+
+class StrawberriesCrate(SpecialLocation):
+    pass
+
+class DoughCrate(SpecialLocation):
+    pass
+
+class ChoppingBoard(SpecialLocation):
+    pass
+
+class Oven(SpecialLocation):
+    def __init__(self):
+        self._content = None
+        self._timer = 0
+
+    @property
+    def content(self):
+        return self._content
+    
+    @content.setter
+    def content(self, content):
+        self._content = match[content]
+        
+    @property
+    def timer(self):
+        return self._timer
+    
+    @timer.setter
+    def timer(self, timer):
+        self._timer = int(timer)
+        
+    def update(self, content, timer):
+        self.content = content
+        self.timer = timer
+
+class Window(SpecialLocation):
+    pass
+
+class Start(Location):
+    passable = True
+
+class Start0(Start): pass
+class Start1(Start): pass
+
+class FloorCell(Location):
+    passable = True
+
+class EmptyTable(Location):
+    pass
+
+locations = [DishWasher, IcecreamCrate, BlueberriesCrate, StrawberriesCrate, DoughCrate, ChoppingBoard, Oven, Window, Start0, Start1, FloorCell, EmptyTable]
+special_locations = [l for l in locations if l is SpecialLocation]
+
+# -- Grid
+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 is content]
+
+    @staticmethod
+    def distance(from_, to_):
+        return abs(from_[0] - to_[0]) + abs(from_[1] - to_[1])
+
+    def items(self):
+        return [c for row in self.cells for c in row]
+
+    def closest_in(self, from_, coords):
+        return sorted([(c, Grid.distance(from_, c)) for c in coords], key=lambda k: k[1])[0]
+
+    def closest(self, from_, content):
+        return self.closest_in(from_, self.where_are(content))
+
+    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).passable
+
+    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
+
+
+class Order(Base):
+    def __init__(self, order):
+        self.order = order
+
+class Plate(Base):
+    def __init__(self):
+        self.pos = (-1, -1)
+        self.content = []
+
+class Dish(Base):
+    location = DishWasher
+
+class BaseDessert(Base):
+    name = ""
+
+class SimpleDessert(BaseDessert):
+    location = None
+
+class PreparedDessert(BaseDessert):
+    ingredients = []
+
+class CookedDessert(BaseDessert):
+    ingredients = []
+
+class Ingredient(Base):
+    location = None
+    transformer = None
+
+class CookIngredient():
+    location = None
+    cooking_time = 0
+    
+# --- desserts
+class IceCream(SimpleDessert):
+    location = IcecreamCrate
+
+class Blueberries(SimpleDessert):
+    location = BlueberriesCrate
+
+class Strawberries(Ingredient):
+    location = StrawberriesCrate
+    transformer = ChoppingBoard
+
+class ChoppedStrawberries(PreparedDessert):
+    ingredients = [Strawberries]
+
+class Dough(CookIngredient):
+    location = DoughCrate
+    cooking_time = 10
+
+class Croissant(CookedDessert):
+    location = Oven
+    ingredients = [Dough]
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.order = [match[i] for i in item.split('-')]
+        self.award = int(award)
+
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = [match[i] for i in item.split('-')]
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+
+# --- Actions
+
+class Action(Base):
+    needs_dish = False
+    needs_hands = False
+    
+    def __init__(self, location):
+        self.location = location
+    
+    def locate(self, player):
+        self.pos, self.dist = grid.closest(player.pos, self.location)
+
+class GetAction(Action):
+
+    def __init__(self, subject):
+        self.subject = subject
+        self.location = self.subject.location
+    
+    def locate(self, player):
+        available = grid.where_are(self.location)
+        available += [t.pos for t in tables if self.subject == [t.item]]
+        self.pos, self.dist = grid.closest_in(player.pos, available)
+
+class GetDessert(GetAction):
+    needs_dish = True
+
+class GetIngredient(GetAction):
+    needs_hands = True
+
+class GetDish(GetAction):
+    def __init__(self):
+        super().__init__(Dish)
+
+class Transform(Action):
+    def __init__(self, subject):
+        self.location = subject.transformer
+
+class Cook(Action):
+    def __init__(self):
+        self.location = Oven
+
+class GetCookedDessert(GetDessert):
+    def __init__(self, subject):
+        super().__init__(subject)
+        self.location = Oven
+        
+class Deliver(Action):
+    def __init__(self):
+        self.location = Window
+
+class DropDish(Action):
+    def __init__(self):
+        self.subject = None
+        self.location = EmptyTable
+
+# class GetBackDish(Action):
+#     def __init__(self, pos):
+#         self._pos = pos
+#     
+#     def locate(self, player):
+#         self.pos = self._pos
+#         self.dist = Grid.distance(player.pos, self._pos)
+        
+class GetOnTable(Action):
+    def __init__(self, pos):
+        self._pos = pos
+    
+    def locate(self, player):
+        self.pos = self._pos
+        self.dist = Grid.distance(player.pos, self._pos)
+
+class Cooker(Base):
+    def __init__(self):
+        self._x = -1
+        self._y = -1
+        self._in_hand = []
+#         self.plate = Plate()
+        self.order = []
+#         self.unexpected = []
+
+    @property
+    def x(self):
+        return self._x
+
+    @x.setter
+    def x(self, x):
+        self._x = int(x)
+
+    @property
+    def y(self):
+        return self._y
+
+    @y.setter
+    def y(self, y):
+        self._y = int(y)
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    @property
+    def in_hand(self):
+        return self._in_hand
+
+    @in_hand.setter
+    def in_hand(self, item):
+        self._in_hand = [x for x in [match[i] for i in item.split('-')] if x is not None]
+
+    def take_order(self, order):
+        self.order = order
+
+    def order_fullfilled(self):
+        self.order = []
+
+    def update(self, x, y, item):
+        self.x = x
+        self.y = y
+        self.in_hand = item
+
+    @property
+    def hands_free(self):
+        return len(self._in_hand) == 0
+
+    @property
+    def dish_handed(self):
+        return Dish in self.in_hand
+
+    def eval_orders(self, customers):
+        waiting = sorted(customers, reverse=True, key=lambda x: x.award)
+        self.take_order(waiting[0].order)
+        
+        
+    def todo(self):
+        todo = []
+        
+        store = None
+        
+        for table in tables:
+            if all((i in self.order and not i in self.in_hand) for i in table.item):
+                store = table
+        
+        for item in self.order:
+
+            if item in self.in_hand or (store and item in store.item):
+                # already done
+                continue
+            
+            if issubclass(item, SimpleDessert):
+                todo.append(GetDessert(item))
+                
+            elif issubclass(item, PreparedDessert):
+                for ingredient in item.ingredients:
+                    if ingredient in self.in_hand:
+                        todo.append(Transform(ingredient))
+                    else:
+                        todo.append(GetIngredient(ingredient))
+                    
+            elif issubclass(item, CookedDessert):
+                for ingredient in item.ingredients:
+                    if ingredient in self.in_hand:
+                        todo.append(Cook())
+                    elif type(ingredient) == type(oven.content) or type(item) == type(oven.content):
+                        if oven.timer < 3:
+                            todo.append(GetCookedDessert(item))
+                    else:
+                        todo.append(GetIngredient(ingredient))
+                        
+            elif issubclass(item, Dish):
+                todo.append(GetDish())
+                 
+            else:
+                log(f"<!> Unknown order: {item}")
+        
+        if store:
+            todo.append(GetOnTable(store.pos))
+        
+        # nothing left to do: deliver
+        if not todo:
+            todo = [Deliver()]
+        
+        # if the current order is not anymore in the queue, drop the dish
+        if not self.order in [c.order for c in customers]:
+            player.order = []
+            todo = [DropDish()]
+        
+        for action in todo:
+            action.locate(self)
+        
+        return todo
+
+
+    def act(self, action):
+        if isinstance(action, Deliver) and action.pos in grid.neighbors(*self.pos):
+            self.order = []
+            
+        elif isinstance(action, GetIngredient) and self.dish_handed:
+            # cannot act, needs to drop the dish
+            log("need to drop")
+            action = DropDish()
+            action.locate(self)
+#             self.unexpected.append(GetBackDish(action.pos))
+            
+        self.use(*action.pos)
+
+    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")
+
+# --- constants
+match = {
+            '0': Start0,
+            '1': Start1,
+            'B': BlueberriesCrate,
+            'I': IcecreamCrate,
+            'S': StrawberriesCrate,
+            'C': ChoppingBoard,
+            'H': DoughCrate,
+            'W': Window,
+            '#': EmptyTable,
+            'D': DishWasher,
+            '.': FloorCell,
+            'O': Oven,
+            'NONE': None,
+            'DISH': Dish,
+            'ICE_CREAM': IceCream,
+            'BLUEBERRIES': Blueberries,
+            'STRAWBERRIES': Strawberries,
+            'CHOPPED_STRAWBERRIES': ChoppedStrawberries,
+            'DOUGH': Dough,
+            'CROISSANT': Croissant
+        }
+
+# --- input vars
+
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([[match[c] for c in input()] for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+player = Cooker()
+partner = Cooker()
+oven = next((i for i in grid.items() if type(i) is Oven), Oven())
+
+while True:
+    # <--- turn input
+    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.update(*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 awarded
+    if not player.order:
+        player.eval_orders(customers)
+        log(f'>>> new order taken: {player.order}')
+
+    todo = player.todo()
+    log(f"todo: {todo}")
+    
+    next_task = None
+    for action in todo:
+        if isinstance(action, Deliver):
+            # order fulfilled: deliver
+            next_task = action
+            break
+            
+        elif isinstance(action, Transform):
+            # If cook has an ingredient in hands, he needs to prepare it
+            next_task = action
+            break
+        
+        elif isinstance(action, Cook):
+            # If cook has an cook_ingredient in hands, he needs to cook it
+            next_task = action
+            break
+
+        elif action.needs_hands and player.hands_free:
+            # If hands are free and an ingredient is needed, we go for it first
+            next_task = action
+            break
+
+        elif isinstance(action, GetOnTable):
+            # there is a dish waiting on a table
+            next_task = action
+            break    
+        
+        elif isinstance(action, GetDish) and any(h for h in player.in_hand if issubclass(h, BaseDessert)):
+            # cook has a dessert in its hands and no dish, he have to take one
+            next_task = action
+            break
+        
+    if not next_task:
+        # else, go for the closest task
+        tasks = sorted(todo, key=lambda x: x.dist)
+        next_task = next(iter(tasks))
+
+    log(f"next task: {next_task}")
+ 
+    # <--- Update moving costs
+    # 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 isinstance(grid.at(*c), SpecialLocation))) else 1
+        grid.add_costs[(x, y)] = k1 * k2 * 3
+    log(grid.add_costs)
+    # --->
+ 
+    # <--- compute shortest path
+    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.act(next_task)
+    else:
+        player.act(next_task)

+ 601 - 0
code_a_la_mode/cook_svg2.py

@@ -0,0 +1,601 @@
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+
+DEBUG = True
+
+
+def log(x):
+    if DEBUG: print(x, file=sys.stderr)
+
+
+# -- Base class
+class Base():
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.__dict__}>"
+
+
+# --- locations
+class Location(Base):
+    name = ""
+    passable = False
+
+class SpecialLocation(Location):
+    pass
+
+class DishWasher(SpecialLocation):
+    pass
+
+class IcecreamCrate(SpecialLocation):
+    pass
+
+class BlueberriesCrate(SpecialLocation):
+    pass
+
+class StrawberriesCrate(SpecialLocation):
+    pass
+
+class DoughCrate(SpecialLocation):
+    pass
+
+class ChoppingBoard(SpecialLocation):
+    pass
+
+class Oven(SpecialLocation):
+    def __init__(self):
+        self._content = None
+        self._timer = 0
+
+    @property
+    def content(self):
+        return self._content
+    
+    @content.setter
+    def content(self, content):
+        self._content = match[content]
+        
+    @property
+    def timer(self):
+        return self._timer
+    
+    @timer.setter
+    def timer(self, timer):
+        self._timer = int(timer)
+        
+    def update(self, content, timer):
+        self.content = content
+        self.timer = timer
+
+class Window(SpecialLocation):
+    pass
+
+class Start(Location):
+    passable = True
+
+class Start0(Start): pass
+class Start1(Start): pass
+
+class FloorCell(Location):
+    passable = True
+
+class EmptyTable(Location):
+    pass
+
+locations = [DishWasher, IcecreamCrate, BlueberriesCrate, StrawberriesCrate, DoughCrate, ChoppingBoard, Oven, Window, Start0, Start1, FloorCell, EmptyTable]
+special_locations = [l for l in locations if l is SpecialLocation]
+tables = []
+
+# -- Grid
+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 is content]
+
+    @staticmethod
+    def distance(from_, to_):
+        return abs(from_[0] - to_[0]) + abs(from_[1] - to_[1])
+
+    def items(self):
+        return [c for row in self.cells for c in row]
+
+    def closest_in(self, from_, coords):
+        return sorted([(c, Grid.distance(from_, c)) for c in coords], key=lambda k: k[1])[0]
+
+    def closest(self, from_, content):
+        return self.closest_in(from_, self.where_are(content))
+
+    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).passable
+
+    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
+
+
+class Order(Base):
+    def __init__(self, order):
+        self.order = order
+
+class Dish(Base):
+    location = DishWasher
+
+class BaseDessert(Base):
+    name = ""
+
+class SimpleDessert(BaseDessert):
+    location = None
+
+class PreparedDessert(BaseDessert):
+    ingredients = []
+
+class CookedDessert(BaseDessert):
+    ingredients = []
+
+class Ingredient(Base):
+    location = None
+    transformer = None
+
+class CookIngredient():
+    location = None
+    cooking_time = 0
+    
+# --- desserts
+class IceCream(SimpleDessert):
+    location = IcecreamCrate
+
+class Blueberries(SimpleDessert):
+    location = BlueberriesCrate
+
+class Strawberries(Ingredient):
+    location = StrawberriesCrate
+    transformer = ChoppingBoard
+
+class ChoppedStrawberries(PreparedDessert):
+    ingredients = [Strawberries]
+
+class Dough(CookIngredient):
+    location = DoughCrate
+    cooking_time = 10
+
+class Croissant(CookedDessert):
+    location = Oven
+    ingredients = [Dough]
+
+class Customer(Base):
+    def __init__(self, item, award):
+        self.order = [match[i] for i in item.split('-')]
+        self.award = int(award)
+
+class Table(Base):
+    def __init__(self, x, y, item):
+        self.x = int(x)
+        self.y = int(y)
+        self.item = [match[i] for i in item.split('-')]
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+
+# --- Actions
+
+class Action(Base):
+    needs_dish = False
+    needs_hands = False
+     
+    def __init__(self, location):
+        self.location = location
+     
+    def locate(self, player):
+        self.pos, self.dist = grid.closest(player.pos, self.location)
+ 
+class GetAction(Action):
+ 
+    def __init__(self, subject):
+        self.subject = subject
+        self.location = self.subject.location
+     
+    def locate(self, player):
+        available = grid.where_are(self.location)
+        available += [t.pos for t in tables if self.subject == [t.item]]
+        self.pos, self.dist = grid.closest_in(player.pos, available)
+ 
+class GetDessert(GetAction):
+    needs_dish = True
+ 
+class GetIngredient(GetAction):
+    needs_hands = True
+ 
+class GetDish(GetAction):
+    def __init__(self):
+        super().__init__(Dish)
+ 
+class Transform(Action):
+    def __init__(self, subject):
+        self.location = subject.transformer
+ 
+class Cook(Action):
+    def __init__(self):
+        self.location = Oven
+ 
+class GetCookedDessert(GetDessert):
+    def __init__(self, subject):
+        super().__init__(subject)
+        self.location = Oven
+         
+class Deliver(Action):
+    def __init__(self):
+        self.location = Window
+ 
+class DropHanded(Action):
+    def __init__(self):
+        self.subject = None
+        self.location = EmptyTable
+         
+class GetOnTable(GetAction):
+    def __init__(self, pos):
+        self._pos = pos
+     
+    def locate(self, player):
+        self.pos = self._pos
+        self.dist = Grid.distance(player.pos, self._pos)
+
+class GetDishOnTable(GetOnTable):
+    pass
+
+class Cooker(Base):
+    def __init__(self):
+        self._x = -1
+        self._y = -1
+        self._in_hand = []
+        self.order = []
+
+    @property
+    def x(self):
+        return self._x
+
+    @x.setter
+    def x(self, x):
+        self._x = int(x)
+
+    @property
+    def y(self):
+        return self._y
+
+    @y.setter
+    def y(self, y):
+        self._y = int(y)
+
+    @property
+    def pos(self):
+        return (self.x, self.y)
+
+    @property
+    def in_hand(self):
+        return self._in_hand
+
+    @in_hand.setter
+    def in_hand(self, item):
+        self._in_hand = [x for x in [match[i] for i in item.split('-')] if x is not None]
+
+    def take_order(self, order):
+        self.order = order
+
+    def order_fullfilled(self):
+        self.order = []
+
+    def update(self, x, y, item):
+        self.x = x
+        self.y = y
+        self.in_hand = item
+
+    @property
+    def hands_free(self):
+        return len(self._in_hand) == 0
+
+    @property
+    def hands_full(self):
+        return any(issubclass(i, Ingredient) for i in self._in_hand)
+
+    @property
+    def dish_handed(self):
+        return Dish in self.in_hand
+
+    def eval_orders(self, customers):
+        waiting = sorted(customers, reverse=True, key=lambda x: x.award)
+        self.take_order(waiting[0].order)
+        
+    def todo(self):
+        todo = []
+        
+        store = None
+        
+        for table in tables:
+            if Dish in table.item and all((i in self.order and not i in self.in_hand) for i in table.item):
+                store = table
+        
+        for item in self.order:
+
+            if item in self.in_hand or (store and item in store.item):
+                # already done
+                continue
+            
+            if issubclass(item, SimpleDessert):
+                todo.append(GetDessert(item))
+                
+            elif issubclass(item, PreparedDessert):
+                
+                on_table = next((t for t in tables if [item] == t.item), None)
+                if on_table:
+                    todo.append(GetOnTable(on_table.pos))
+
+                else:
+                    for ingredient in item.ingredients:
+                        if ingredient in self.in_hand:
+                            todo.append(Transform(ingredient))
+                        else:
+                            todo.append(GetIngredient(ingredient))
+                    
+            elif issubclass(item, CookedDessert):
+                
+                on_table = next((t for t in tables if [item] in t.item), None)
+                if on_table:
+                    todo.append(GetOnTable(on_table.pos))
+
+                else:
+                    for ingredient in item.ingredients:
+                        if ingredient in self.in_hand:
+                            todo.append(Cook())
+                            
+                        elif type(item) == type(oven.content):
+                            todo.append(GetCookedDessert(item))
+                            
+                        elif type(ingredient) == type(oven.content):
+                            if oven.timer < 3:
+                                todo.append(GetCookedDessert(item))
+                                
+                        else:
+                            todo.append(GetIngredient(ingredient))
+                        
+            elif issubclass(item, Dish):
+                todo.append(GetDish())
+                 
+            else:
+                log(f"<!> Unknown order: {item}")
+        
+        if store:
+            todo.append(GetOnTable(store.pos))
+        
+        # nothing left to do: deliver
+        if not todo:
+            todo = [Deliver()]
+        
+        # if the current order is not anymore in the queue, drop the dish
+        if not self.order in [c.order for c in customers]:
+            player.order = []
+            todo = [DropHanded()]
+        
+        for action in todo:
+            action.locate(self)
+        
+        return todo
+
+
+    def act(self, action):
+        log("{}, {}, {}, {}".format(action.needs_hands, self.hands_free, isinstance(action, GetAction), self.hands_full))
+        if isinstance(action, Deliver) and action.pos in grid.neighbors(*self.pos):
+            self.order = []
+        
+        elif (action.needs_hands and not self.hands_free) or (isinstance(action, GetAction) and self.hands_full):
+            # cannot act, needs to drop the dish
+            log("need to drop")
+            action = DropHanded()
+            action.locate(self)
+            
+        self.use(*action.pos)
+
+    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")
+
+# --- constants
+match = {
+            '0': Start0,
+            '1': Start1,
+            'B': BlueberriesCrate,
+            'I': IcecreamCrate,
+            'S': StrawberriesCrate,
+            'C': ChoppingBoard,
+            'H': DoughCrate,
+            'W': Window,
+            '#': EmptyTable,
+            'D': DishWasher,
+            '.': FloorCell,
+            'O': Oven,
+            'NONE': None,
+            'DISH': Dish,
+            'ICE_CREAM': IceCream,
+            'BLUEBERRIES': Blueberries,
+            'STRAWBERRIES': Strawberries,
+            'CHOPPED_STRAWBERRIES': ChoppedStrawberries,
+            'DOUGH': Dough,
+            'CROISSANT': Croissant
+        }
+
+# --- input vars
+
+num_all_customers = int(input())
+all_customers = [Customer(*input().split()) for _ in range(num_all_customers)]
+
+grid = Grid([[match[c] for c in input()] for i in range(7)])
+
+log(f"{num_all_customers} customers: {all_customers}")
+log(f"grid: {grid}")
+
+player = Cooker()
+partner = Cooker()
+oven = next((i for i in grid.items() if type(i) is Oven), Oven())
+
+while True:
+    # <--- turn input
+    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.update(*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 awarded
+    if not player.order:
+        player.eval_orders(customers)
+        log('>>> new order taken')
+
+    log(f"order: {player.order}")
+
+    todo = player.todo()
+    log(f"todo: {todo}")
+    
+
+    priorities = [p for p in [
+            # order fulfilled: deliver
+            next((a for a in todo if type(a) is Deliver), None),
+            
+            # something to take out from the oven!
+            next((a for a in todo if type(a) is GetCookedDessert), None),
+            
+            # If cook has an ingredient in hands, he needs to prepare it
+            next((a for a in todo if type(a) is Transform), None),
+            
+            # If cook has an cook_ingredient in hands, he needs to cook it
+            next((a for a in todo if type(a) is Cook), None),
+            
+            # If hands are free and an ingredient is needed, we go for it first
+            next((a for a in todo if a.needs_hands and player.hands_free), None),
+            
+            # there is a dish waiting on a table
+            next((a for a in todo if type(a) is GetDishOnTable), None),
+            
+            # cook has a dessert in its hands and no dish, he have to take one
+            next((a for a in todo if isinstance(a, GetDish) and any(h for h in player.in_hand if issubclass(h, BaseDessert))), None),
+        ] if p is not None]
+    
+    if priorities:
+        next_task = priorities[0]
+        log(f"next task (priority): {next_task}")
+    else:
+        # else, go for the closest task
+        tasks = sorted(todo, key=lambda x: x.dist)
+        next_task = next(iter(tasks))
+        log(f"next task (closest): {next_task}")
+ 
+    # <--- Update moving costs
+    # 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 isinstance(grid.at(*c), SpecialLocation))) else 1
+        grid.add_costs[(x, y)] = k1 * k2 * 3
+    log(grid.add_costs)
+    # --->
+ 
+    # <--- compute shortest path
+    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.act(next_task)
+    else:
+        player.act(next_task)

+ 109 - 0
code_a_la_mode/recurse_try.py

@@ -0,0 +1,109 @@
+'''
+Created on 17 mars 2019
+
+@author: olinox
+'''
+class Base(): pass
+
+
+
+class Location(Base):
+    name = ""
+    passable = False
+
+class SpecialLocation(Location):
+    pass
+class DishWasher(SpecialLocation):
+    pass
+class IcecreamCrate(SpecialLocation):
+    pass
+class BlueberriesCrate(SpecialLocation):
+    pass
+class StrawberriesCrate(SpecialLocation):
+    pass
+class DoughCrate(SpecialLocation):
+    pass
+class ChoppingBoard(SpecialLocation):
+    pass
+
+
+
+class Recipe(Base):
+    location = None
+    cooking_time = 0
+    requirements = []
+    needs_free_hands = False
+    
+    def available(self):
+        return []
+    
+    def get(self):
+        
+        pass
+        
+#         if available:
+#             # get it
+#         else:
+#             # does it have requirements?
+#             # if it has requirements that we don't hold: 
+#             #   recurse on those.
+#             
+#             # if it has requirements that we hold:
+#             # does it have a cooking time?
+#             
+#             # if yes: cook it
+#             # if no: go to self.location
+        
+        
+class FinalProduct(Recipe):
+    needs_dish = True
+    
+class Ingredient(Recipe):
+    needs_free_hands = True    
+
+class Dish(Recipe):
+    location = DishWasher
+
+class IceCream(FinalProduct):
+    location = IcecreamCrate
+
+class Blueberries(FinalProduct):
+    location = BlueberriesCrate
+    
+class Strawberries(Ingredient):
+    location = StrawberriesCrate
+    needs_free_hands = True
+    
+class ChoppedStrawberries(FinalProduct):
+    location = ChoppingBoard
+    requirements = [Strawberries]
+    
+class Dough(Ingredient):
+    location = DoughCrate
+    needs_free_hands = True
+
+class Croissant(FinalProduct):
+    requirements = [Dough]
+    cooking_time = 10
+    
+class ChoppedDough(Ingredient):
+    requirements = [Dough]
+    location = ChoppingBoard
+    
+class RawTart(Ingredient):
+    ingredients = [ChoppedDough, Blueberries]
+    
+class Tart(FinalProduct):
+    ingredients = [RawTart]
+    cooking_time = 10
+
+
+
+class ExampleOrder():
+    elements = [Dish, IceCream, ChoppedStrawberries, Croissant]
+    
+    def available(self):
+        # special
+        return []
+    
+