Browse Source

this is a lazy commit, sorry to myself

olinox 6 years ago
parent
commit
9eed0cb5ea
1 changed files with 268 additions and 110 deletions
  1. 268 110
      i_n_f/script.py

+ 268 - 110
i_n_f/script.py

@@ -2,18 +2,21 @@
 >> https://www.codingame.com/ide/173171838252e7c6fd6f3ff9cb8169431a08eec1
 @author: olivier.massot, may 2019
 '''
-from collections import Counter
 import heapq
 import sys
 import time
 
 # TODO
-# * when building a tower, check that there is not already one, and block the cell for moving
-# * do not train a unit of a cell if it is in a threatened pivot zone, especially not a level 3 unit!
+# * priorize attack on neighbors of pivots (allies'pivots to colonize and ennemies to threat)
+# * !!! moves: check before if unit is not on a zone which needs to be protected
+# x optimize moves, especially at start! (by introducing dispersion?)
 # * take units and towers in account when computing the threat
-# * take units and towers in account when computing the strategig value
-# * review the tower placement in case of threat
-
+# x priorize colonization of cells near HQ
+# ? when priority needs a lvl3 unit, find a way to spare
+# * resurrect the strategic value: number of cells depending of the cell
+# * consider defending also cells not owned
+# x limit the training of level 3 when it's really needed
+# * make a first turn of moves for units deep inside the territory, to avoid unecessary training and computing
 
 debug = True
 t0 = time.time()
@@ -43,6 +46,9 @@ class Queue(Base):
     def __repr__(self):
         return str(self.items)
 
+    def values(self):
+        return (v for _, v in self.items)
+
     def put(self, item, priority):
         heapq.heappush(self.items, (priority, item))
 
@@ -95,7 +101,6 @@ class Position(BasePosition):
         
         self.possession = 0
         self.threat = 0
-        self.strategic_value = 0
         self.pivot = 0
         self.union = 0
         self.depth = 0
@@ -125,11 +130,7 @@ class Position(BasePosition):
         if self.cell.active_owned:
             self.threat = self.cell.threat
 
-        # 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])
+        self.covers = sum([grid[n].owned for n in self.cell.neighbors])
         
         # eval pivot
         self.pivot = sum([1 + grid[p].get_unit_level() for p in self.cell.pivot_for])
@@ -143,9 +144,17 @@ class Position(BasePosition):
         # priorize adjacent cells
         self.union = len([n for n in self.cell.neighbors if grid[n].active_owned])
         
+        # favorize dispersion
+        self.concentration = len([n for n in self.cell.neighbors if grid[n].unit and grid[n].unit.owned])
+        
+        self.deadends = len([n for n in self.cell.neighbors if not grid[n].movable])
+        
         # include 'depthmap'
         self.depth = self.cell.depth
         
+        self.under_tower = self.cell.under_tower
+        self.overlaps_tower = sum([grid[n].under_tower for n in self.cell.neighbors])
+        
         # priorize mines or HQ
         if self.cell.building:
             self.hq = int(self.cell.building.type_ == Building.HQ)
@@ -161,11 +170,11 @@ class Position(BasePosition):
 
     def eval(self):
         self.pre_eval()
-        self.interest = 3 * self.depth + self.dist_to_goal
+        self.interest = 3 * self.depth + self.dist_to_goal + 2 * self.concentration + 2 * self.deadends
 
     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]
+        detail = [self.possession, self.threat, self.pivot, self.dist_to_goal,
+                  self.union, self.concentration, self.depth, self.hq, self.tower, self.mine]
         return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
         
         
@@ -175,16 +184,21 @@ class Defend(Position):
         self.emergency = emergency
     
     def __repr__(self):
-        detail = [self.threat, self.covers, self.pivot, self.emergency, self.dist_to_hq]
+        detail = [self.threat, self.covers, self.pivot, self.emergency, 
+                  self.dist_to_hq, self.cell.danger, self.cell.critical, 
+                  self.under_tower, self.overlaps_tower]
         return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
     
     def eval(self):
         self.pre_eval()
-        self.interest = 100 \
-                        - 10 * self.threat \
-                        - self.covers // 5 \
-                        - 5 * self.pivot \
-                        - 20 * self.emergency * (22 - self.dist_to_hq)
+        self.interest = (200 if not self.emergency else 50) \
+                        - 2 * self.threat \
+                        - 2 * self.covers \
+                        - 8 * self.pivot \
+                        - 80 * self.cell.critical \
+                        - 25 * self.cell.danger \
+                        + 25 * self.cell.under_tower \
+                        + 10 * self.overlaps_tower
         
 class Attack(Position):
     def eval(self):
@@ -192,27 +206,23 @@ class Attack(Position):
         self.interest = 15 * self.possession \
                         - 5 * self.pivot \
                         - 2 * self.union \
-                        + 3 * self.depth \
+                        + 3 * self.concentration \
+                        + 2 * self.deadends \
+                        + 4 * self.depth \
                         + self.dist_to_goal \
+                        - 2 * max([0, 11 - self.dist_to_hq]) \
                         - 30 * self.tower \
                         - 15 * self.mine \
-                        - 100 * self.hq
+                        - 100 * self.hq \
+                        + 10 * (self.min_level - 1)
         
 class MinePosition(BasePosition):
-    def __init__(self, target, type_):
-        self.type_ = type_
-        super().__init__(target, type_)
+    def __init__(self, target):
+        super().__init__(target)
     
     def eval(self):
         # the lower the better
-        self.interest = 0
-        
-        if self.type_ == Building.MINE:
-            self.interest -= self.target.depth
-            
-        elif self.type_ == Building.TOWER:
-            if self.target.pivot:
-                self.interest -= 20
+        self.interest -= self.cell.depth
 
 class BaseLoc(Base):
     def __init__(self, x, y):
@@ -301,9 +311,12 @@ class Player(Base):
     def training_capacity(self):
         return min([(self.gold - self.spent) // Unit.cost[1], (self.income - self.new_charges) // Unit.maintenance[1]])
 
+    def can_afford(self, lvl):
+        return (self.gold - self.spent) >= Unit.cost[lvl] and (self.income - self.new_charges) >= Unit.maintenance[lvl]
+
     def max_affordable(self):
         for lvl in range(3, 0, -1):
-            if (self.gold - self.spent) >= Unit.cost[lvl] and (self.income - self.new_charges) >= Unit.maintenance[lvl]:
+            if self.can_afford(lvl):
                 return lvl
         return 0
 
@@ -323,13 +336,10 @@ class Cell(Base):
         self.under_tower = False
         self.depth = 0
         self.pivot_for = []
-        self.strategic_value = 0
+        self.critical = False
+        self.danger = False
         self.threat = 0
-        
-        # front cells
-        self.facing = []
-        self.support = []
-        self.in_front_of = []
+        self.threat_by = None
         
     @property
     def pos(self):
@@ -347,13 +357,11 @@ class Cell(Base):
         self.under_tower = False
         self.depth = 0
         self.pivot_for = []
-        self.strategic_value = 0
         self.threat = 0
-    
-        self.facing = []
-        self.support = []
-        self.in_front_of = []
-    
+        self.threat_by = None
+        self.critical = False
+        self.danger = False
+
     @property
     def movable(self):
         return self._content != "#"
@@ -374,6 +382,7 @@ class Cell(Base):
             return OPPONENT
         else:
             return None
+
         
     @property
     def headquarter(self):
@@ -395,6 +404,14 @@ class Cell(Base):
     def active_opponent(self):
         return self._content == "X"
     
+    def is_active_owner(self, player_id):
+        if player_id == ME:
+            return self.active_owned
+        elif player_id == ME:
+            return self.active_opponent
+        else:
+            return False
+    
     def owned_unit(self):
         if self.unit and self.unit.owned:
             return self.unit
@@ -416,6 +433,16 @@ class Node(Base):
     def __init__(self, pos, path=[]):
         self.pos = pos
         self.path = path
+        
+class PathNode(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+    
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, c:{self.cost}>"
     
 class Grid(Base):
     dim = 12
@@ -460,15 +487,18 @@ class Grid(Base):
                                           units_ix.get((x, y), None), 
                                           buildings_ix.get((x, y), None))
 
+        log(" * update pivots")
         self.update_pivots()
-        self.update_state()
         
+        log(" * update threats")
+        self.update_threats()
+        
+        self.update_state()
 
     def update_state(self):
         self.update_tower_areas()
         self.update_frontlines()
         self.update_depth_map()
-        self.update_threats()
 
     @staticmethod
     def manhattan(from_, to_):
@@ -534,8 +564,7 @@ class Grid(Base):
             next_buffer = []
     
     def _active_owned(self, pos, player_id):
-        c = self.cells[pos]
-        return c.owner == player_id and c.active
+        return self.cells[pos].is_active_owner(player_id)
     
     def update_pivot_for(self, player_id):
         start = self.get_hq(player_id).pos
@@ -544,11 +573,18 @@ class Grid(Base):
         buffer = [start_node]
         nodes = {start_node}
         
-        ignored = [p for p in self.cells if len([n for n in self.neighbors(*p, diags=True) if self._active_owned(n, player_id)]) == 8]
+        its, breakdown = 0, 1200
+        
+        ignored = [p for p, c in self.cells.items() if c.is_active_owner(player_id) and 
+                   len([n for n in self.neighbors(*p, diags=True) if self._active_owned(n, player_id)]) == 8]
         
         while buffer:
             new_buffer = []
             for node in buffer:
+                its += 1
+                if its > breakdown:
+                    log("pivots: broken")
+                    return
                 if node.pos in ignored:
                     continue
                 for n in self.neighbors(*node.pos):
@@ -576,49 +612,115 @@ class Grid(Base):
                         pivots[candidate] = []
                     pivots[candidate].append(p)
         
+        log("player {} pivots: {}".format(player_id, {k: len(v) for k, v in pivots.items()}))
+        
         for pivot, pivot_for in pivots.items():
             self.cells[pivot].pivot_for = pivot_for
             
-        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
-            
-#         if player_id == ME:
-#             log(pivots)
-#             log(occurrences)
-#             log({cell.pos: cell.strategic_value for cell in self.cells.values() if cell.owned})
-        
     def update_pivots(self):
         self.update_pivot_for(ME)
         self.update_pivot_for(OPPONENT)
     
     def update_threats(self):
         # 1 + max number of units opponents can produce in one turn
-        self.threat_level = 1 + min([(opponent.gold + opponent.income) // Unit.cost[1], opponent.income // Unit.maintenance[1]])
+        self.threat_level = 2 + (opponent.gold + opponent.income) // Unit.cost[1]
         
-        ennemy_frontier = [c for c in self.cells.values() if c.opponents \
+        ennemy_frontier = [c for c in self.cells.values() if c.active_opponent \
                            and any(self.cells[n].movable and not self.cells[n].opponents for n in c.neighbors)]
 
         for cell in self.cells.values():
             if cell.owned:
-                threat = min([Grid.manhattan(cell.pos, o.pos) for o in ennemy_frontier])
-                cell.threat = self.threat_level - threat
+                closest, dist = min([(o, Grid.manhattan(cell.pos, o.pos)) for o in ennemy_frontier], key=lambda x: x[1])
+                if cell.unit:
+                    dist += cell.unit.level
+                if cell.under_tower:
+                    dist += 2
+                    
+                if dist < self.threat_level:
+                    cell.threat = self.threat_level - dist
+                    cell.threat_by = closest
+        
+        hqcell = grid[player.hq.pos]
+        self.emergency = hqcell.threat > 0
         
-        self.emergency = grid[player.hq.pos].threat > 0
+        self.update_threat_path()
         
-#         log({c.pos: c.threat for c in self.cells.values() if c.owned and c.threat is not None})
+#         log({c.pos: (c.threat, c.threat_by) for c in self.cells.values() if c.owned and c.threat > 0})
+    
+    def update_threat_path(self):
+        
+        hqcell = grid[player.hq.pos]
+        if not hqcell.threat > 0:
+            return
+        closest_threat = hqcell.threat_by
+#         log(f"* {closest_threat.pos} threats HQ")
+        
+        path  = self.path(closest_threat.pos, player.hq.pos)
+        if path:
+#             log(f"* Path to HQ: {path}")
+            extended_path = set()
+            for p in path:
+                extended_path |= set(self.neighbors(*p))
+            extended_path -= set(path)
+            
+            for p in path:
+                if self._active_owned(p, ME):
+                    self.cells[p].critical = True
+            for p in extended_path:
+                if self._active_owned(p, ME):
+                    self.cells[p].danger = True
+    
+    
+    def path(self, start, target):
+        nodes = Queue()
+        its, break_on = 0, 2000
+        
+        origin = PathNode(*start)
+        nodes.put(origin, 0)
+        
+        neighbors = []
+
+        while nodes:
+            current = nodes.get()
+            
+            if current == target:
+                path = []
+                previous = current
+                while previous:
+                    if previous != start:
+                        path.insert(0, previous)
+                    previous = previous.parent
+                return path
+
+            neighbors = self.neighbors(*current)
+
+            for x, y in neighbors:
+                its += 1
+                if its > break_on:
+                    log("<!> pathfinding broken")
+                    return None
+                
+                if (x, y) == current.parent:
+                    continue
+                
+                cell = self.cells[(x, y)]
+                if not cell.movable:
+                    continue
+
+                moving_cost = 1
+                if cell.unit and cell.unit.owned:
+                    moving_cost += cell.unit.level
+                if cell.under_tower:
+                    moving_cost += 2
+                    
+                cost = current.cost + moving_cost
+                priority = cost + 10 * Grid.manhattan((x, y), target)
+                
+                node = PathNode(x, y, current)
+                node.cost = cost
+                nodes.put(node, priority)
+                
+        return None
     
     def influence_zone(self, player_id):
         owned, neighbors = {p for p, c in self.cells.items() if c.owner == player_id and c.active}, set()
@@ -689,7 +791,7 @@ class Grid(Base):
     def get_building_site(self, type_):
         q = InterestQueue()
         for cell in self.building_zone(type_):
-            q.put(Position(cell, type_))
+            q.put(MinePosition(cell))
         if not q:
             return None
         return q.get()
@@ -708,17 +810,19 @@ class Grid(Base):
             
             if new_cell.unit:
                 if new_cell.unit.owned:
-                    log("ERROR: impossible move")
+                    log("ERROR: impossible move (owned unit here)")
                     return
                 if action.unit.level < 3 and new_cell.unit.level >= action.unit.level:
-                    log("ERROR: impossible move")
+                    log("ERROR: impossible move (unit here, level too low)")
                     return
                 # cell is occupied by an opponent's unit with an inferior level
                 self.remove_unit_from(new_cell)
             
             if new_cell.building and new_cell.building.type_ == Building.TOWER:
+                if new_cell.owned:
+                    log("ERROR: impossible move (allied tower here)")
                 if action.unit.level < 3:
-                    log("ERROR: impossible move")
+                    log("ERROR: impossible move (tower here, level too low)")
                 opponent.buildings.remove(new_cell.building)
                 self.buildings.remove(new_cell.building)
                 new_cell.building = None
@@ -756,7 +860,7 @@ class Grid(Base):
                 # cell is occupied by an opponent's unit with an inferior level
                 self.remove_unit_from(new_cell)
             
-            if new_cell.building and new_cell.building.type_ == Building.TOWER:
+            if new_cell.building and new_cell.building.opponents and new_cell.building.type_ == Building.TOWER:
                 if unit.level < 3:
                     log("ERROR: impossible training")
                 opponent.buildings.remove(new_cell.building)
@@ -789,6 +893,8 @@ class Grid(Base):
                     self.cells[n].under_tower = True
 
             self.update_state()
+            if self.emergency:
+                self.update_threat_path()
 
         elif type(action) is BuildMine:
             
@@ -895,6 +1001,7 @@ def get_input():
 
 while True:
     
+    
     # <--- get and parse input 
     gold, income, opponent_gold, opponent_income, new_grid_input, buildings_input, units_input = get_input()
 
@@ -907,6 +1014,9 @@ while True:
 #     log(f"* units: {units}")   
     # --->
     
+    log("## Start ##")
+    log("* Update")
+    
     # <--- update
     player.update(gold, income, units, buildings)
 #     log(f"player: {player}")
@@ -927,43 +1037,78 @@ while True:
     
     todo = []
     abandonned = []
+    acted = True
+    defs, max_def = 0, 8
     
     while player.can_act():
         
-        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)
-        
+        if acted:
+            # only re-eval if an action occured
+            q = InterestQueue()
+            for cell in grid.influence_zone(ME):
+                if cell.movable and not cell in abandonned:
+                    if cell.owned and cell.threat > 0 and cell.pos != player.hq.pos:
+                        if defs > max_def:
+                            continue
+                        p = Defend(cell, grid.emergency)
+                    elif not cell.owned:
+                        p = Attack(cell)
+                    else:
+                        continue
+                    p.eval()
+                    q.put(p)
+            
         if not q:
             break
         
+        acted = False
+        
         objective = q.get()
+            
         if type(objective) is Defend:
+            defs += 1
+            
             if player.current_gold > Building.cost[Building.TOWER] \
                and not objective.cell.mine_site \
                and not objective.cell.building:
                 action = BuildTower(objective.cell)
                 
-            else:
-                # TODO: recruit units
-                abandonned.append(objective.cell)
+            elif objective.cell.unit and objective.cell.unit.owned:
+                # already a unit here: stay on place
+                objective.cell.unit.has_moved = True
                 continue
+                
+            elif player.can_afford(1):
+                action = Train(1, objective.cell)
+                
+            else:
+                near_unit = next((grid[n].unit for n in objective.cell.neighbors 
+                                  if grid[n].unit 
+                                  and grid[n].unit.owned 
+                                  and grid[n].unit.level == 1
+                                  and not grid[n].unit.has_moved), 
+                                 None)
+                if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
+                    action = Move(near_unit, objective.cell)
+                else:
+                    abandonned.append(objective.cell)
+                    continue
             
         elif type(objective) is Attack:
-            near_unit = next((grid[n].unit for n in objective.cell.neighbors if grid[n].unit \
+            
+            near_units = [grid[n].unit for n in objective.cell.neighbors if grid[n].unit \
                                                                              and grid[n].unit.owned \
                                                                              and not grid[n].unit.has_moved \
-                                                                             and grid[n].unit.level >= objective.min_level), 
-                              None)
-            if near_unit:
+                                                                             and grid[n].unit.level >= objective.min_level]
+            
+            if len(near_units) == 1:
+                near_unit = near_units[0]
+            elif len(near_units) > 1:
+                near_unit = min(near_units, key=lambda u: len([u.pos == o.cell.pos for o in q.items]))
+            else:
+                near_unit = None
+                
+            if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
                 action = Move(near_unit, objective.cell)
             else:
                 if objective.min_level > player.max_affordable():
@@ -971,9 +1116,15 @@ while True:
                     continue
                 action = Train(objective.min_level, objective.cell)
         
-        log(f"priority: {action} -> {objective}")
+        log(f"priority: {objective}")
+        
+        # a unit occupy the cell
         already_there = action.cell.unit
         if already_there and already_there.owned:
+            if already_there.has_moved:
+                log("cell is occupied: abandon")
+                abandonned.append(objective.cell)
+                continue
             log(f"* needs to evacuate unit {already_there.id_} before")
             evacuate = grid.get_next_move(already_there)
             if evacuate:
@@ -983,7 +1134,11 @@ while True:
                 todo.append(evac_action)
             else:
                 log(f"<!> No move available for unit {already_there.id_}")
+                abandonned.append(objective.cell)
+                continue
         
+        acted = True
+        log(f"> Apply action {action}")
         grid.apply(action)
         todo.append(action)
         
@@ -997,15 +1152,18 @@ while True:
                 grid.apply(action)
                 todo.append(action)
             else:
-                log(f"<!> No move available for unit {already_there.id_}")
+                log(f"<!> No move available for unit {unit.id_}")
+                unit.has_moved = True
                 
     # can build mines?
     if player.current_gold > grid.cost_for_new_mine():
-        action = BuildMine(grid.get_building_site(Building.MINE))
-        if action:
-            log(f"default: {action}")
-            grid.apply(action)
-            todo.append(action)
+        site = grid.get_building_site(Building.MINE)
+        if site:
+            action = BuildMine(site.cell)
+            if action:
+                log(f"default: {action}")
+                grid.apply(action)
+                todo.append(action)
         
     log(f"* todo: {todo}")