Selaa lähdekoodia

big refactoring

olinox 6 vuotta sitten
vanhempi
commit
824fd122e7
1 muutettua tiedostoa jossa 448 lisäystä ja 240 poistoa
  1. 448 240
      i_n_f/script.py

+ 448 - 240
i_n_f/script.py

@@ -2,18 +2,14 @@
 >> https://www.codingame.com/ide/173171838252e7c6fd6f3ff9cb8169431a08eec1
 @author: olivier.massot, may 2019
 '''
+from collections import Counter
 import heapq
 import sys
 import time
 
 # TODO
-# x update the grid state after each action
-# * improve the conditions for training
-# * estimate the threat level of ennemies (proximity to HQ or to a pivot, unprotected cells around or inferior units)
-# * use a propagation loop to estimate the weight of a pivot (both opponents and owned)
-# * modelize fronts
-# * moving part: order units action order by proximity to an ennemy unit / building
-# * add the 'lasso' technique
+# * do not train a unit of a cell if it is in a threatened pivot zone, especially not a level 3 unit!
+
 
 debug = True
 t0 = time.time()
@@ -65,13 +61,22 @@ class InterestQueue(Queue):
     def get(self):
         return heapq.heappop(self.items)
 
-class Action(Base):
-    def __init__(self, target, *args, **kwargs):
+class BasePosition(Base):
+    def __init__(self, cell, *args, **kwargs):
         self.interest = 0
-        self.target = target
-        self.x, self.y = target.pos
+        self.cell = cell
+        
+    @property
+    def x(self):
+        return self.cell.x
+        
+    @property
+    def y(self):
+        return self.cell.y
         
-        self.eval()
+    @property
+    def pos(self):
+        return self.cell.pos
         
     def __lt__(self, other):
         return self.interest < other.interest
@@ -79,122 +84,110 @@ class Action(Base):
     def eval(self):
         raise NotImplementedError
         
-class Move(Action):
-    def __init__(self, target, unit):
-        self.unit = unit
+        
+class Position(BasePosition):
+    def __init__(self, cell):
+        super().__init__(cell)
         
         self.possession = 0
-        self.pivot = False
-        self.prey_level = 0
+        self.threat = 0
+        self.strategic_value = 0
+        self.pivot = 0
         self.union = 0
         self.depth = 0
-        self.hq = False
-        self.tower = False
-        self.mine = False
+        self.hq = 0
+        self.tower = 0
+        self.mine = 0
         self.dist_to_goal = 0
         
-        super().__init__(target, unit)
+        self.min_level = 1
         
-    def eval(self):
-        # the lower the better
+    def pre_eval(self):
+        
+        # *** Eval interest: the lower the better
         
         self.interest = 0
+        self.min_level = 1
         
-        if self.target.active_owned:
+        # eval possession
+        if self.cell.active_owned:
             self.possession = 1
-        elif self.target.active_opponent:
-            self.possession = -1
             
-        self.pivot = self.target.pivot
+        elif self.cell.active_opponent:
+            self.possession = -1
+
+        # eval threat
+        self.threat = 0
+        if self.cell.active_owned:
+            self.threat = self.cell.threat
 
-        self.dist_to_goal = Grid.manhattan(self.target.pos, opponent.hq.pos)
+        # eval strategic value
+        self.strategic_value = self.cell.strategic_value
+        
+        # covers (towers only)
+        self.covers = self.strategic_value + sum([grid[n].strategic_value for n in self.cell.neighbors])
         
-        # an ennemy here
-        if self.target.unit and self.target.unit.opponents:
-            if self.target.unit.level < self.unit.level or self.unit.level == 3:
-                self.prey_level = self.target.unit.level
+        # eval pivot
+        self.pivot = sum([1 + grid[p].get_unit_level() for p in self.cell.pivot_for])
+
+        # distance to the ennemy HQ
+        self.dist_to_goal = Grid.manhattan(self.cell.pos, opponent.hq.pos)
         
-        # priorize adjacent cells 
-        self.union = len([n for n in self.target.neighbors if grid[n].active_owned])
+        # priorize adjacent cells
+        self.union = len([n for n in self.cell.neighbors if grid[n].active_owned])
         
         # include 'depthmap'
-        self.depth = self.target.depth
+        self.depth = self.cell.depth
         
         # priorize mines or HQ
-        if self.target.building and self.target.building.opponents:
-            self.hq = self.target.building.type_ == Building.HQ
-            self.tower = self.target.building.type_ == Building.TOWER and self.unit.level == 3
-            self.mine = self.target.building.type_ == Building.MINE
-        
-        self.interest = 15 * self.possession - 10 * self.pivot - 10 * self.prey_level \
-                        - 2 * self.union + 3 * self.depth + self.dist_to_goal \
-                        - 30 * self.tower - 15 * self.mine \
-                        - 100 * self.hq
-
-    def __repr__(self):
-        return f"<{self.__class__.__name__} ({self.x}, {self.y}), {self.unit.id_}: {self.interest} (" \
-                f"{self.possession} / {self.pivot} / {self.prey_level} / {self.union} / {self.dist_to_goal}" \
-                f" / {self.depth} / {self.tower} / {self.mine} / {self.hq})>"
+        if self.cell.building:
+            self.hq = int(self.cell.building.type_ == Building.HQ)
+            self.tower = int(self.cell.building.type_ == Building.TOWER)
+            self.mine = int(self.cell.building.type_ == Building.MINE)
         
-    def resolution(self):
-        return f"MOVE {self.unit.id_} {self.target.x} {self.target.y}"
-        
-class Train(Move):
-    def __init__(self, target, level=1):
-        self.level = level
-    
-        self.possession = 0
-        self.pivot = False
-        self.prey_level = 0
-        self.union = 0
-        self.depth = 0
-        self.hq = False
-        self.tower = False
-        self.mine = False
-        self.dist_to_goal = 0
-        
-        super().__init__(target, level)
+        # *** Min level to go there
+        if self.cell.unit and self.cell.unit.opponents:
+            self.min_level = min([self.cell.unit.level + 1, 3])
 
-    
-    def eval(self):
-        # the lower the better
-        self.interest = 0
-        
-        if self.target.active_owned:
-            self.possession = 1
-        elif self.target.active_opponent:
-            self.possession = -1
-            
-        self.pivot = self.target.pivot
+        if self.cell.under_tower:
+            self.min_level = 3
 
-        self.dist_to_goal = Grid.manhattan(self.target.pos, opponent.hq.pos)
+    def eval(self):
+        self.pre_eval()
+        self.interest = 3 * self.depth + self.dist_to_goal
 
-        # an ennemy here
-        if self.target.unit and self.target.unit.opponents:
-            if self.target.unit.level < self.level or self.level == 3:
-                self.prey_level = self.target.unit.level
+    def __repr__(self):
+        detail = [self.possession, self.threat, self.strategic_value, self.pivot, self.dist_to_goal,
+                  self.union, self.depth, self.hq, self.tower, self.mine]
+        return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
         
-        # priorize adjacent cells 
-        self.union = len([n for n in self.target.neighbors if grid[n].active_owned])
         
-        # include 'depthmap'
-        self.depth = self.target.depth
-
-        # priorize mines or HQ
-        if self.target.building and self.target.building.opponents:
-            self.hq = self.target.building.type_ == Building.HQ
-            self.tower = self.target.building.type_ == Building.TOWER and self.level == 3
-            self.mine = self.target.building.type_ == Building.MINE
-        
-        self.interest = 15 * self.possession - 10 * self.pivot - 10 * self.prey_level \
-                        - 2 * self.union + 3 * self.depth + self.dist_to_goal \
-                        - 30 * self.tower - 15 * self.mine \
+class Defend(Position):
+    def __init__(self, cell, emergency = False):
+        super().__init__(cell)
+        self.emergency = emergency
+    
+    def eval(self):
+        self.pre_eval()
+        self.interest = 100 \
+                        - 10 * self.threat\
+                        - self.covers // 5 \
+                        - 10 * self.pivot \
+                        - 50 * self.emergency
+        
+class Attack(Position):
+    def eval(self):
+        self.pre_eval()
+        self.interest = 15 * self.possession \
+                        - 5 * self.pivot \
+                        - 2 * self.union \
+                        + 3 * self.depth \
+                        + self.dist_to_goal \
+                        - 30 * self.tower \
+                        - 15 * self.mine \
                         - 100 * self.hq
-
-    def resolution(self):
-        return f"TRAIN {self.level} {self.target.x} {self.target.y}"
         
-class Build(Action):
+class MinePosition(BasePosition):
     def __init__(self, target, type_):
         self.type_ = type_
         super().__init__(target, type_)
@@ -209,11 +202,7 @@ class Build(Action):
         elif self.type_ == Building.TOWER:
             if self.target.pivot:
                 self.interest -= 20
-        
-    def resolution(self):
-        str_type = {1: "MINE", 2: "TOWER"}[self.type_]
-        return f"BUILD {str_type} {self.target.x} {self.target.y}"
-        
+
 class BaseLoc(Base):
     def __init__(self, x, y):
         self.x = x
@@ -256,6 +245,8 @@ class Building(BaseOwnedLoc):
     def hq(self):
         return self.type_ == Building.HQ
 
+
+
 class Unit(BaseOwnedLoc):
     cost = {1: 10, 2: 20, 3: 30}
     maintenance = {1: 1, 2: 4, 3: 20}
@@ -263,6 +254,8 @@ class Unit(BaseOwnedLoc):
         super().__init__(x, y, owner)
         self.id_ = id_
         self.level = level
+        
+        self.has_moved = False
 
 class Player(Base):
     def __init__(self, id_):
@@ -272,6 +265,9 @@ class Player(Base):
         self.units = []
         self.buildings = []
         self.hq = None
+        
+        self.spent = 0
+        self.new_charges = 0
 
     def update(self, gold, income, units, buildings):
         self.gold = gold
@@ -280,6 +276,29 @@ class Player(Base):
         self.buildings = [b for b in buildings if b.owner == self.id_]
         self.hq = next((b for b in self.buildings if b.type_ == HQ))
 
+        self.spent = 0
+        self.new_charges = 0
+
+    @property
+    def current_gold(self):
+        return self.gold - self.spent
+
+    @property
+    def current_income(self):
+        return self.income - self.new_charges
+
+    def training_capacity(self):
+        return min([(self.gold - self.spent) // Unit.cost[1], (self.income - self.new_charges) // Unit.maintenance[1]])
+
+    def max_affordable(self):
+        for lvl in range(3, 0, -1):
+            if (self.gold - self.spent) >= Unit.cost[lvl] and (self.income - self.new_charges) >= Unit.maintenance[lvl]:
+                return lvl
+        return 0
+
+    def can_act(self):
+        return self.training_capacity() > 0 or self.gold >= 15 or any(not unit.has_moved for unit in self.units)
+
 class Cell(Base):
     def __init__(self, x, y):
         self.x = x
@@ -292,13 +311,13 @@ class Cell(Base):
         
         self.under_tower = False
         self.depth = 0
-        self.pivot = False
-        self.needs_unit = False
+        self.pivot_for = []
+        self.strategic_value = 0
+        self.threat = 0
         
         # front cells
         self.facing = []
         self.support = []
-        self.threat = 0
         self.in_front_of = []
         
     @property
@@ -316,39 +335,14 @@ class Cell(Base):
         
         self.under_tower = False
         self.depth = 0
-        self.pivot = False
-        self.needs_unit = False
+        self.pivot_for = []
+        self.strategic_value = 0
+        self.threat = 0
     
         self.facing = []
         self.support = []
-        self.threat = 0
         self.in_front_of = []
     
-    def update_threat(self):
-        
-        self.threat = 0
-        
-        for n in self.neighbors:
-            ncell = grid[n]
-            if ncell.active_owned:
-                self.support.append(ncell)
-            else:
-                ncell.in_front_of.append(self)
-                self.facing.append(ncell)
-                if ncell.opponents:
-                    self.threat -= 1
-        
-        if self.unit:
-            self.threat -= 2 * self.unit.level
-        
-        for cell in self.support:
-            if cell.unit:
-                self.threat -= cell.unit.level
-                
-        for cell in self.facing:
-            if cell.unit:
-                self.threat += 2 * cell.unit.level
-    
     @property
     def movable(self):
         return self._content != "#"
@@ -363,8 +357,13 @@ class Cell(Base):
     
     @property
     def owner(self):
-        return ME if self.owned else OPPONENT
-    
+        if self.owned:
+            return ME
+        elif self.opponents:
+            return OPPONENT
+        else:
+            return None
+        
     @property
     def headquarter(self):
         return self.pos in Grid.hqs
@@ -396,7 +395,17 @@ class Cell(Base):
     def take_possession(self):
         self._content = "O"
     
-
+    def desactivate(self):
+        self._content = self._content.lower()
+    
+    def get_unit_level(self):
+        return self.unit.level if self.unit else 0
+    
+class Node(Base):
+    def __init__(self, pos, path=[]):
+        self.pos = pos
+        self.path = path
+    
 class Grid(Base):
     dim = 12
     hqs = [(0,0), (11,11)]
@@ -411,6 +420,7 @@ class Grid(Base):
         self.buildings = []
         for m in mines_sites:
             self.cells[(m.x, m.y)].mine_site = m
+        self.threat_level = 0
         
     def print_grid(self):
         return "\n".join(["".join([c for c in row]) for row in self.grid])
@@ -439,10 +449,14 @@ class Grid(Base):
                                           units_ix.get((x, y), None), 
                                           buildings_ix.get((x, y), None))
 
+        self.update_state()
+
+    def update_state(self):
         self.update_tower_areas()
         self.update_frontlines()
         self.update_depth_map()
         self.update_pivots()
+        self.update_threats()
 
     @staticmethod
     def manhattan(from_, to_):
@@ -456,8 +470,22 @@ class Grid(Base):
             neighs += [(x - 1, y - 1), (x + 1, y - 1), (x - 1, y + 1), (x + 1, y + 1)]
         return [(x, y) for x, y in neighs if 0 <= x < Grid.dim and 0 <= y < Grid.dim]
     
-    def get_hq(self, player):
-        return next((b for b in self.buildings if b.owner == player and b.hq))
+    @classmethod
+    def zone(cls, center, radius):
+        return [(x, y) for x in range(0, cls.dim) for y in range(0, cls.dim) if cls.manhattan(center, (x, y)) <= radius]      
+    
+    def zone_set(self, center, radius):
+        zone = set(center)
+        for _ in range(radius):
+            for p in zone:
+                zone |= self.neighbors(*p)
+
+    @staticmethod
+    def closest(from_, in_):
+        return min(in_, key=lambda x: Grid.manhattan(from_, x))
+    
+    def get_hq(self, player_id):
+        return next((b for b in self.buildings if b.owner == player_id and b.hq))
     
     def update_tower_areas(self):
         for b in self.buildings:
@@ -475,12 +503,8 @@ class Grid(Base):
             if cell.active_owned:
                 if any(self.cells[c].movable and not self.cells[c].active_owned
                        for c in cell.neighbors):
-                    cell.update_threat()
                     self.frontline.append(cell)
 
-        
-
-
     def update_depth_map(self):
         buffer = [c.pos for c in self.frontline]
         for p in buffer:
@@ -497,41 +521,119 @@ class Grid(Base):
             buffer = list(next_buffer)
             next_buffer = []
     
+    def _active_owned(self, pos, player_id):
+        c = self.cells[pos]
+        return c.owner == player_id and c.active
+    
     def update_pivot_for(self, player_id):
-#         for cell in self.cells.values():
-#             if cell.owner == player_id and \
-#                len([n for n in cell.neighbors if self.cells[n].owner == player_id]) == 2:
-#                 cell.pivot = True
-        pass
+        start = self.get_hq(player_id).pos
+        start_node = Node(start)
+        
+        buffer = [start_node]
+        nodes = {start_node}
+        ignored = set()
+        
+        while buffer:
+            new_buffer = []
+            for node in buffer:
+                neighbors = [p for p in self.neighbors(*node.pos) if self._active_owned(p, player_id)]
+                if len(neighbors) == 4:
+                    ignored.add(node.pos)
+                    continue
+                for n in neighbors:
+                    if not n in node.path:
+                        new_node = Node(n, node.path + [node.pos])
+                        nodes.add(new_node)
+                        new_buffer.append(new_node)
+            
+            buffer = new_buffer
+        
+        paths_to = {}
+        for node in nodes:
+            if not node.pos in paths_to:
+                paths_to[node.pos] = []
+            paths_to[node.pos].append(node.path)
     
+        pivots = {}
+        for candidate in paths_to:
+            if candidate == start:
+                continue
+            for p, paths in paths_to.items():
+                if not paths or not paths[0] or p in ignored:
+                    continue
+                if all(candidate in path for path in paths):
+                    if not candidate in pivots:
+                        pivots[candidate] = []
+                    pivots[candidate].append(p)
+        
+        for pivot, pivot_for in pivots.items():
+            self.cells[pivot].pivot_for = pivot_for
+            
+        occurrences = Counter(sum(sum(paths_to.values(), []), []))
+        
+        while ignored:
+            new_ignored = set()
+            for p in ignored:
+                occured_neighbors = [occurrences[n] for n in self.neighbors(*p) if n in occurrences]
+                if not occured_neighbors:
+                    new_ignored.add[p]
+                    continue
+                occurrences[p] = 2 * sum(occured_neighbors) // len(occured_neighbors)
+            ignored = new_ignored
+        
+        max_occ = max(occurrences.values()) if occurrences else 1
+        for p, occ in occurrences.items():
+            self.cells[p].strategic_value = (100 * occ) // max_occ
+            
+#         log(player_id, {c.pos: (c.strategic_value, len(c.pivot_for)) for c in self.cells.values() if c.owner == player_id})
+        
     def update_pivots(self):
         self.update_pivot_for(ME)
         self.update_pivot_for(OPPONENT)
     
-    def training_places(self):
-        owned, neighbors = {p for p, c in self.cells.items() if c.owned}, set()
+    def update_threats(self):
+        # 1 + max number of units opponents can produce in one turn
+        self.threat_level = 1 + opponent.training_capacity()
+        
+        ennemy_frontier = [c for c in self.cells.values() if c.opponents \
+                           and any(self.cells[n].movable and not self.cells[n].opponents for n in c.neighbors)]
+
+        for cell in self.cells.values():
+            if cell.owned:
+                threat = min([Grid.manhattan(cell.pos, o.pos) for o in ennemy_frontier])
+                cell.threat = self.threat_level - threat
+        
+        self.emergency = grid[player.hq.pos].threat > 0
+        
+#         log({c.pos: c.threat for c in self.cells.values() if c.owned and c.threat is not None})
+    
+    def influence_zone(self, player_id):
+        owned, neighbors = {p for p, c in self.cells.items() if c.owner == player_id and c.active}, set()
         for p in owned:
             neighbors |= set(self.neighbors(*p))
-        return (self.cells[p] for p in (owned | neighbors) if self.can_move(p))
+        return (self.cells[p] for p in (owned | neighbors) if self.cells[p].movable)
+               
+    def training_places(self):
+        return (c for c in self.influence_zone(ME) if self.can_move(c.pos))
     
     def get_next_training(self, max_level=3):
         q = InterestQueue()
         for cell in self.training_places():
-            q.put(Train(cell, max_level))
+            q.put(Position(cell))
         if not q:
             return None
         
         action = q.get()
     
         if max_level < 3: 
-            while action.target.under_tower:
+            while action.cell.under_tower:
                 try:
                     action = q.get()
                 except IndexError:
                     return None
         level = 1
         for ennemy in opponent.units:
-            if Grid.manhattan(action.target.pos, ennemy.pos) < 3:
+            if Grid.manhattan(action.cell.pos, ennemy.pos) < 3:
                 level = min(ennemy.level + 1, max_level)
                 break
                 
@@ -547,7 +649,7 @@ class Grid(Base):
         can_move &= not cell.owned_building()
         if level != 3:
             can_move &= (cell.unit is None or cell.unit.level < level)
-            can_move &= not cell.under_tower
+            can_move &= cell.owned or not cell.under_tower
         return can_move
     
     def moving_zone(self, unit):
@@ -557,15 +659,13 @@ class Grid(Base):
     def get_next_move(self, unit):
         q = InterestQueue()
         for cell in self.moving_zone(unit):
-            q.put(Move(cell, unit))
+            o = Position(cell)
+            o.eval()
+            q.put(o)
         if not q:
             return None
-        log(q.items)
-        action = q.get()
-        if action.interest > Move(self.cells[unit.pos], unit).interest:
-            # move has lower interest than staying there
-            return None
-        return action
+        objective = q.get()
+        return objective
     
     def building_zone(self, type_):
         if type_ == Building.MINE:
@@ -576,82 +676,164 @@ class Grid(Base):
     def get_building_site(self, type_):
         q = InterestQueue()
         for cell in self.building_zone(type_):
-            q.put(Build(cell, type_))
+            q.put(Position(cell, type_))
         if not q:
             return None
         return q.get()
 
+    def remove_unit_from(self, cell):
+        opponent.units.remove(cell.unit)
+        self.units.remove(cell.unit)
+        cell.unit = None
+
+    def cost_for_new_mine(self):
+        return Building.cost[Building.MINE] + 4 * len([b for b in player.buildings if b.type_ == Building.MINE])
+
     def apply(self, action):
         if type(action) is Move:
-            unit, new_cell = action.unit, action.target
-            old_cell = self.cells[unit.pos]
+            old_cell, new_cell = self.cells[action.unit.pos], action.cell
             
             if new_cell.unit:
                 if new_cell.unit.owned:
-                    log(f"ERROR: impossible {action}")
+                    log("ERROR: impossible move")
                     return
-                if unit.level < 3 and new_cell.unit.level >= unit.level:
-                    log(f"ERROR: impossible {action}")
+                if action.unit.level < 3 and new_cell.unit.level >= action.unit.level:
+                    log("ERROR: impossible move")
                     return
                 # cell is occupied by an opponent's unit with an inferior level
-                opponent.units.remove(new_cell.unit)
-                self.units.remove(new_cell.unit)
-                new_cell.unit = None
+                self.remove_unit_from(new_cell)
             
             if new_cell.building and new_cell.building.type_ == Building.TOWER:
-                if unit.level < 3:
-                    log(f"ERROR: impossible {action}")
+                if action.unit.level < 3:
+                    log("ERROR: impossible move")
                 opponent.buildings.remove(new_cell.building)
                 self.buildings.remove(new_cell.building)
                 new_cell.building = None
                 
             old_cell.unit = None
-            unit.x, unit.y = new_cell.pos
-            new_cell.unit = unit
+            action.unit.x, action.unit.y = new_cell.pos
+            new_cell.unit = action.unit
+            action.unit.has_moved = True
             
+            if new_cell.opponents:
+                for p in new_cell.pivot_for:
+                    self.cells[p].desactivate()
+                    if self.cells[p].unit and self.cells[p].unit.opponents:
+                        self.remove_unit_from(self.cells[p])
             new_cell.take_possession()
         
+            self.update_state()
+        
         elif type(action) is Train:
-            level, new_cell = action.level, action.target
-            unit = Unit(ME, None, level, *new_cell.pos)
+            new_cell = action.cell
+            unit = Unit(ME, None, action.level, *new_cell.pos)
+            unit.has_moved = True
+            
+            player.spent += Unit.cost[action.level]
+            player.new_charges += Unit.maintenance[action.level]
             
             if new_cell.unit:
                 if new_cell.unit.owned:
-                    log(f"ERROR: impossible {action}")
+                    log("ERROR: impossible training")
                     return
                 if unit.level < 3 and new_cell.unit.level >= unit.level:
-                    log(f"ERROR: impossible {action}")
+                    log("ERROR: impossible training")
                     return
                 # cell is occupied by an opponent's unit with an inferior level
-                opponent.units.remove(new_cell.unit)
-                self.units.remove(new_cell.unit)
-                new_cell.unit = None
+                self.remove_unit_from(new_cell)
             
             if new_cell.building and new_cell.building.type_ == Building.TOWER:
                 if unit.level < 3:
-                    log(f"ERROR: impossible {action}")
+                    log("ERROR: impossible training")
                 opponent.buildings.remove(new_cell.building)
                 self.buildings.remove(new_cell.building)
                 new_cell.building = None
-                
+            
             new_cell.unit = unit
+            if new_cell.opponents:
+                for p in new_cell.pivot_for:
+                    self.cells[p].desactivate()
+                    if self.cells[p].unit and self.cells[p].unit.opponents:
+                        self.remove_unit_from(self.cells[p])
             new_cell.take_possession()
-        
-        elif type(action) is Build:
-            type_, new_cell = action.type_, action.target
-            building = Building(ME, type_, *new_cell.pos)
+            
+            self.update_state()
+            
+        elif type(action) is BuildTower:
+            new_cell = action.cell
+            building = Building(ME, Building.TOWER, *new_cell.pos)
             new_cell.building = building
+            player.buildings.append(building)
+            
+            player.spent += Building.cost[Building.TOWER]
             
             if building.type_ == Building.TOWER:
                 new_cell.under_tower = True
                 for n in new_cell.neighbors:
                     self.cells[n].under_tower = True
-        
-        else: 
-            log(f"unknown action : {type(action)}")
 
+            self.update_state()
 
+        elif type(action) is BuildMine:
+            
+            player.spent += self.cost_for_new_mine()
+            
+            new_cell = action.cell
+            building = Building(ME, Building.MINE, *new_cell.pos)
+            new_cell.building = building
+            player.buildings.append(building)
+
+class Action(Base):
+    def command(self):
+        raise NotImplementedError()
+    
+class Wait(Action):
+    def command(self):
+        return "WAIT"
 
+class Move(Action):
+    def __init__(self, unit, cell):
+        self.unit = unit
+        self.cell = cell
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.unit.id_} {self.cell.pos}>"
+
+    def command(self):
+        return f"MOVE {self.unit.id_} {self.cell.x} {self.cell.y}"
+    
+class Train(Action):
+    def __init__(self, level, cell):
+        self.level = level
+        self.cell = cell
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.level} {self.cell.pos}>"
+    
+    def command(self):
+        return f"TRAIN {self.level} {self.cell.x} {self.cell.y}"
+
+class Build(Action):
+    str_types = {1: "MINE", 2: "TOWER"}
+    def __init__(self, type_, cell):
+        self.type_ = type_
+        self.cell = cell
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__}: {self.str_types[self.type_]} {self.cell.pos}>"
+    
+    def command(self):
+        return f"BUILD {self.str_types[self.type_]} {self.cell.x} {self.cell.y}"
+
+class BuildTower(Build):
+    def __init__(self, cell):
+        super().__init__(Building.TOWER, cell)
+
+class BuildMine(Build):
+    def __init__(self, cell):
+        super().__init__(Building.MINE, cell)
+        
+        
 # ******** MAIN *************
 
 test = False
@@ -710,71 +892,97 @@ while True:
     # --->
     
     # <--- update
-    grid.update(new_grid, buildings, units)
-#     log(f"grid:\n{grid.print_grid()}")
-    
     player.update(gold, income, units, buildings)
 #     log(f"player: {player}")
     
     opponent.update(opponent_gold, opponent_income, units, buildings)
 #     log(f"opponent: {opponent}")
+    
+    grid.update(new_grid, buildings, units)
+#     log(f"grid:\n{grid.print_grid()}")
     # --->
     
     commands = []
     
     # start
+    log(f"Threat level: {grid.threat_level}")
+    if grid.emergency:
+        log("<!> HQ is threaten!")
     
-    
-    has_move = []
-    for i in range(2):
-        log(f"# Moving (turn {i})")
-        for unit in player.units:
-            if unit in has_move:
-                continue
-            
-            action = grid.get_next_move(unit)
-            if action:
-                grid.apply(action)
-                has_move.append(unit)
-                commands.append(action.resolution())
-    
-    log("# Training")
-    spent, charges = 0, 0
-    
-    while player.income - charges > 0 and \
-          player.gold - spent > 10 and \
-          len(player.units) < len(grid.frontline):
+    todo = []
+    abandonned = []
+    while player.can_act():
         
-        max_level = next((i for i in range(3, 0, -1) if (player.gold - spent) > Unit.cost[i] and (player.income - charges) > Unit.maintenance[i]), 1)
+        q = InterestQueue()
+        for cell in grid.influence_zone(ME):
+            if cell.movable and not cell in abandonned:
+                if cell.owned and cell.threat > 0 and cell.pos != player.hq.pos:
+                    p = Defend(cell, grid.emergency)
+                elif not cell.owned:
+                    p = Attack(cell)
+                else:
+                    continue
+                p.eval()
+                q.put(p)
         
-        action = grid.get_next_training(max_level)
-        if action:
-            grid.apply(action)
-            spent += Unit.cost[action.level]
-            charges += Unit.maintenance[action.level]
-            commands.append(action.resolution())
-        else:
+        if not q:
             break
-    
-    log("# Building")
-    mine_cost = Building.cost[Building.MINE] + 4 * len([b for b in player.buildings if b.type_ == Building.MINE])
-    if (player.gold - spent) > mine_cost:
-        action = grid.get_building_site(Building.MINE)
-        if action:
-            spent += mine_cost
-            grid.apply(action)
-            commands.append(action.resolution())
+        
+        objective = q.get()
+        if type(objective) is Defend:
+            if player.current_gold > Building.cost[Building.TOWER]:
+                action = BuildTower(objective.cell)
+                
+            else:
+                # TODO: recruit units
+                abandonned.append(objective.cell)
+                continue
             
-    if player.gold - spent > Building.cost[Building.TOWER]:
-        action = grid.get_building_site(Building.TOWER)
+        elif type(objective) is Attack:
+            near_unit = next((grid[n].unit for n in objective.cell.neighbors if grid[n].unit \
+                                                                             and grid[n].unit.owned \
+                                                                             and not grid[n].unit.has_moved \
+                                                                             and grid[n].unit.level >= objective.min_level), 
+                              None)
+            if near_unit:
+                action = Move(near_unit, objective.cell)
+            else:
+                if objective.min_level > player.max_affordable():
+                    abandonned.append(objective.cell)
+                    continue
+                action = Train(objective.min_level, objective.cell)
+        
+        log(f"priority: {action} -> {objective}")
+        grid.apply(action)
+        todo.append(action)
+        
+    # units which did not move
+    for unit in player.units:
+        if not unit.has_moved:
+            objective = grid.get_next_move(unit)
+            if objective:
+                action = Move(unit, objective.cell)
+                log(f"default: {action}")
+                grid.apply(action)
+                todo.append(action)
+            else:
+                log(f"<!> No move available for {unit}")
+                
+    # can build mines?
+    if player.current_gold > grid.cost_for_new_mine():
+        action = BuildMine(grid.get_building_site(Building.MINE))
         if action:
-            spent += Building.cost[Building.TOWER]
+            log(f"default: {action}")
             grid.apply(action)
-            commands.append(action.resolution())
-            
+            todo.append(action)
+        
+    log(f"* todo: {todo}")
+    
+    commands = [action.command() for action in todo]
+
     if not commands:
         log("nothing to do: wait")
         commands = ["WAIT"]
         
-    log(commands)
+    log(f"* commands: {commands}")
     print(";".join(commands))