|
|
@@ -40,15 +40,15 @@ class PathNode(tuple):
|
|
|
|
|
|
|
|
|
class DiscoveryNode(tuple):
|
|
|
- def __new__(cls, x, y, ancestors=None, matches=None):
|
|
|
+ def __new__(cls, x, y, cost=0, ancestors=None, matches=None):
|
|
|
n = tuple.__new__(cls, (x, y))
|
|
|
+ n.cost = cost
|
|
|
n.ancestors = ancestors if ancestors is not None else []
|
|
|
- n.cost = 0
|
|
|
n.matches = matches if matches is not None else []
|
|
|
return n
|
|
|
|
|
|
def __repr__(self):
|
|
|
- return f"<{self[0]}, {self[1]}, c:{self.cost}>"
|
|
|
+ return f"<{self[0]}, {self[1]}>"
|
|
|
|
|
|
|
|
|
class Queue(BaseClass):
|
|
|
@@ -76,6 +76,48 @@ class Queue(BaseClass):
|
|
|
return heapq.heappop(self.items)
|
|
|
|
|
|
|
|
|
+class ConversionStep:
|
|
|
+ def __init__(self):
|
|
|
+ self.pos = None
|
|
|
+ self.candidates = []
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return f"<{self.pos}, c:{self.candidates}>"
|
|
|
+
|
|
|
+
|
|
|
+class ConversionPath:
|
|
|
+ def __init__(self):
|
|
|
+ self.steps = []
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return f"<{self.steps}>"
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def make_from_discovery_node(cls, node):
|
|
|
+ nodes = node.ancestors + [node]
|
|
|
+ path = cls()
|
|
|
+ found = []
|
|
|
+
|
|
|
+ for node in nodes:
|
|
|
+ step = ConversionStep()
|
|
|
+ step.pos = tuple(node)
|
|
|
+ for m in node.matches:
|
|
|
+ if m in found:
|
|
|
+ continue
|
|
|
+ step.candidates.append(m)
|
|
|
+ found.append(m)
|
|
|
+ path.steps.append(step)
|
|
|
+ return path
|
|
|
+
|
|
|
+ def next_candidate(self):
|
|
|
+ path = []
|
|
|
+ for step in self.steps:
|
|
|
+ path.append(step)
|
|
|
+ if step.candidates:
|
|
|
+ return path
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
class Player(BaseClass):
|
|
|
def __init__(self, id_):
|
|
|
self.id = id_
|
|
|
@@ -187,7 +229,7 @@ class Grid(BaseClass):
|
|
|
|
|
|
self.index = {}
|
|
|
self.units = {}
|
|
|
- self.round = 0
|
|
|
+ self.round = 1
|
|
|
self.threat = {}
|
|
|
self.control = {}
|
|
|
self.heat_map = {}
|
|
|
@@ -301,14 +343,15 @@ class Grid(BaseClass):
|
|
|
def list_actions(self):
|
|
|
actions = Queue()
|
|
|
|
|
|
- k_convert_neutrals = 10
|
|
|
+ k_convert_number = -10
|
|
|
+ k_convert_distance = 10
|
|
|
k_convert_danger = 30
|
|
|
k_shoot_opponent_cultist = 10
|
|
|
k_shoot_opponent_cult_leader = 5
|
|
|
k0_protect_cult_leader = 30
|
|
|
k_protect_threat_level = -5
|
|
|
k_cover_threat = 10
|
|
|
- k_cover_interest = -3
|
|
|
+ k_cover_interest = -10
|
|
|
k0_position = 50
|
|
|
k_position_distance = 10
|
|
|
k_position_heat = -2
|
|
|
@@ -318,31 +361,34 @@ class Grid(BaseClass):
|
|
|
cult_leader = self.cult_leader()
|
|
|
opponent_cult_leader = self.opponent_cult_leader()
|
|
|
|
|
|
- log(self.obstacles)
|
|
|
+ log(self.obstacles + [u.pos for u in self.allied_cultists()])
|
|
|
log([n.pos for n in self.neutrals()])
|
|
|
|
|
|
# compute conversion paths
|
|
|
- conversion_paths = []
|
|
|
+ conversion_path = []
|
|
|
if cult_leader and self.neutrals():
|
|
|
- conversion_paths = self.discover(
|
|
|
- cult_leader.pos,
|
|
|
+ conversion_path = self.get_conversion_path(
|
|
|
+ cult_leader,
|
|
|
key=(lambda pos: pos in self.index and self.index[pos].neutral),
|
|
|
- limit=min(5, len(self.neutrals()))
|
|
|
+ limit=min(4, len(self.neutrals()))
|
|
|
)
|
|
|
+ log(conversion_path)
|
|
|
|
|
|
# Conversion des neutres
|
|
|
- if cult_leader:
|
|
|
- for path, target_pos in conversion_paths:
|
|
|
- target = self.index[target_pos]
|
|
|
+ if cult_leader and conversion_path:
|
|
|
+ path = conversion_path.next_candidate()
|
|
|
+ if path:
|
|
|
+ targets = [self.index[c] for c in path[-1].candidates]
|
|
|
|
|
|
priority = 0
|
|
|
- priority += k_convert_neutrals * len(path)
|
|
|
- priority += k_convert_danger * sum([self.threat[pos] for pos in path])
|
|
|
+ priority += k_convert_number * len(targets)
|
|
|
+ priority += k_convert_distance * len(path)
|
|
|
+ priority += k_convert_danger * sum([self.threat[s.pos] for s in path])
|
|
|
|
|
|
- if target_pos in self.neighbors(*cult_leader.pos):
|
|
|
- action = ActionConvert(cult_leader, target)
|
|
|
+ if len(path) == 1:
|
|
|
+ action = ActionConvert(cult_leader, targets[0])
|
|
|
else:
|
|
|
- action = ActionMove(cult_leader, path[0], f'go convert {target.id}')
|
|
|
+ action = ActionMove(cult_leader, path[1].pos, f'go convert {",".join([str(t.id) for t in targets])}')
|
|
|
actions.put(priority, action)
|
|
|
|
|
|
# Attaque d'unités ennemies
|
|
|
@@ -368,20 +414,19 @@ class Grid(BaseClass):
|
|
|
# on garde les trois points les plus chauds
|
|
|
hot_spots = sorted(self.heat_map.items(), key=lambda p: p[1], reverse=True)[:3]
|
|
|
|
|
|
- results = self.discover(a.pos, key=lambda x: x in [s[0] for s in hot_spots])
|
|
|
-
|
|
|
- for path, target_pos in results:
|
|
|
- if not path:
|
|
|
- break
|
|
|
+ # results = self.discover(a.pos, key=lambda x: x in [s[0] for s in hot_spots])
|
|
|
|
|
|
- heat = self.heat_map[target_pos]
|
|
|
+ # for path, target_pos in results:
|
|
|
+ # if not path:
|
|
|
+ # break
|
|
|
|
|
|
+ for spot_pos, heat in hot_spots:
|
|
|
priority = k0_position
|
|
|
priority += k_position_heat * heat
|
|
|
- priority += k_position_distance * len(path)
|
|
|
- priority += k_position_danger * sum(self.threat[p] for p in path)
|
|
|
+ priority += k_position_distance * self.manhattan(a.pos, spot_pos)
|
|
|
+ priority += k_position_danger * self.threat[spot_pos]
|
|
|
|
|
|
- action = ActionMove(a, path[0], f'pos on {target_pos}')
|
|
|
+ action = ActionMove(a, spot_pos, f'pos on {spot_pos}')
|
|
|
|
|
|
actions.put(priority, action)
|
|
|
|
|
|
@@ -389,13 +434,12 @@ class Grid(BaseClass):
|
|
|
if cult_leader:
|
|
|
current_threat = self.threat[cult_leader.pos]
|
|
|
if current_threat:
|
|
|
- covers = [n for n in self.neighbors(*cult_leader.pos) if self.can_move_on(n)]
|
|
|
+ covers = [n for n in self.neighbors(*cult_leader.pos) if self.can_move_on(cult_leader, n)]
|
|
|
|
|
|
for pos in covers:
|
|
|
action = ActionMove(cult_leader, pos, 'take cover')
|
|
|
|
|
|
- # interest is the number of conversion paths that start with this pos
|
|
|
- interest = len([p for p in conversion_paths if p[0] and p[0][0] == pos])
|
|
|
+ interest = conversion_path and conversion_path.steps and conversion_path.steps[0].pos == pos
|
|
|
|
|
|
priority = k0_protect_cult_leader
|
|
|
priority += k_protect_threat_level * current_threat
|
|
|
@@ -412,14 +456,14 @@ class Grid(BaseClass):
|
|
|
def can_see_trough(self, pos):
|
|
|
return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
|
|
|
|
|
|
- def can_move_on(self, pos):
|
|
|
- return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
|
|
|
+ def can_move_on(self, unit, pos):
|
|
|
+ return self.in_grid(pos) and pos not in self.obstacles and (pos not in self.index or self.index[pos] is unit)
|
|
|
|
|
|
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):
|
|
|
+ def moving_cost(self, unit, pos):
|
|
|
+ if not self.can_move_on(unit, pos):
|
|
|
return -1
|
|
|
return 1 + self.threat[pos]
|
|
|
|
|
|
@@ -491,11 +535,11 @@ class Grid(BaseClass):
|
|
|
def shooting_distance(self, from_, to_):
|
|
|
return len(self.line_of_sight(from_, to_))
|
|
|
|
|
|
- def path(self, start, target):
|
|
|
+ def path(self, unit, target):
|
|
|
nodes = Queue()
|
|
|
its, break_on = 0, 400
|
|
|
|
|
|
- origin = PathNode(*start)
|
|
|
+ origin = PathNode(*unit.pos)
|
|
|
nodes.put(0, origin)
|
|
|
|
|
|
while nodes:
|
|
|
@@ -505,7 +549,7 @@ class Grid(BaseClass):
|
|
|
path = []
|
|
|
previous = current
|
|
|
while previous:
|
|
|
- if previous != start:
|
|
|
+ if previous != unit.pos:
|
|
|
path.insert(0, previous)
|
|
|
previous = previous.parent
|
|
|
return path
|
|
|
@@ -521,10 +565,10 @@ class Grid(BaseClass):
|
|
|
if (x, y) == current.parent:
|
|
|
continue
|
|
|
|
|
|
- if not self.can_move_on((x, y)):
|
|
|
+ if not self.can_move_on(unit, (x, y)):
|
|
|
continue
|
|
|
|
|
|
- moving_cost = self.moving_cost((x, y))
|
|
|
+ moving_cost = self.moving_cost(unit, (x, y))
|
|
|
|
|
|
cost = current.cost + moving_cost
|
|
|
priority = cost + 10 * Grid.manhattan((x, y), target)
|
|
|
@@ -535,12 +579,12 @@ class Grid(BaseClass):
|
|
|
|
|
|
return None
|
|
|
|
|
|
- def discover(self, start, key, limit=5):
|
|
|
+ def discover(self, unit, key, limit=5):
|
|
|
paths = []
|
|
|
|
|
|
nodes = []
|
|
|
its, break_on = 0, 2000
|
|
|
- origin = DiscoveryNode(*start)
|
|
|
+ origin = DiscoveryNode(*unit.pos)
|
|
|
|
|
|
nodes.append(origin)
|
|
|
|
|
|
@@ -566,56 +610,78 @@ class Grid(BaseClass):
|
|
|
if pos in current.ancestors:
|
|
|
continue
|
|
|
|
|
|
- if not self.can_move_on(pos):
|
|
|
+ if not self.can_move_on(unit, pos):
|
|
|
continue
|
|
|
|
|
|
node = DiscoveryNode(*pos, current.ancestors + [current])
|
|
|
nodes.append(node)
|
|
|
return paths
|
|
|
|
|
|
- def discover_multiple(self, start, key, limit=5):
|
|
|
+ def get_conversion_path(self, unit, key, limit=5):
|
|
|
+ """ essaies de trouver le meilleur chemin pour relier des cases dont au moins une voisine valide
|
|
|
+ la condition 'key' (dans la limite de 'limit')"""
|
|
|
nodes = Queue()
|
|
|
- its, break_on = 0, 2000
|
|
|
- origin = DiscoveryNode(*start)
|
|
|
+ winners = Queue()
|
|
|
+ its, break_on = 0, 1000
|
|
|
+ origin = DiscoveryNode(*unit.pos)
|
|
|
+
|
|
|
+ abandon_at = 120 # number of paths explored
|
|
|
|
|
|
nodes.put(0, origin)
|
|
|
|
|
|
+ for n in self.neighbors(*unit.pos):
|
|
|
+ if key(n):
|
|
|
+ origin.matches.append(n)
|
|
|
+
|
|
|
while nodes:
|
|
|
+ its += 1
|
|
|
+ if its > break_on:
|
|
|
+ log(f"<!> get_conversion_path broke")
|
|
|
+ break
|
|
|
+
|
|
|
+ if len(nodes.items) > abandon_at:
|
|
|
+ log("> get_conversion_path early exit")
|
|
|
+ break
|
|
|
+
|
|
|
current = nodes.get()
|
|
|
|
|
|
- neighbors = self.neighbors(*current)
|
|
|
+ for pos in self.neighbors(*current):
|
|
|
+ if not self.can_move_on(unit, pos):
|
|
|
+ continue
|
|
|
|
|
|
- for pos in neighbors:
|
|
|
- its += 1
|
|
|
- if its > break_on:
|
|
|
- log(f"<!> discovery broke")
|
|
|
- break
|
|
|
+ moving_cost = 1
|
|
|
|
|
|
- if pos not in current.matches and key(pos):
|
|
|
- current.matches.append(pos)
|
|
|
- if len(current.matches) >= limit:
|
|
|
- break
|
|
|
+ matches = []
|
|
|
+ for n in self.neighbors(*pos):
|
|
|
+ if n not in current.matches and key(n):
|
|
|
+ matches.append(n)
|
|
|
|
|
|
- if not self.can_move_on(pos):
|
|
|
- continue
|
|
|
+ cost = current.cost + moving_cost
|
|
|
+ priority = 1000 * cost - (2000 * (len(current.matches) + len(matches)))
|
|
|
+ priority += 100 * len(
|
|
|
+ [a for a in current.ancestors if a == pos]) # décourage de revenir à une case visitée
|
|
|
|
|
|
node = DiscoveryNode(
|
|
|
- *pos,
|
|
|
+ pos[0],
|
|
|
+ pos[1],
|
|
|
+ cost,
|
|
|
current.ancestors + [current],
|
|
|
- current.ancestors[-1].matches if current.ancestors else None
|
|
|
+ current.matches + matches
|
|
|
)
|
|
|
|
|
|
- # on priorise le plus haut ratio matches / longueur de chemin (donc le plus bas inverse, pour la Queue).
|
|
|
- priority = (10 * (len(node.ancestors) + 1)) // (1 + len(node.matches))
|
|
|
+ if matches:
|
|
|
+ winners.put(40 * node.cost - 100 * len(node.matches), node)
|
|
|
|
|
|
nodes.put(priority, node)
|
|
|
|
|
|
if len(current.matches) >= limit or its > break_on:
|
|
|
break
|
|
|
|
|
|
- best_node = nodes.get()
|
|
|
- path = best_node.ancestors[:-1:-1] + [best_node]
|
|
|
- return path, best_node.matches
|
|
|
+ try:
|
|
|
+ best_node = winners.get()
|
|
|
+ except IndexError:
|
|
|
+ best_node = nodes.get()
|
|
|
+ return ConversionPath.make_from_discovery_node(best_node)
|
|
|
|
|
|
def _repr_cell(self, pos):
|
|
|
# return f"{self.control[pos]}/{self.threat[pos]}"
|
|
|
@@ -654,11 +720,12 @@ while 1:
|
|
|
# TODO: ajouter une action "s'interposer" où une unité s'interpose entre le leader et un ennemi ; à mettre en balance avec le 'take cover'
|
|
|
# TODO: remplacer le discover par un algo qui cherche le plus court chemin pour relier tous les neutres les uns après les autres
|
|
|
|
|
|
- log(f"start round {GRID.round}")
|
|
|
|
|
|
GRID.reinit_round()
|
|
|
for _ in range(int(input())):
|
|
|
GRID.update_unit(*[int(j) for j in input().split()])
|
|
|
+
|
|
|
+ log(f"start round {GRID.round}")
|
|
|
GRID.update()
|
|
|
|
|
|
actions = GRID.list_actions()
|