main.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. import heapq
  2. import sys
  3. import time
  4. debug = True
  5. t0 = time.time()
  6. def log(*msg):
  7. if debug:
  8. print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr, flush=True)
  9. def time_to(total, step):
  10. """ number of steps to reach total """
  11. return total // step + (1 if total % step > 0 else 0)
  12. class BaseClass:
  13. def __repr__(self):
  14. return f"<{self.__class__.__name__}: {self.__dict__}>"
  15. class Node(BaseClass):
  16. def __init__(self, pos, path=None):
  17. self.pos = pos
  18. self.path = path or []
  19. class PathNode(tuple):
  20. def __new__(cls, x, y, parent=None):
  21. n = tuple.__new__(cls, (x, y))
  22. n.parent = parent
  23. n.cost = 0
  24. return n
  25. def __repr__(self):
  26. return f"<{self[0]}, {self[1]}, c:{self.cost}>"
  27. class DiscoveryNode(tuple):
  28. def __new__(cls, x, y, ancestors=None, matches=None):
  29. n = tuple.__new__(cls, (x, y))
  30. n.ancestors = ancestors if ancestors is not None else []
  31. n.cost = 0
  32. n.matches = matches if matches is not None else []
  33. return n
  34. def __repr__(self):
  35. return f"<{self[0]}, {self[1]}, c:{self.cost}>"
  36. class Queue(BaseClass):
  37. def __init__(self):
  38. self.items = []
  39. def __bool__(self):
  40. return bool(self.items)
  41. def __repr__(self):
  42. return str(self.items)
  43. def values(self):
  44. return (v for _, v in self.items)
  45. def put(self, priority, item):
  46. while priority in [p for p, _ in self.items]:
  47. priority += 1
  48. heapq.heappush(self.items, (priority, item))
  49. def get(self):
  50. return heapq.heappop(self.items)[1]
  51. def get_items(self):
  52. return heapq.heappop(self.items)
  53. class Player(BaseClass):
  54. def __init__(self, id_):
  55. self.id = id_
  56. ME = Player(int(input())) # Input gives the player id: 0 plays first
  57. OPPONENT = Player(1 - ME.id)
  58. PLAYERS_INDEX = {p.id: p for p in [ME, OPPONENT]}
  59. PLAYERS_ORDER = sorted([ME, OPPONENT], key=lambda p: p.id)
  60. class Unit(BaseClass):
  61. TYPE_CULTIST = 0
  62. TYPE_CULT_LEADER = 1
  63. OWNER_0 = 0
  64. OWNER_1 = 1
  65. NO_OWNER = 2
  66. SHOOTING_RANGE = 6
  67. SHOOTING_MAX_DAMAGE = 7
  68. def __init__(self, id_):
  69. self.id = id_
  70. self.hp = 10
  71. self.x = None
  72. self.y = None
  73. self.owner = None
  74. @property
  75. def pos(self):
  76. return self.x, self.y
  77. @property
  78. def owned(self):
  79. return self.owner == ME.id
  80. @property
  81. def opponent(self):
  82. return self.owner == OPPONENT.id
  83. @property
  84. def neutral(self):
  85. return self.owner == self.NO_OWNER
  86. class CultLeader(Unit):
  87. pass
  88. class Action(BaseClass):
  89. pass
  90. class ActionWait(Action):
  91. def __repr__(self):
  92. return f"<ActionWait>"
  93. def exec(self):
  94. print("WAIT")
  95. class ActionMove(Action):
  96. def __init__(self, unit, pos, message=''):
  97. self.unit = unit
  98. self.pos = pos
  99. self.message = message
  100. def __repr__(self):
  101. return f"<ActionMove: {self.unit.id} to {self.pos} ({self.message})>"
  102. def exec(self):
  103. print(f"{self.unit.id} MOVE {self.pos[0]} {self.pos[1]}")
  104. class ActionShoot(Action):
  105. def __init__(self, unit, target):
  106. self.unit = unit
  107. self.target = target
  108. def __repr__(self):
  109. return f"<ActionShoot: {self.unit.id} to {self.target.id}>"
  110. def exec(self):
  111. print(f"{self.unit.id} SHOOT {self.target.id}")
  112. class ActionConvert(Action):
  113. def __init__(self, unit, target):
  114. self.unit = unit
  115. self.target = target
  116. def __repr__(self):
  117. return f"<ActionConvert: {self.unit.id} to {self.target.id}>"
  118. def exec(self):
  119. print(f"{self.unit.id} CONVERT {self.target.id}")
  120. class Grid(BaseClass):
  121. def __init__(self, width, height):
  122. self.width = width
  123. self.height = height
  124. self.cells = []
  125. self.obstacles = []
  126. self._neighbors = {}
  127. self.index = {}
  128. self.units = {}
  129. self.round = 0
  130. self.threat = {}
  131. self.control = {}
  132. self.heat_map = {}
  133. def pre_compute(self):
  134. self.cells = [(x, y) for x in range(self.width) for y in range(self.height)]
  135. for x, y in self.cells:
  136. self._neighbors[(x, y)] = [(xn, yn) for xn, yn in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if
  137. 0 <= xn < self.width and 0 <= yn < self.height]
  138. def reinit_round(self):
  139. self.units = {}
  140. def update_unit(self, id_, type_, hp, x, y, owner):
  141. self.units[id_] = Unit(id_) if type_ != Unit.TYPE_CULT_LEADER else CultLeader(id_)
  142. unit = self.units[id_]
  143. unit.hp = hp
  144. unit.x = x
  145. unit.y = y
  146. unit.owner = owner
  147. def update_threat_map(self):
  148. self.threat = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
  149. sources = self.opponent_cultists()
  150. # On ajoute les neutres voisins du leader ennemi aux sources de menace possible
  151. opponent_cult_leader = self.opponent_cult_leader()
  152. if opponent_cult_leader:
  153. for pos in self.neighbors(*opponent_cult_leader.pos):
  154. if pos in self.index and self.index[pos].neutral:
  155. sources.append(self.index[pos])
  156. for u in sources:
  157. shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
  158. for x, y in shooting_zone:
  159. dist = shooting_zone[(x, y)]
  160. if not self.line_of_sight(u.pos, (x, y)):
  161. continue
  162. threat = Unit.SHOOTING_RANGE + 1 - dist
  163. if u.neutral:
  164. threat //= 2
  165. if threat > self.threat[(x, y)]:
  166. self.threat[(x, y)] = threat
  167. def update_control(self):
  168. self.control = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
  169. for u in self.allied_cultists():
  170. shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
  171. for x, y in shooting_zone:
  172. dist = shooting_zone[(x, y)]
  173. if not self.line_of_sight(u.pos, (x, y)):
  174. continue
  175. control = Unit.SHOOTING_RANGE + 1 - dist
  176. if control > self.control[(x, y)]:
  177. self.control[(x, y)] = control
  178. def update_heat_map(self):
  179. lines = []
  180. self.heat_map = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
  181. cult_leader = self.cult_leader()
  182. for o in self.opponent_cultists():
  183. if cult_leader:
  184. lines += self.line(o.pos, cult_leader.pos)
  185. for n in self.neutrals():
  186. lines += self.line(o.pos, n.pos)
  187. for pos in lines:
  188. self.heat_map[pos] += 1
  189. def update(self):
  190. self.index = {}
  191. for unit in self.units.values():
  192. self.index[(unit.x, unit.y)] = unit
  193. self.update_threat_map()
  194. self.update_control()
  195. self.update_heat_map()
  196. def cult_leader(self):
  197. return next((u for u in self.units.values() if type(u) is CultLeader and u.owned), None)
  198. def allied_cultists(self):
  199. return [u for u in self.units.values() if type(u) is not CultLeader and u.owned]
  200. def owned_units(self):
  201. return [u for u in self.units.values() if u.owned]
  202. def opponent_units(self):
  203. return [u for u in self.units.values() if u.opponent]
  204. def opponent_cult_leader(self):
  205. return next((u for u in self.units.values() if type(u) is CultLeader and not u.owned), None)
  206. def opponent_cultists(self):
  207. return [u for u in self.units.values() if type(u) is not CultLeader and u.opponent]
  208. def neutrals(self):
  209. return [u for u in self.units.values() if u.neutral]
  210. def list_actions(self):
  211. actions = Queue()
  212. k_convert_neutrals = 10
  213. k_convert_danger = 30
  214. k_shoot_opponent_cultist = 10
  215. k_shoot_opponent_cult_leader = 5
  216. k0_protect_cult_leader = 30
  217. k_protect_threat_level = -5
  218. k_cover_threat = 10
  219. k_cover_interest = -3
  220. k0_position = 50
  221. k_position_distance = 10
  222. k_position_heat = -2
  223. k_position_danger = 5
  224. k_position_advantage = 10
  225. cult_leader = self.cult_leader()
  226. opponent_cult_leader = self.opponent_cult_leader()
  227. log(self.obstacles)
  228. log([n.pos for n in self.neutrals()])
  229. # compute conversion paths
  230. conversion_paths = []
  231. if cult_leader and self.neutrals():
  232. conversion_paths = self.discover(
  233. cult_leader.pos,
  234. key=(lambda pos: pos in self.index and self.index[pos].neutral),
  235. limit=min(5, len(self.neutrals()))
  236. )
  237. # Conversion des neutres
  238. if cult_leader:
  239. for path, target_pos in conversion_paths:
  240. target = self.index[target_pos]
  241. priority = 0
  242. priority += k_convert_neutrals * len(path)
  243. priority += k_convert_danger * sum([self.threat[pos] for pos in path])
  244. if target_pos in self.neighbors(*cult_leader.pos):
  245. action = ActionConvert(cult_leader, target)
  246. else:
  247. action = ActionMove(cult_leader, path[0], f'go convert {target.id}')
  248. actions.put(priority, action)
  249. # Attaque d'unités ennemies
  250. targets = self.opponent_cultists()
  251. if opponent_cult_leader:
  252. targets.append(opponent_cult_leader)
  253. advantage = sum([t.hp for t in targets]) < sum([u.hp for u in self.owned_units()])
  254. for a in self.allied_cultists():
  255. for u in targets:
  256. shooting_distance = self.shooting_distance(a.pos, u.pos)
  257. if shooting_distance and shooting_distance < u.SHOOTING_RANGE:
  258. action = ActionShoot(a, u)
  259. priority = (k_shoot_opponent_cult_leader if type(
  260. u) is CultLeader else k_shoot_opponent_cultist) * shooting_distance
  261. actions.put(priority, action)
  262. # Position
  263. for a in self.allied_cultists():
  264. # on garde les trois points les plus chauds
  265. hot_spots = sorted(self.heat_map.items(), key=lambda p: p[1], reverse=True)[:3]
  266. results = self.discover(a.pos, key=lambda x: x in [s[0] for s in hot_spots])
  267. for path, target_pos in results:
  268. if not path:
  269. break
  270. heat = self.heat_map[target_pos]
  271. priority = k0_position
  272. priority += k_position_heat * heat
  273. priority += k_position_distance * len(path)
  274. priority += k_position_danger * sum(self.threat[p] for p in path)
  275. action = ActionMove(a, path[0], f'pos on {target_pos}')
  276. actions.put(priority, action)
  277. # Mise en sécurité du chef
  278. if cult_leader:
  279. current_threat = self.threat[cult_leader.pos]
  280. if current_threat:
  281. covers = [n for n in self.neighbors(*cult_leader.pos) if self.can_move_on(n)]
  282. for pos in covers:
  283. action = ActionMove(cult_leader, pos, 'take cover')
  284. # interest is the number of conversion paths that start with this pos
  285. interest = len([p for p in conversion_paths if p[0] and p[0][0] == pos])
  286. priority = k0_protect_cult_leader
  287. priority += k_protect_threat_level * current_threat
  288. priority += k_cover_threat * self.threat[pos]
  289. priority += k_cover_interest * interest
  290. actions.put(priority, action)
  291. return actions
  292. def in_grid(self, pos):
  293. return 0 <= pos[0] < self.width and 0 <= pos[1] < self.height
  294. def can_see_trough(self, pos):
  295. return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
  296. def can_move_on(self, pos):
  297. return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
  298. def can_discover(self, pos):
  299. return self.in_grid(pos) and pos not in self.obstacles
  300. def moving_cost(self, pos):
  301. if not self.can_move_on(pos):
  302. return -1
  303. return 1 + self.threat[pos]
  304. @staticmethod
  305. def manhattan(from_, to_):
  306. xa, ya = from_
  307. xb, yb = to_
  308. return abs(xa - xb) + abs(ya - yb)
  309. def neighbors(self, x, y):
  310. return self._neighbors[(x, y)]
  311. def zone(self, pos, radius):
  312. x0, y0 = pos
  313. zone = {}
  314. for x in range(max(x0 - radius, 0), min(x0 + radius, self.width)):
  315. for y in range(max(y0 - radius, 0), min(y0 + radius, self.height)):
  316. dist = self.manhattan(pos, (x, y))
  317. if dist <= radius:
  318. zone[(x, y)] = dist
  319. return zone
  320. @classmethod
  321. def line(cls, from_, to_):
  322. """ Implementation of bresenham's algorithm """
  323. xa, ya = from_
  324. xb, yb = to_
  325. if (xa, ya) == (xb, yb):
  326. return [(xa, ya)]
  327. # diagonal symmetry
  328. vertically_oriented = (abs(yb - ya) > abs(xb - xa))
  329. if vertically_oriented:
  330. ya, xa, yb, xb = xa, ya, xb, yb
  331. # horizontal symmetry
  332. reversed_sym = (xa > xb)
  333. if reversed_sym:
  334. xb, yb, xa, ya = xa, ya, xb, yb
  335. # angle
  336. dx, dy = xb - xa, yb - ya
  337. alpha = (abs(dy) / dx)
  338. offset = 0.0
  339. step = 1 if dy > 0 else -1
  340. result = []
  341. y_ = ya
  342. for x_ in range(xa, xb + 1):
  343. result.append((y_, x_) if vertically_oriented else (x_, y_))
  344. offset += alpha
  345. if offset > 0.5:
  346. y_ += step
  347. offset -= 1.0
  348. if reversed_sym:
  349. result.reverse()
  350. return result
  351. def line_of_sight(self, from_, to_):
  352. line = self.line(from_, to_)[1:]
  353. return line if all(self.can_see_trough(c) for c in line[:-1]) else []
  354. def shooting_distance(self, from_, to_):
  355. return len(self.line_of_sight(from_, to_))
  356. def path(self, start, target):
  357. nodes = Queue()
  358. its, break_on = 0, 400
  359. origin = PathNode(*start)
  360. nodes.put(0, origin)
  361. while nodes:
  362. current = nodes.get()
  363. if current == target:
  364. path = []
  365. previous = current
  366. while previous:
  367. if previous != start:
  368. path.insert(0, previous)
  369. previous = previous.parent
  370. return path
  371. neighbors = self.neighbors(*current)
  372. for x, y in neighbors:
  373. its += 1
  374. if its > break_on:
  375. log("<!> pathfinding broken")
  376. return None
  377. if (x, y) == current.parent:
  378. continue
  379. if not self.can_move_on((x, y)):
  380. continue
  381. moving_cost = self.moving_cost((x, y))
  382. cost = current.cost + moving_cost
  383. priority = cost + 10 * Grid.manhattan((x, y), target)
  384. node = PathNode(x, y, current)
  385. node.cost = cost
  386. nodes.put(priority, node)
  387. return None
  388. def discover(self, start, key, limit=5):
  389. paths = []
  390. nodes = []
  391. its, break_on = 0, 2000
  392. origin = DiscoveryNode(*start)
  393. nodes.append(origin)
  394. while nodes:
  395. current = nodes.pop(0) # l'ordre est important, pour que les premiers indexés soient les premiers analysés
  396. neighbors = self.neighbors(*current)
  397. for pos in neighbors:
  398. its += 1
  399. if its > break_on:
  400. log(f"<!> discovery broke, {len(paths)} results")
  401. return paths
  402. if key(pos):
  403. path = current.ancestors[:-1:-1] + [current] # reverse ancestors after having removed the start
  404. paths.append((path, pos))
  405. if len(paths) >= limit:
  406. return paths
  407. continue
  408. if pos in current.ancestors:
  409. continue
  410. if not self.can_move_on(pos):
  411. continue
  412. node = DiscoveryNode(*pos, current.ancestors + [current])
  413. nodes.append(node)
  414. return paths
  415. def discover_multiple(self, start, key, limit=5):
  416. nodes = Queue()
  417. its, break_on = 0, 2000
  418. origin = DiscoveryNode(*start)
  419. nodes.put(0, origin)
  420. while nodes:
  421. current = nodes.get()
  422. neighbors = self.neighbors(*current)
  423. for pos in neighbors:
  424. its += 1
  425. if its > break_on:
  426. log(f"<!> discovery broke")
  427. break
  428. if pos not in current.matches and key(pos):
  429. current.matches.append(pos)
  430. if len(current.matches) >= limit:
  431. break
  432. if not self.can_move_on(pos):
  433. continue
  434. node = DiscoveryNode(
  435. *pos,
  436. current.ancestors + [current],
  437. current.ancestors[-1].matches if current.ancestors else None
  438. )
  439. # on priorise le plus haut ratio matches / longueur de chemin (donc le plus bas inverse, pour la Queue).
  440. priority = (10 * (len(node.ancestors) + 1)) // (1 + len(node.matches))
  441. nodes.put(priority, node)
  442. if len(current.matches) >= limit or its > break_on:
  443. break
  444. best_node = nodes.get()
  445. path = best_node.ancestors[:-1:-1] + [best_node]
  446. return path, best_node.matches
  447. def _repr_cell(self, pos):
  448. # return f"{self.control[pos]}/{self.threat[pos]}"
  449. # return self.heat_map[pos]
  450. if pos in self.obstacles:
  451. return "X"
  452. unit = self.index.get(pos, None)
  453. if type(unit) is CultLeader:
  454. return "C"
  455. elif unit is None:
  456. return "."
  457. else:
  458. return "U"
  459. def graph(self):
  460. return "\n".join(
  461. ["|".join([str(self._repr_cell((x, y))) for x in range(self.width)]) for y in range(self.height)])
  462. # Create grid
  463. GRID = Grid(*[int(i) for i in input().split()])
  464. obstacles_input = [input() for y in range(GRID.height)]
  465. GRID.obstacles = [(x, y) for y, row in enumerate(obstacles_input) for x, val in enumerate(row) if val == 'x']
  466. GRID.pre_compute()
  467. while 1:
  468. # TODO : en cas de choix entre plusieurs neutres à convertir, privilégier un neutre se trouvant entre un ennemi et le leader
  469. # 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
  470. # (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
  471. # 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
  472. # zone de menace
  473. # TODO: donner un coup à l'action "ne rien faire", car c'est parfois la meilleure chose à faire...
  474. # TODO: ajouter une action "s'interposer" où une unité s'interpose entre le leader et un ennemi ; à mettre en balance avec le 'take cover'
  475. # TODO: remplacer le discover par un algo qui cherche le plus court chemin pour relier tous les neutres les uns après les autres
  476. log(f"start round {GRID.round}")
  477. GRID.reinit_round()
  478. for _ in range(int(input())):
  479. GRID.update_unit(*[int(j) for j in input().split()])
  480. GRID.update()
  481. actions = GRID.list_actions()
  482. for action in actions.items:
  483. log(f"* {action}")
  484. # print("\n" + GRID.graph(), file=sys.stderr)
  485. try:
  486. action = actions.get()
  487. except IndexError:
  488. log("no action...")
  489. action = ActionWait()
  490. log(f"exec : {action}")
  491. action.exec()
  492. GRID.round += 1