Forráskód Böngészése

refact the acquiring phase

olinox 6 éve
szülő
commit
77d4c27f53
1 módosított fájl, 866 hozzáadás és 877 törlés
  1. 866 877
      carribean/script.py

+ 866 - 877
carribean/script.py

@@ -1,877 +1,866 @@
-'''
-
-@author: olivier.massot, 2019
-'''
-import heapq
-import sys
-
-
-# TODO:
-# * add an esquive manoeuvre / try to avoid cannonballs
-# * 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 Queue():
-    def __init__(self):
-        self.items = []
-
-    def put(self, item, priority):
-        heapq.heappush(self.items, (priority, item))
-
-    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 __bool__(self):
-        return bool(self.items)
-    
-    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):
-    pass
-
-    
-class Base():
-    def __repr__(self):
-        return f"<{self.__class__.__name__}: {self.__dict__}>"
-
-class Objective(Base):
-    def __init__(self, ship, target):
-        self.ship = ship
-        self.target = target
-        self.interest = 0
-
-    def __lt__(self, other):
-        return self.interest < other.interest
-
-    def __repr__(self):
-        return f"<{self.__class__.__name__}({self.target.id})>"
-
-    def update_interest(self, from_= None):
-        # the lower the better
-        if from_ is None:
-            from_ = self.ship.next_pos
-        self.interest = Grid.manhattan(from_, self.target.pos)
-
-class GetBarrel(Objective):
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.distance = 0
-        self.alignment = 0
-        
-    def update_interest(self, from_= None, orientation=None):
-        if from_ is None:
-            from_ = self.ship.next_pos
-        if orientation is None:
-            orientation = self.ship.orientation
-        distance = Grid.manhattan(from_, self.target.pos)
-        alignment = abs(Grid.diff_directions(Grid.direction_to(*from_, *self.target.pos), orientation))
-        
-        self.interest = 6 * distance + 9 * alignment + 3 * self.target.dispersal + self.target.mine_threat ** 2 - 36 * self.target.ennemy_near
-
-class Attack(Objective):
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.distance = 0
-        self.alignment = 0
-        
-    def update_interest(self, from_= None, orientation=None):
-        if from_ is None:
-            from_ = self.ship.next_pos
-        if orientation is None:
-            orientation = self.ship.orientation
-        distance = Grid.manhattan(from_, self.target.next_pos)
-        alignment = abs(Grid.diff_directions(Grid.direction_to(*from_, *self.target.next_pos), orientation))
-        
-        self.interest = 7 * distance + 3 * alignment - 20 * self.target.blocked_since - 10 * self.target.same_traject_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):
-    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_eval_barrels(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))
-            b.ennemy_near = any(b.pos in e.next_area for e in self.ennemy_ships)
-
-    def eval_objectives(self):
-        objectives = {GetBarrel: {}, Attack: {}}
-        
-        for b in self.barrels:
-            for s in self.owned_ships:
-                obj = GetBarrel(s, b)
-                obj.update_interest()
-                if not s in objectives[GetBarrel]:
-                    objectives[GetBarrel][s] = ObjectivesQueue()
-                objectives[GetBarrel][s].put(obj)
-                
-        for s in self.owned_ships:
-            objectives[Attack][s] = ObjectivesQueue()
-            for e in self.ennemy_ships:
-                obj = Attack(s, e)
-                obj.update_interest()
-                objectives[Attack][s].put(obj)
-                
-        return objectives
-    
-    def update_moving_costs(self):
-        base_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):
-                    base_costs[(x, y)] = 15 # borders are a little more expensive
-                elif x in (-1, self.w + 1) or y in (-1, self.h + 1):
-                    base_costs[(x, y)] = 1000 # out of the map
-                else:
-                    base_costs[(x, y)] = 10 # base moving cost
-        
-        for m in self.mines:
-            for n in self.neighbors(*m.pos):
-                base_costs[n] += 30
-        for m in self.mines:
-            base_costs[m.pos] += 1000
-        for c in self.cannonballs:
-            base_costs[c.pos] += (100 + (5 - c.countdown) * 200)
-        
-        for ship in self.ships:
-            ship._moving_costs = base_costs
-            for other in self.ships:
-                if other is ship:
-                    continue
-                dist = self.manhattan(ship.pos, other.pos)
-                if dist > 8:
-                    continue
-                for c in self.neighbors(*other.pos):
-                    ship._moving_costs[c] += 100 * abs(3 - other.speed)
-                for c in self.zone(other.next_pos, 4):
-                    ship._moving_costs[c] += 20
-
-    def shooting_spot(self, ship, target):
-        shooting_spots = Queue()
-        target_pos = target.next_pos if type(target) is Ship else target.pos
-        
-        for x, y in self.zone(target_pos, 10):
-            if ship.moving_cost(x, y) > 10:
-                continue
-            if self.manhattan((x, y), target_pos) < 2:
-                continue
-            
-            interest = 0 # the lower the better
-            
-            # avoid cells too close from borders
-            if not (3 <= x <= (self.w - 3) and 3 <= y < (self.h - 3)):
-                interest += 10
-            
-            # priorize spots at distance 5 from active ship
-            interest -= 10 * abs(5 - self.manhattan((x, y), ship.pos))
-            
-            shooting_spots.put((x, y), interest)
-
-        return shooting_spots.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) 
-
-    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, orientat0, target, moving_costs={}, incl_start=False, limit=10000):
-        nodes = Queue()
-        break_on, iteration = limit, 0
-        
-        
-        origin = PathNode(*origin)
-        origin.orientation = orientat0
-        
-        nodes.put(origin, 0)
-        neighbors = []
-
-        while nodes:
-            current = nodes.get()
-
-            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 = 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(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
-                nodes.put(node, priority)
-        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)
-    
-    def __lt__(self, other):
-        # default comparison, used to avoid errors when used with queues and priorities are equals
-        return self.id < other.id
-    
-    
-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._moving_costs = {}
-        
-        self.target = None
-        self.second_target = 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}>"
-
-    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.objective = None
-        self.objective_next = None
-        self.objective_fallback = 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_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)
-
-    def moving_cost(self, x, y):
-        return self._moving_costs[(x, y)]
-    
-    def move(self, *args, **kwargs):
-        try:
-            self._move(*args, **kwargs)
-            return True
-        except DidNotAct:
-            return False
-
-    def _move(self, path):
-        
-        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 self.moving_cost(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.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 = Grid()
-
-
-while True:
-    seen = []
-    current_turn += 1
-    
-    # <--- get input
-    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}
-    # --->
-    
-    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}")
-
-
-    ### Evaluate
-    grid.pre_eval_barrels()
-    
-    objectives = grid.eval_objectives()
-    
-    grid.update_moving_costs()
-    
-    ### Acquire
-    log("# Acquiring")
-    
-    if objectives[GetBarrel]:
-        # objectives are shared between allies by priority
-        merged_objs = ObjectivesQueue.merge(*[obj_lst for ship, obj_lst in objectives[GetBarrel].items() if ship.owned])
-        aimed = []
-        
-        # first objective
-        obj = merged_objs.get()
-        while not all(s.objective for s in grid.owned_ships):
-            if not obj.ship.objective and not obj.target in aimed:
-                obj.ship.objective = obj
-                aimed.append(obj.target)
-            if not merged_objs:
-                break
-            obj = merged_objs.get()
-            
-        del merged_objs, aimed
-
-    if objectives[Attack]:
-        for ship in grid.owned_ships:
-            ship.objective_fallback = objectives[Attack][ship].get()
-    else:
-        log("ERROR: no objectives")
-    
-    for ship in grid.owned_ships:
-        log(f"Ship {ship.id}: obj: {ship.objective}; next: {ship.objective_next}; fallback: {ship.objective_fallback}")
-    
-    ### Plan
-    log("# Planning")
-    
-    for ship in grid.owned_ships:
-        
-        log(f"---- ship {ship.id} ---")
-        log(f"ship: {ship}")
-        
-        if ship.objective:
-            goto = ship.objective.target.pos
-        elif ship.objective_fallback:
-            goto = grid.shooting_spot(ship, ship.objective_fallback.target)
-        else:
-            log("ERROR: No target")
-            continue
-        
-        log(f"goto: {goto}")
-        
-        ship.path = grid.path(ship.next_pos, ship.orientation, goto, ship._moving_costs, limit=6000 // len(grid.owned_ships))
-        
-        if ship.objective_next and ship.path:
-            ship.path += grid.path(goto, 
-                                   ship.path[-1].orientation, 
-                                   ship.objective_next.target.pos, 
-                                   ship._moving_costs,
-                                   limit=6000 // len(grid.owned_ships)) or []
-        
-        log(f"path: {ship.path}")
-        
-    ### Process
-    log("# Processing")
-    
-    for ship in grid.owned_ships:
-        if not ship.objective and not ship.objective_fallback:
-            log("No target: wait")
-            ship.wait()
-        
-        if ship.move(ship.path):
-            continue
-        
-        # no movement was required, can fire
-        if ship.fire_at_will(ship.objective_fallback.target, allies=grid.owned_ships):
-            continue
-        
-        log("ERROR: Did not act, wait")
-        ship.wait()
+'''
+
+@author: olivier.massot, 2019
+'''
+import heapq
+import sys
+
+
+# TODO:
+# * add an esquive manoeuvre / try to avoid cannonballs
+# * 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
+# * compute the probabilities of presence of an ennemy on dirrefent coords at next turn
+
+debug = True
+
+def log(*msg):
+    if debug:
+        print(*msg, file=sys.stderr)
+
+current_turn = 0
+
+class DidNotAct(Exception):
+    pass
+
+class Queue():
+    def __init__(self):
+        self.items = []
+
+    def put(self, item, priority):
+        heapq.heappush(self.items, (priority, item))
+
+    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 __bool__(self):
+        return bool(self.items)
+    
+    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):
+    pass
+
+    
+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__}({self.target.id})>"
+
+    def 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
+        self._compute_interest()
+        
+    def _compute_interest(self):
+        self.interest = 7 * self.distance + 3 * self.alignment
+        
+class GetBarrel(BaseObjective):
+    def _compute_interest(self):
+        self.interest = 6 * self.distance + 9 * self.alignment + 3 * self.target.dispersal + self.target.mine_threat ** 2 - 36 * self.target.ennemy_near
+
+class Attack(BaseObjective):
+    def _compute_interest(self):
+        self.interest = 7 * self.distance + 3 * self.alignment - 20 * self.target.blocked_since - 10 * self.target.same_traject_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):
+    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)
+                
+        for s in self.owned_ships:
+            s.allies = [other for other in self.owned_ships if other is not s]
+                
+        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))
+            b.ennemy_near = any(b.pos in e.next_area for e in self.ennemy_ships)
+                
+        for s in self.owned_ships:
+            
+            for b in self.barrels:
+                obj = GetBarrel(b)
+                obj.eval(s.next_pos, s.orientation)
+                s.objectives.put(obj)
+                
+            for e in self.ennemy_ships:
+                obj = Attack(e)
+                obj.eval(s.next_pos, s.orientation)
+                s.ennemies.put(obj)
+
+
+    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 update_moving_costs(self):
+        base_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):
+                    base_costs[(x, y)] = 15 # borders are a little more expensive
+                elif x in (-1, self.w + 1) or y in (-1, self.h + 1):
+                    base_costs[(x, y)] = 1000 # out of the map
+                else:
+                    base_costs[(x, y)] = 10 # base moving cost
+        
+        for m in self.mines:
+            for n in self.neighbors(*m.pos):
+                base_costs[n] += 30
+        for m in self.mines:
+            base_costs[m.pos] += 1000
+        for c in self.cannonballs:
+            base_costs[c.pos] += (100 + (5 - c.countdown) * 200)
+        
+        for ship in self.ships:
+            ship._moving_costs = base_costs
+            for other in self.ships:
+                if other is ship:
+                    continue
+                dist = self.manhattan(ship.pos, other.pos)
+                if dist > 8:
+                    continue
+                for c in self.neighbors(*other.pos):
+                    ship._moving_costs[c] += 100 * abs(3 - other.speed)
+                for c in self.zone(other.next_pos, 4):
+                    ship._moving_costs[c] += 20
+
+    def shooting_spot(self, ship, target):
+        shooting_spots = Queue()
+        target_pos = target.next_pos if type(target) is Ship else target.pos
+        
+        for x, y in self.zone(target_pos, 10):
+            if ship.moving_cost(x, y) > 10:
+                continue
+            if self.manhattan((x, y), target_pos) < 2:
+                continue
+            
+            interest = 0 # the lower the better
+            
+            # avoid cells too close from borders
+            if not (3 <= x <= (self.w - 3) and 3 <= y < (self.h - 3)):
+                interest += 10
+            
+            # priorize spots at distance 5 from active ship
+            interest -= 10 * abs(5 - self.manhattan((x, y), ship.pos))
+            
+            shooting_spots.put((x, y), interest)
+
+        return shooting_spots.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) 
+
+    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, orientat0, target, moving_costs={}, incl_start=False, limit=10000):
+        nodes = Queue()
+        break_on, iteration = limit, 0
+        
+        
+        origin = PathNode(*origin)
+        origin.orientation = orientat0
+        
+        nodes.put(origin, 0)
+        neighbors = []
+
+        while nodes:
+            current = nodes.get()
+
+            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 = 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(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
+                nodes.put(node, priority)
+        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)
+    
+    def __lt__(self, other):
+        # default comparison, used to avoid errors when used with queues and priorities are equals
+        return self.id < other.id
+    
+    
+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.allies = []
+        self._moving_costs = {}
+        
+        self.objectives = ObjectivesQueue()
+        self.ennemies = ObjectivesQueue()
+        
+        self.objective = None
+        self.objective_next = None
+        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}>"
+
+    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.objective_next = None
+        self.target_ennemy = 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_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)
+
+    def moving_cost(self, x, y):
+        return self._moving_costs[(x, y)]
+    
+    def move(self, *args, **kwargs):
+        try:
+            self._move(*args, **kwargs)
+            return True
+        except DidNotAct:
+            return False
+
+    def _move(self, path):
+        
+        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 self.moving_cost(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.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 = Grid()
+
+
+
+### *** Main Loop ***
+
+while True:
+    seen = []
+    current_turn += 1
+    
+    # <--- get input
+    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}
+    # --->
+    
+    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}")
+
+
+    ### Evaluate
+    grid.update_moving_costs()
+    
+    ### 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):
+                    s.objective = o
+            
+        except IndexError:
+            break
+        
+    # after_that objective
+    for s in grid.owned_ships:
+        if not s.objective: 
+            continue
+        after_that = ObjectivesQueue()
+        for b in grid.barrels:
+            obj = GetBarrel(b)
+            obj.eval(s.objective.target.pos)
+            after_that.put(obj)
+        if after_that:
+            s.objective_next = after_that.get()
+    
+    # targetted ennemy
+    for s in grid.owned_ships:
+        s.target_ennemy = s.ennemies.get()
+        
+    for ship in grid.owned_ships:
+        log(f"Ship {ship.id}: obj: {ship.objective}; next: {ship.objective_next}; target: {ship.target_ennemy}")
+    
+    ### Plan
+    log("# Planning")
+    
+    for ship in grid.owned_ships:
+        
+        log(f"---- ship {ship.id} ---")
+        log(f"ship: {ship}")
+        
+        if ship.objective:
+            goto = ship.objective.target.pos
+        elif ship.target_ennemy:
+            goto = grid.shooting_spot(ship, ship.target_ennemy.target)
+        else:
+            log("ERROR: No target")
+            continue
+        
+        log(f"goto: {goto}")
+        
+        ship.path = grid.path(ship.next_pos, ship.orientation, goto, ship._moving_costs, limit=6000 // len(grid.owned_ships))
+        
+        if ship.objective_next and ship.path:
+            ship.path += grid.path(goto, 
+                                   ship.path[-1].orientation, 
+                                   ship.objective_next.target.pos, 
+                                   ship._moving_costs,
+                                   limit=6000 // len(grid.owned_ships)) or []
+        
+        log(f"path: {ship.path}")
+        
+    ### Process
+    log("# Processing")
+    
+    for ship in grid.owned_ships:
+        if not ship.objective and not ship.target_ennemy:
+            log("No target: wait")
+            ship.wait()
+        
+        if ship.move(ship.path):
+            continue
+        
+        # no movement was required, can fire
+        if ship.fire_at_will(ship.target_ennemy.target, allies=grid.owned_ships):
+            continue
+        
+        log("ERROR: Did not act, wait")
+        ship.wait()