|
|
@@ -1,6 +1,5 @@
|
|
|
import heapq
|
|
|
import sys
|
|
|
-import math
|
|
|
import time
|
|
|
|
|
|
debug = True
|
|
|
@@ -23,6 +22,34 @@ class BaseClass:
|
|
|
return f"<{self.__class__.__name__}: {self.__dict__}>"
|
|
|
|
|
|
|
|
|
+class Node(BaseClass):
|
|
|
+ def __init__(self, pos, path=None):
|
|
|
+ self.pos = pos
|
|
|
+ self.path = path or []
|
|
|
+
|
|
|
+
|
|
|
+class PathNode(tuple):
|
|
|
+ def __new__(cls, x, y, parent=None):
|
|
|
+ n = tuple.__new__(cls, (x, y))
|
|
|
+ n.parent = parent
|
|
|
+ n.cost = 0
|
|
|
+ return n
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return f"<{self[0]}, {self[1]}, c:{self.cost}>"
|
|
|
+
|
|
|
+
|
|
|
+class DiscoverNode(tuple):
|
|
|
+ def __new__(cls, x, y, ancestors=None):
|
|
|
+ n = tuple.__new__(cls, (x, y))
|
|
|
+ n.ancestors = ancestors if ancestors is not None else []
|
|
|
+ n.cost = 0
|
|
|
+ return n
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return f"<{self[0]}, {self[1]}, c:{self.cost}>"
|
|
|
+
|
|
|
+
|
|
|
class Queue(BaseClass):
|
|
|
def __init__(self):
|
|
|
self.items = []
|
|
|
@@ -112,12 +139,13 @@ class ActionWait(Action):
|
|
|
|
|
|
|
|
|
class ActionMove(Action):
|
|
|
- def __init__(self, unit, pos):
|
|
|
+ def __init__(self, unit, pos, message=''):
|
|
|
self.unit = unit
|
|
|
self.pos = pos
|
|
|
+ self.message = message
|
|
|
|
|
|
def __repr__(self):
|
|
|
- return f"<ActionMove: {self.unit.id} to {self.pos}>"
|
|
|
+ return f"<ActionMove: {self.unit.id} to {self.pos} ({self.message})>"
|
|
|
|
|
|
def exec(self):
|
|
|
print(f"{self.unit.id} MOVE {self.pos[0]} {self.pos[1]}")
|
|
|
@@ -151,12 +179,26 @@ class Grid(BaseClass):
|
|
|
def __init__(self, width, height):
|
|
|
self.width = width
|
|
|
self.height = height
|
|
|
+
|
|
|
+ self.cells = []
|
|
|
self.obstacles = []
|
|
|
+ self._neighbors = {}
|
|
|
+
|
|
|
self.index = {}
|
|
|
self.units = {}
|
|
|
self.round = 0
|
|
|
+ self.threat = {}
|
|
|
+ self.control = {}
|
|
|
+ self.heat_map = {}
|
|
|
+
|
|
|
+ def pre_compute(self):
|
|
|
+ self.cells = [(x, y) for x in range(self.width) for y in range(self.height)]
|
|
|
|
|
|
- def prepare_round(self):
|
|
|
+ for x, y in self.cells:
|
|
|
+ self._neighbors[(x, y)] = [(xn, yn) for xn, yn in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if
|
|
|
+ 0 <= xn < self.width and 0 <= yn < self.height]
|
|
|
+
|
|
|
+ def reinit_round(self):
|
|
|
self.units = {}
|
|
|
|
|
|
def update_unit(self, id_, type_, hp, x, y, owner):
|
|
|
@@ -167,13 +209,63 @@ class Grid(BaseClass):
|
|
|
unit.y = y
|
|
|
unit.owner = owner
|
|
|
|
|
|
- def update_index(self):
|
|
|
+ def update_threat_map(self):
|
|
|
+ self.threat = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
|
|
|
+
|
|
|
+ for u in self.opponent_cultists():
|
|
|
+ shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
|
|
|
+ for x, y in shooting_zone:
|
|
|
+ dist = shooting_zone[(x, y)]
|
|
|
+
|
|
|
+ if not self.line_of_sight(u.pos, (x, y)):
|
|
|
+ continue
|
|
|
+
|
|
|
+ threat = Unit.SHOOTING_RANGE + 1 - dist
|
|
|
+ if threat > self.threat[(x, y)]:
|
|
|
+ self.threat[(x, y)] = threat
|
|
|
+
|
|
|
+ def update_control(self):
|
|
|
+ self.control = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
|
|
|
+
|
|
|
+ for u in self.allied_cultists():
|
|
|
+ shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
|
|
|
+ for x, y in shooting_zone:
|
|
|
+ dist = shooting_zone[(x, y)]
|
|
|
+
|
|
|
+ if not self.line_of_sight(u.pos, (x, y)):
|
|
|
+ continue
|
|
|
+
|
|
|
+ control = Unit.SHOOTING_RANGE + 1 - dist
|
|
|
+ if control > self.control[(x, y)]:
|
|
|
+ self.control[(x, y)] = control
|
|
|
+
|
|
|
+ def update_heat_map(self):
|
|
|
+ lines = []
|
|
|
+ self.heat_map = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
|
|
|
+
|
|
|
+ cult_leader = self.cult_leader()
|
|
|
+
|
|
|
+ for o in self.opponent_cultists():
|
|
|
+ if cult_leader:
|
|
|
+ lines += self.line(o.pos, cult_leader.pos)
|
|
|
+
|
|
|
+ for n in self.neutrals():
|
|
|
+ lines += self.line(o.pos, n.pos)
|
|
|
+
|
|
|
+ for pos in lines:
|
|
|
+ self.heat_map[pos] += 1
|
|
|
+
|
|
|
+ def update(self):
|
|
|
self.index = {}
|
|
|
for unit in self.units.values():
|
|
|
self.index[(unit.x, unit.y)] = unit
|
|
|
|
|
|
- def own_cult_leader(self):
|
|
|
- return next(u for u in self.units.values() if type(u) is CultLeader and u.owned)
|
|
|
+ self.update_threat_map()
|
|
|
+ self.update_control()
|
|
|
+ self.update_heat_map()
|
|
|
+
|
|
|
+ def cult_leader(self):
|
|
|
+ return next((u for u in self.units.values() if type(u) is CultLeader and u.owned), None)
|
|
|
|
|
|
def allied_cultists(self):
|
|
|
return [u for u in self.units.values() if type(u) is not CultLeader and u.owned]
|
|
|
@@ -197,41 +289,86 @@ class Grid(BaseClass):
|
|
|
k_convert_danger = 30
|
|
|
k_shoot_opponent_cultist = 20
|
|
|
k_shoot_opponent_cult_leader = 10
|
|
|
+ k0_protect_cult_leader = 10
|
|
|
+ k0_position = 50
|
|
|
+ k_position_distance = 10
|
|
|
+ k_position_heat = -5
|
|
|
+
|
|
|
+ cult_leader = self.cult_leader()
|
|
|
+
|
|
|
+ # Conversion des neutres
|
|
|
+ if cult_leader:
|
|
|
+ paths = self.discover(
|
|
|
+ cult_leader.pos,
|
|
|
+ key=(lambda pos: pos in self.index and self.index[pos].neutral),
|
|
|
+ limit=5
|
|
|
+ )
|
|
|
+ for path in paths:
|
|
|
+ log(path)
|
|
|
+
|
|
|
+ for path in paths:
|
|
|
+ target = self.index[path[-1]]
|
|
|
+
|
|
|
+ priority = 0
|
|
|
+ priority += k_convert_neutrals * len(path)
|
|
|
+ priority += k_convert_danger * sum([self.threat[pos] for pos in path])
|
|
|
+
|
|
|
+ if target in self.neighbors(*cult_leader.pos):
|
|
|
+ action = ActionConvert(cult_leader, target)
|
|
|
+ else:
|
|
|
+ action = ActionMove(cult_leader, path[0], f'go convert {target.id}')
|
|
|
+ actions.put(priority, action)
|
|
|
+
|
|
|
+ # Attaque d'unités ennemies
|
|
|
+ for a in self.allied_cultists():
|
|
|
+ for u in self.opponent_cultists():
|
|
|
+ shooting_distance = self.shooting_distance(a.pos, u.pos)
|
|
|
+ if shooting_distance and shooting_distance < u.SHOOTING_RANGE:
|
|
|
+ action = ActionShoot(a, u)
|
|
|
|
|
|
- k_limit = 200
|
|
|
+ priority = (k_shoot_opponent_cult_leader if type(
|
|
|
+ u) is CultLeader else k_shoot_opponent_cultist) * shooting_distance
|
|
|
|
|
|
- cult_leader = self.own_cult_leader()
|
|
|
+ actions.put(priority, action)
|
|
|
|
|
|
- for n in self.neutrals():
|
|
|
- action = ActionConvert(cult_leader, n)
|
|
|
+ # Position
|
|
|
+ for a in self.allied_cultists():
|
|
|
+ # on garde les trois points les plus chauds
|
|
|
+ hot_spots = sorted(self.heat_map.items(), key=lambda p: p[1], reverse=True)[:3]
|
|
|
|
|
|
- distance = self.manhattan(cult_leader.pos, n.pos)
|
|
|
- danger = 0
|
|
|
- for u in self.opponent_cultists():
|
|
|
- fire_dist = self.fire_dist(cult_leader.pos, u.pos)
|
|
|
- if fire_dist < u.SHOOTING_RANGE:
|
|
|
- danger += (u.SHOOTING_MAX_DAMAGE - fire_dist)
|
|
|
+ for pos, heat in hot_spots:
|
|
|
+ action = ActionMove(a, pos)
|
|
|
|
|
|
- priority = k_convert_neutrals * distance + k_convert_danger * danger
|
|
|
+ path = self.path(a.pos, pos)
|
|
|
|
|
|
- if priority > k_limit:
|
|
|
- continue
|
|
|
+ safe_path = []
|
|
|
+ for step in path:
|
|
|
+ if self.threat[step] > 1:
|
|
|
+ break
|
|
|
+ safe_path.append(step)
|
|
|
|
|
|
- actions.put(priority, action)
|
|
|
+ if not safe_path:
|
|
|
+ continue
|
|
|
|
|
|
- for a in self.allied_cultists():
|
|
|
- for u in self.opponent_cultists():
|
|
|
- fire_dist = self.fire_dist(a.pos, u.pos)
|
|
|
- if fire_dist < u.SHOOTING_RANGE:
|
|
|
- action = ActionShoot(a, u)
|
|
|
+ priority = k0_position
|
|
|
+ priority += k_position_heat * heat
|
|
|
+ priority += k_position_distance * len(path)
|
|
|
|
|
|
- priority = (k_shoot_opponent_cult_leader if type(
|
|
|
- u) is CultLeader else k_shoot_opponent_cultist) * fire_dist
|
|
|
+ actions.put(priority, action)
|
|
|
|
|
|
- if priority > k_limit:
|
|
|
- continue
|
|
|
+ # Mise en sécurité du chef
|
|
|
+ if cult_leader:
|
|
|
+ current_threat = self.threat[cult_leader.pos]
|
|
|
+ if current_threat:
|
|
|
+ target = min(
|
|
|
+ [n for n in self.neighbors(*cult_leader.pos) if self.can_move_on(n)],
|
|
|
+ key=lambda x: self.threat[x]
|
|
|
+ )
|
|
|
|
|
|
- actions.put(priority, action)
|
|
|
+ action = ActionMove(cult_leader, target)
|
|
|
+ priority = k0_protect_cult_leader
|
|
|
+
|
|
|
+ actions.put(priority, action)
|
|
|
|
|
|
return actions
|
|
|
|
|
|
@@ -242,7 +379,15 @@ class Grid(BaseClass):
|
|
|
return self.in_grid(pos) and pos not in self.obstacles + list(self.index.values())
|
|
|
|
|
|
def can_move_on(self, pos):
|
|
|
- return self.in_grid(pos) and pos not in self.obstacles + list(self.index.values())
|
|
|
+ return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
|
|
|
+
|
|
|
+ def can_discover(self, pos):
|
|
|
+ return self.in_grid(pos) and pos not in self.obstacles
|
|
|
+
|
|
|
+ def moving_cost(self, pos):
|
|
|
+ if not self.can_move_on(pos):
|
|
|
+ return -1
|
|
|
+ return 1 + self.threat[pos]
|
|
|
|
|
|
@staticmethod
|
|
|
def manhattan(from_, to_):
|
|
|
@@ -250,6 +395,21 @@ class Grid(BaseClass):
|
|
|
xb, yb = to_
|
|
|
return abs(xa - xb) + abs(ya - yb)
|
|
|
|
|
|
+ def neighbors(self, x, y):
|
|
|
+ return self._neighbors[(x, y)]
|
|
|
+
|
|
|
+ def zone(self, pos, radius):
|
|
|
+ x0, y0 = pos
|
|
|
+ zone = {}
|
|
|
+
|
|
|
+ for x in range(max(x0 - radius, 0), min(x0 + radius, self.width)):
|
|
|
+ for y in range(max(y0 - radius, 0), min(y0 + radius, self.height)):
|
|
|
+ dist = self.manhattan(pos, (x, y))
|
|
|
+ if dist <= radius:
|
|
|
+ zone[(x, y)] = dist
|
|
|
+
|
|
|
+ return zone
|
|
|
+
|
|
|
@classmethod
|
|
|
def line(cls, from_, to_):
|
|
|
""" Implementation of bresenham's algorithm """
|
|
|
@@ -290,31 +450,153 @@ class Grid(BaseClass):
|
|
|
result.reverse()
|
|
|
return result
|
|
|
|
|
|
- def fire_line(self, from_, to_):
|
|
|
+ def line_of_sight(self, from_, to_):
|
|
|
line = self.line(from_, to_)
|
|
|
return line if all(self.can_see_trough(c) for c in line) else []
|
|
|
|
|
|
- def fire_dist(self, from_, to_):
|
|
|
- return len(self.fire_line(from_, to_))
|
|
|
+ def shooting_distance(self, from_, to_):
|
|
|
+ return len(self.line_of_sight(from_, to_))
|
|
|
+
|
|
|
+ def path(self, start, target):
|
|
|
+ nodes = Queue()
|
|
|
+ its, break_on = 0, 300
|
|
|
+
|
|
|
+ origin = PathNode(*start)
|
|
|
+ nodes.put(0, origin)
|
|
|
+
|
|
|
+ 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
|
|
|
+
|
|
|
+ if not self.can_move_on((x, y)):
|
|
|
+ continue
|
|
|
+
|
|
|
+ moving_cost = self.moving_cost((x, y))
|
|
|
+
|
|
|
+ cost = current.cost + moving_cost
|
|
|
+ priority = cost + 10 * Grid.manhattan((x, y), target)
|
|
|
+
|
|
|
+ node = PathNode(x, y, current)
|
|
|
+ node.cost = cost
|
|
|
+ nodes.put(priority, node)
|
|
|
+
|
|
|
+ return None
|
|
|
+
|
|
|
+ def discover(self, start, key, limit=5):
|
|
|
+ paths = []
|
|
|
+ found = []
|
|
|
+
|
|
|
+ nodes = Queue()
|
|
|
+ its, break_on = 0, 2000
|
|
|
+
|
|
|
+ origin = DiscoverNode(*start)
|
|
|
+ nodes.put(0, origin)
|
|
|
+
|
|
|
+ while nodes:
|
|
|
+ current = nodes.get()
|
|
|
+
|
|
|
+ if current not in found and current != start and key(tuple(current)):
|
|
|
+ path = []
|
|
|
+ previous = current
|
|
|
+ while previous.ancestors:
|
|
|
+ if previous != start:
|
|
|
+ path.insert(0, previous)
|
|
|
+ previous = previous.ancestors[-1]
|
|
|
+ found.append(path[-1])
|
|
|
+ paths.append(path)
|
|
|
+
|
|
|
+ if len(paths) >= limit:
|
|
|
+ return paths
|
|
|
+
|
|
|
+ neighbors = self.neighbors(*current)
|
|
|
+
|
|
|
+ for x, y in neighbors:
|
|
|
+ its += 1
|
|
|
+ if its > break_on:
|
|
|
+ log("<!> discovering ended earlier than expected")
|
|
|
+ return paths
|
|
|
+
|
|
|
+ if (x, y) in current.ancestors:
|
|
|
+ continue
|
|
|
+
|
|
|
+ if not self.can_discover((x, y)):
|
|
|
+ continue
|
|
|
+
|
|
|
+ moving_cost = self.moving_cost((x, y))
|
|
|
+
|
|
|
+ cost = current.cost + moving_cost
|
|
|
+ priority = cost + Grid.manhattan((x, y), start)
|
|
|
+
|
|
|
+ ancestors = current.ancestors + [current]
|
|
|
+
|
|
|
+ node = DiscoverNode(x, y, ancestors)
|
|
|
+ node.cost = cost
|
|
|
+ nodes.put(priority, node)
|
|
|
+
|
|
|
+ return paths
|
|
|
+
|
|
|
+ def _repr_cell(self, pos):
|
|
|
+ # return f"{self.control[pos]}/{self.threat[pos]}"
|
|
|
+ # return self.heat_map[pos]
|
|
|
+
|
|
|
+ if pos in self.obstacles:
|
|
|
+ return "X"
|
|
|
+
|
|
|
+ unit = self.index.get(pos, None)
|
|
|
+ if type(unit) is CultLeader:
|
|
|
+ return "C"
|
|
|
+ elif unit is None:
|
|
|
+ return "."
|
|
|
+ else:
|
|
|
+ return "U"
|
|
|
+
|
|
|
+ def graph(self):
|
|
|
+ return "\n".join(
|
|
|
+ ["|".join([str(self._repr_cell((x, y))) for x in range(self.width)]) for y in range(self.height)])
|
|
|
|
|
|
|
|
|
# Create grid
|
|
|
GRID = Grid(*[int(i) for i in input().split()])
|
|
|
-GRID.obstacles = [(i, j) for i in range(GRID.height) for j, val in enumerate(input()) if val == 'x']
|
|
|
+obstacles_input = [input() for y in range(GRID.height)]
|
|
|
+GRID.obstacles = [(x, y) for y, row in enumerate(obstacles_input) for x, val in enumerate(row) if val == 'x']
|
|
|
+
|
|
|
+GRID.pre_compute()
|
|
|
|
|
|
while 1:
|
|
|
# TODO: prendre en compte le terrain dans la ligne de visée et les déplacements
|
|
|
+ log(f"start round {GRID.round}")
|
|
|
|
|
|
- GRID.prepare_round()
|
|
|
+ GRID.reinit_round()
|
|
|
for _ in range(int(input())):
|
|
|
GRID.update_unit(*[int(j) for j in input().split()])
|
|
|
- GRID.update_index()
|
|
|
+ GRID.update()
|
|
|
|
|
|
actions = GRID.list_actions()
|
|
|
|
|
|
for action in actions.items:
|
|
|
log(f"* {action}")
|
|
|
|
|
|
+ # print("\n" + GRID.graph(), file=sys.stderr)
|
|
|
+
|
|
|
try:
|
|
|
action = actions.get()
|
|
|
except IndexError:
|