| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682 |
- import heapq
- import sys
- import time
- debug = True
- t0 = time.time()
- def log(*msg):
- if debug:
- print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr, flush=True)
- def time_to(total, step):
- """ number of steps to reach total """
- return total // step + (1 if total % step > 0 else 0)
- class BaseClass:
- def __repr__(self):
- 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 DiscoveryNode(tuple):
- def __new__(cls, x, y, ancestors=None, matches=None):
- n = tuple.__new__(cls, (x, y))
- 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}>"
- class Queue(BaseClass):
- def __init__(self):
- self.items = []
- def __bool__(self):
- return bool(self.items)
- def __repr__(self):
- return str(self.items)
- def values(self):
- return (v for _, v in self.items)
- def put(self, priority, item):
- while priority in [p for p, _ in self.items]:
- priority += 1
- heapq.heappush(self.items, (priority, item))
- def get(self):
- return heapq.heappop(self.items)[1]
- def get_items(self):
- return heapq.heappop(self.items)
- class Player(BaseClass):
- def __init__(self, id_):
- self.id = id_
- ME = Player(int(input())) # Input gives the player id: 0 plays first
- OPPONENT = Player(1 - ME.id)
- PLAYERS_INDEX = {p.id: p for p in [ME, OPPONENT]}
- PLAYERS_ORDER = sorted([ME, OPPONENT], key=lambda p: p.id)
- class Unit(BaseClass):
- TYPE_CULTIST = 0
- TYPE_CULT_LEADER = 1
- OWNER_0 = 0
- OWNER_1 = 1
- NO_OWNER = 2
- SHOOTING_RANGE = 6
- SHOOTING_MAX_DAMAGE = 7
- def __init__(self, id_):
- self.id = id_
- self.hp = 10
- self.x = None
- self.y = None
- self.owner = None
- @property
- def pos(self):
- return self.x, self.y
- @property
- def owned(self):
- return self.owner == ME.id
- @property
- def opponent(self):
- return self.owner == OPPONENT.id
- @property
- def neutral(self):
- return self.owner == self.NO_OWNER
- class CultLeader(Unit):
- pass
- class Action(BaseClass):
- pass
- class ActionWait(Action):
- def __repr__(self):
- return f"<ActionWait>"
- def exec(self):
- print("WAIT")
- class ActionMove(Action):
- 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} ({self.message})>"
- def exec(self):
- print(f"{self.unit.id} MOVE {self.pos[0]} {self.pos[1]}")
- class ActionShoot(Action):
- def __init__(self, unit, target):
- self.unit = unit
- self.target = target
- def __repr__(self):
- return f"<ActionShoot: {self.unit.id} to {self.target.id}>"
- def exec(self):
- print(f"{self.unit.id} SHOOT {self.target.id}")
- class ActionConvert(Action):
- def __init__(self, unit, target):
- self.unit = unit
- self.target = target
- def __repr__(self):
- return f"<ActionConvert: {self.unit.id} to {self.target.id}>"
- def exec(self):
- print(f"{self.unit.id} CONVERT {self.target.id}")
- 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)]
- 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):
- self.units[id_] = Unit(id_) if type_ != Unit.TYPE_CULT_LEADER else CultLeader(id_)
- unit = self.units[id_]
- unit.hp = hp
- unit.x = x
- unit.y = y
- unit.owner = owner
- def update_threat_map(self):
- self.threat = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
- sources = self.opponent_cultists()
- # On ajoute les neutres voisins du leader ennemi aux sources de menace possible
- opponent_cult_leader = self.opponent_cult_leader()
- if opponent_cult_leader:
- for pos in self.neighbors(*opponent_cult_leader.pos):
- if pos in self.index and self.index[pos].neutral:
- sources.append(self.index[pos])
- for u in sources:
- 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 u.neutral:
- threat //= 2
- 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
- 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]
- def owned_units(self):
- return [u for u in self.units.values() if u.owned]
- def opponent_units(self):
- return [u for u in self.units.values() if u.opponent]
- def opponent_cult_leader(self):
- return next((u for u in self.units.values() if type(u) is CultLeader and not u.owned), None)
- def opponent_cultists(self):
- return [u for u in self.units.values() if type(u) is not CultLeader and u.opponent]
- def neutrals(self):
- return [u for u in self.units.values() if u.neutral]
- def list_actions(self):
- actions = Queue()
- k_convert_neutrals = 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
- k0_position = 50
- k_position_distance = 10
- k_position_heat = -2
- k_position_danger = 5
- k_position_advantage = 10
- cult_leader = self.cult_leader()
- opponent_cult_leader = self.opponent_cult_leader()
- log(self.obstacles)
- log([n.pos for n in self.neutrals()])
- # compute conversion paths
- conversion_paths = []
- if cult_leader and self.neutrals():
- conversion_paths = self.discover(
- cult_leader.pos,
- key=(lambda pos: pos in self.index and self.index[pos].neutral),
- limit=min(5, len(self.neutrals()))
- )
- # Conversion des neutres
- if cult_leader:
- for path, target_pos in conversion_paths:
- target = self.index[target_pos]
- priority = 0
- priority += k_convert_neutrals * len(path)
- priority += k_convert_danger * sum([self.threat[pos] for pos in path])
- if target_pos 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
- targets = self.opponent_cultists()
- if opponent_cult_leader:
- targets.append(opponent_cult_leader)
- advantage = sum([t.hp for t in targets]) < sum([u.hp for u in self.owned_units()])
- for a in self.allied_cultists():
- for u in targets:
- shooting_distance = self.shooting_distance(a.pos, u.pos)
- if shooting_distance and shooting_distance < u.SHOOTING_RANGE:
- action = ActionShoot(a, u)
- priority = (k_shoot_opponent_cult_leader if type(
- u) is CultLeader else k_shoot_opponent_cultist) * shooting_distance
- actions.put(priority, action)
- # 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]
- 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
- heat = self.heat_map[target_pos]
- 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)
- action = ActionMove(a, path[0], f'pos on {target_pos}')
- actions.put(priority, action)
- # Mise en sécurité du chef
- 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)]
- 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])
- priority = k0_protect_cult_leader
- priority += k_protect_threat_level * current_threat
- priority += k_cover_threat * self.threat[pos]
- priority += k_cover_interest * interest
- actions.put(priority, action)
- return actions
- def in_grid(self, pos):
- return 0 <= pos[0] < self.width and 0 <= pos[1] < self.height
- 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_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_):
- xa, ya = from_
- 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 """
- xa, ya = from_
- xb, yb = to_
- if (xa, ya) == (xb, yb):
- return [(xa, ya)]
- # diagonal symmetry
- vertically_oriented = (abs(yb - ya) > abs(xb - xa))
- if vertically_oriented:
- ya, xa, yb, xb = xa, ya, xb, yb
- # horizontal symmetry
- reversed_sym = (xa > xb)
- if reversed_sym:
- xb, yb, xa, ya = xa, ya, xb, yb
- # angle
- dx, dy = xb - xa, yb - ya
- alpha = (abs(dy) / dx)
- offset = 0.0
- step = 1 if dy > 0 else -1
- result = []
- y_ = ya
- for x_ in range(xa, xb + 1):
- result.append((y_, x_) if vertically_oriented else (x_, y_))
- offset += alpha
- if offset > 0.5:
- y_ += step
- offset -= 1.0
- if reversed_sym:
- result.reverse()
- return result
- def line_of_sight(self, from_, to_):
- line = self.line(from_, to_)[1:]
- return line if all(self.can_see_trough(c) for c in line[:-1]) else []
- 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, 400
- 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 = []
- nodes = []
- its, break_on = 0, 2000
- origin = DiscoveryNode(*start)
- nodes.append(origin)
- while nodes:
- current = nodes.pop(0) # l'ordre est important, pour que les premiers indexés soient les premiers analysés
- neighbors = self.neighbors(*current)
- for pos in neighbors:
- its += 1
- if its > break_on:
- log(f"<!> discovery broke, {len(paths)} results")
- return paths
- if key(pos):
- path = current.ancestors[:-1:-1] + [current] # reverse ancestors after having removed the start
- paths.append((path, pos))
- if len(paths) >= limit:
- return paths
- continue
- if pos in current.ancestors:
- continue
- if not self.can_move_on(pos):
- continue
- node = DiscoveryNode(*pos, current.ancestors + [current])
- nodes.append(node)
- return paths
- def discover_multiple(self, start, key, limit=5):
- nodes = Queue()
- its, break_on = 0, 2000
- origin = DiscoveryNode(*start)
- nodes.put(0, origin)
- while nodes:
- current = nodes.get()
- neighbors = self.neighbors(*current)
- for pos in neighbors:
- its += 1
- if its > break_on:
- log(f"<!> discovery broke")
- break
- if pos not in current.matches and key(pos):
- current.matches.append(pos)
- if len(current.matches) >= limit:
- break
- if not self.can_move_on(pos):
- continue
- node = DiscoveryNode(
- *pos,
- current.ancestors + [current],
- current.ancestors[-1].matches if current.ancestors else None
- )
- # 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))
- 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
- 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()])
- 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 : en cas de choix entre plusieurs neutres à convertir, privilégier un neutre se trouvant entre un ennemi et le leader
- # TODO: Laisser l'algo de découverte des cibles à convertir passer outre les alliés, ajouter une action "dégager le passage" pour ces unités
- # (prévoir le cas où cet allié ne peut pas se dégager). Le fait de passer outre un allié doit augmenter le coût de mouvement de 1
- # TODO : Etre plus prudent dans le positionnement des unités; ne pas s'attaquer à plus fort que soi, et décourager plus fortement l'entrée dans une
- # zone de menace
- # TODO: donner un coup à l'action "ne rien faire", car c'est parfois la meilleure chose à faire...
- # 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()])
- 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:
- log("no action...")
- action = ActionWait()
- log(f"exec : {action}")
- action.exec()
- GRID.round += 1
|