|
|
@@ -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))
|