main.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  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 DiscoverNode(tuple):
  28. def __new__(cls, x, y, ancestors=None):
  29. n = tuple.__new__(cls, (x, y))
  30. n.ancestors = ancestors if ancestors is not None else []
  31. n.cost = 0
  32. return n
  33. def __repr__(self):
  34. return f"<{self[0]}, {self[1]}, c:{self.cost}>"
  35. class Queue(BaseClass):
  36. def __init__(self):
  37. self.items = []
  38. def __bool__(self):
  39. return bool(self.items)
  40. def __repr__(self):
  41. return str(self.items)
  42. def values(self):
  43. return (v for _, v in self.items)
  44. def put(self, priority, item):
  45. while priority in [p for p, _ in self.items]:
  46. priority += 1
  47. heapq.heappush(self.items, (priority, item))
  48. def get(self):
  49. return heapq.heappop(self.items)[1]
  50. def get_items(self):
  51. return heapq.heappop(self.items)
  52. class Player(BaseClass):
  53. def __init__(self, id_):
  54. self.id = id_
  55. ME = Player(int(input())) # Input gives the player id: 0 plays first
  56. OPPONENT = Player(1 - ME.id)
  57. PLAYERS_INDEX = {p.id: p for p in [ME, OPPONENT]}
  58. PLAYERS_ORDER = sorted([ME, OPPONENT], key=lambda p: p.id)
  59. class Unit(BaseClass):
  60. TYPE_CULTIST = 0
  61. TYPE_CULT_LEADER = 1
  62. OWNER_0 = 0
  63. OWNER_1 = 1
  64. NO_OWNER = 2
  65. SHOOTING_RANGE = 6
  66. SHOOTING_MAX_DAMAGE = 7
  67. def __init__(self, id_):
  68. self.id = id_
  69. self.hp = 10
  70. self.x = None
  71. self.y = None
  72. self.owner = None
  73. @property
  74. def pos(self):
  75. return self.x, self.y
  76. @property
  77. def owned(self):
  78. return self.owner == ME.id
  79. @property
  80. def opponent(self):
  81. return self.owner == OPPONENT.id
  82. @property
  83. def neutral(self):
  84. return self.owner == self.NO_OWNER
  85. class CultLeader(Unit):
  86. pass
  87. class Action(BaseClass):
  88. pass
  89. class ActionWait(Action):
  90. def __repr__(self):
  91. return f"<ActionWait>"
  92. def exec(self):
  93. print("WAIT")
  94. class ActionMove(Action):
  95. def __init__(self, unit, pos, message=''):
  96. self.unit = unit
  97. self.pos = pos
  98. self.message = message
  99. def __repr__(self):
  100. return f"<ActionMove: {self.unit.id} to {self.pos} ({self.message})>"
  101. def exec(self):
  102. print(f"{self.unit.id} MOVE {self.pos[0]} {self.pos[1]}")
  103. class ActionShoot(Action):
  104. def __init__(self, unit, target):
  105. self.unit = unit
  106. self.target = target
  107. def __repr__(self):
  108. return f"<ActionShoot: {self.unit.id} to {self.target.id}>"
  109. def exec(self):
  110. print(f"{self.unit.id} SHOOT {self.target.id}")
  111. class ActionConvert(Action):
  112. def __init__(self, unit, target):
  113. self.unit = unit
  114. self.target = target
  115. def __repr__(self):
  116. return f"<ActionConvert: {self.unit.id} to {self.target.id}>"
  117. def exec(self):
  118. print(f"{self.unit.id} CONVERT {self.target.id}")
  119. class Grid(BaseClass):
  120. def __init__(self, width, height):
  121. self.width = width
  122. self.height = height
  123. self.cells = []
  124. self.obstacles = []
  125. self._neighbors = {}
  126. self.index = {}
  127. self.units = {}
  128. self.round = 0
  129. self.threat = {}
  130. self.control = {}
  131. self.heat_map = {}
  132. def pre_compute(self):
  133. self.cells = [(x, y) for x in range(self.width) for y in range(self.height)]
  134. for x, y in self.cells:
  135. self._neighbors[(x, y)] = [(xn, yn) for xn, yn in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if
  136. 0 <= xn < self.width and 0 <= yn < self.height]
  137. def reinit_round(self):
  138. self.units = {}
  139. def update_unit(self, id_, type_, hp, x, y, owner):
  140. self.units[id_] = Unit(id_) if type_ != Unit.TYPE_CULT_LEADER else CultLeader(id_)
  141. unit = self.units[id_]
  142. unit.hp = hp
  143. unit.x = x
  144. unit.y = y
  145. unit.owner = owner
  146. def update_threat_map(self):
  147. self.threat = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
  148. for u in self.opponent_cultists():
  149. shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
  150. for x, y in shooting_zone:
  151. dist = shooting_zone[(x, y)]
  152. if not self.line_of_sight(u.pos, (x, y)):
  153. continue
  154. threat = Unit.SHOOTING_RANGE + 1 - dist
  155. if threat > self.threat[(x, y)]:
  156. self.threat[(x, y)] = threat
  157. def update_control(self):
  158. self.control = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
  159. for u in self.allied_cultists():
  160. shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
  161. for x, y in shooting_zone:
  162. dist = shooting_zone[(x, y)]
  163. if not self.line_of_sight(u.pos, (x, y)):
  164. continue
  165. control = Unit.SHOOTING_RANGE + 1 - dist
  166. if control > self.control[(x, y)]:
  167. self.control[(x, y)] = control
  168. def update_heat_map(self):
  169. lines = []
  170. self.heat_map = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
  171. cult_leader = self.cult_leader()
  172. for o in self.opponent_cultists():
  173. if cult_leader:
  174. lines += self.line(o.pos, cult_leader.pos)
  175. for n in self.neutrals():
  176. lines += self.line(o.pos, n.pos)
  177. for pos in lines:
  178. self.heat_map[pos] += 1
  179. def update(self):
  180. self.index = {}
  181. for unit in self.units.values():
  182. self.index[(unit.x, unit.y)] = unit
  183. self.update_threat_map()
  184. self.update_control()
  185. self.update_heat_map()
  186. def cult_leader(self):
  187. return next((u for u in self.units.values() if type(u) is CultLeader and u.owned), None)
  188. def allied_cultists(self):
  189. return [u for u in self.units.values() if type(u) is not CultLeader and u.owned]
  190. def opponent_units(self):
  191. return [u for u in self.units.values() if u.opponent]
  192. def opponent_cult_leader(self):
  193. return [u for u in self.units.values() if type(u) is CultLeader and not u.owned]
  194. def opponent_cultists(self):
  195. return [u for u in self.units.values() if type(u) is not CultLeader and u.opponent]
  196. def neutrals(self):
  197. return [u for u in self.units.values() if u.neutral]
  198. def list_actions(self):
  199. actions = Queue()
  200. k_convert_neutrals = 10
  201. k_convert_danger = 30
  202. k_shoot_opponent_cultist = 20
  203. k_shoot_opponent_cult_leader = 10
  204. k0_protect_cult_leader = 10
  205. k0_position = 50
  206. k_position_distance = 10
  207. k_position_heat = -5
  208. cult_leader = self.cult_leader()
  209. # Conversion des neutres
  210. if cult_leader:
  211. paths = self.discover(
  212. cult_leader.pos,
  213. key=(lambda pos: pos in self.index and self.index[pos].neutral),
  214. limit=5
  215. )
  216. for path in paths:
  217. log(path)
  218. for path in paths:
  219. target = self.index[path[-1]]
  220. priority = 0
  221. priority += k_convert_neutrals * len(path)
  222. priority += k_convert_danger * sum([self.threat[pos] for pos in path])
  223. if target in self.neighbors(*cult_leader.pos):
  224. action = ActionConvert(cult_leader, target)
  225. else:
  226. action = ActionMove(cult_leader, path[0], f'go convert {target.id}')
  227. actions.put(priority, action)
  228. # Attaque d'unités ennemies
  229. for a in self.allied_cultists():
  230. for u in self.opponent_cultists():
  231. shooting_distance = self.shooting_distance(a.pos, u.pos)
  232. if shooting_distance and shooting_distance < u.SHOOTING_RANGE:
  233. action = ActionShoot(a, u)
  234. priority = (k_shoot_opponent_cult_leader if type(
  235. u) is CultLeader else k_shoot_opponent_cultist) * shooting_distance
  236. actions.put(priority, action)
  237. # Position
  238. for a in self.allied_cultists():
  239. # on garde les trois points les plus chauds
  240. hot_spots = sorted(self.heat_map.items(), key=lambda p: p[1], reverse=True)[:3]
  241. for pos, heat in hot_spots:
  242. action = ActionMove(a, pos)
  243. path = self.path(a.pos, pos)
  244. safe_path = []
  245. for step in path:
  246. if self.threat[step] > 1:
  247. break
  248. safe_path.append(step)
  249. if not safe_path:
  250. continue
  251. priority = k0_position
  252. priority += k_position_heat * heat
  253. priority += k_position_distance * len(path)
  254. actions.put(priority, action)
  255. # Mise en sécurité du chef
  256. if cult_leader:
  257. current_threat = self.threat[cult_leader.pos]
  258. if current_threat:
  259. target = min(
  260. [n for n in self.neighbors(*cult_leader.pos) if self.can_move_on(n)],
  261. key=lambda x: self.threat[x]
  262. )
  263. action = ActionMove(cult_leader, target)
  264. priority = k0_protect_cult_leader
  265. actions.put(priority, action)
  266. return actions
  267. def in_grid(self, pos):
  268. return 0 <= pos[0] < self.width and 0 <= pos[1] < self.height
  269. def can_see_trough(self, pos):
  270. return self.in_grid(pos) and pos not in self.obstacles + list(self.index.values())
  271. def can_move_on(self, pos):
  272. return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
  273. def can_discover(self, pos):
  274. return self.in_grid(pos) and pos not in self.obstacles
  275. def moving_cost(self, pos):
  276. if not self.can_move_on(pos):
  277. return -1
  278. return 1 + self.threat[pos]
  279. @staticmethod
  280. def manhattan(from_, to_):
  281. xa, ya = from_
  282. xb, yb = to_
  283. return abs(xa - xb) + abs(ya - yb)
  284. def neighbors(self, x, y):
  285. return self._neighbors[(x, y)]
  286. def zone(self, pos, radius):
  287. x0, y0 = pos
  288. zone = {}
  289. for x in range(max(x0 - radius, 0), min(x0 + radius, self.width)):
  290. for y in range(max(y0 - radius, 0), min(y0 + radius, self.height)):
  291. dist = self.manhattan(pos, (x, y))
  292. if dist <= radius:
  293. zone[(x, y)] = dist
  294. return zone
  295. @classmethod
  296. def line(cls, from_, to_):
  297. """ Implementation of bresenham's algorithm """
  298. xa, ya = from_
  299. xb, yb = to_
  300. if (xa, ya) == (xb, yb):
  301. return [(xa, ya)]
  302. # diagonal symmetry
  303. vertically_oriented = (abs(yb - ya) > abs(xb - xa))
  304. if vertically_oriented:
  305. ya, xa, yb, xb = xa, ya, xb, yb
  306. # horizontal symmetry
  307. reversed_sym = (xa > xb)
  308. if reversed_sym:
  309. xb, yb, xa, ya = xa, ya, xb, yb
  310. # angle
  311. dx, dy = xb - xa, yb - ya
  312. alpha = (abs(dy) / dx)
  313. offset = 0.0
  314. step = 1 if dy > 0 else -1
  315. result = []
  316. y_ = ya
  317. for x_ in range(xa, xb + 1):
  318. result.append((y_, x_) if vertically_oriented else (x_, y_))
  319. offset += alpha
  320. if offset > 0.5:
  321. y_ += step
  322. offset -= 1.0
  323. if reversed_sym:
  324. result.reverse()
  325. return result
  326. def line_of_sight(self, from_, to_):
  327. line = self.line(from_, to_)
  328. return line if all(self.can_see_trough(c) for c in line) else []
  329. def shooting_distance(self, from_, to_):
  330. return len(self.line_of_sight(from_, to_))
  331. def path(self, start, target):
  332. nodes = Queue()
  333. its, break_on = 0, 300
  334. origin = PathNode(*start)
  335. nodes.put(0, origin)
  336. while nodes:
  337. current = nodes.get()
  338. if current == target:
  339. path = []
  340. previous = current
  341. while previous:
  342. if previous != start:
  343. path.insert(0, previous)
  344. previous = previous.parent
  345. return path
  346. neighbors = self.neighbors(*current)
  347. for x, y in neighbors:
  348. its += 1
  349. if its > break_on:
  350. log("<!> pathfinding broken")
  351. return None
  352. if (x, y) == current.parent:
  353. continue
  354. if not self.can_move_on((x, y)):
  355. continue
  356. moving_cost = self.moving_cost((x, y))
  357. cost = current.cost + moving_cost
  358. priority = cost + 10 * Grid.manhattan((x, y), target)
  359. node = PathNode(x, y, current)
  360. node.cost = cost
  361. nodes.put(priority, node)
  362. return None
  363. def discover(self, start, key, limit=5):
  364. paths = []
  365. found = []
  366. nodes = Queue()
  367. its, break_on = 0, 2000
  368. origin = DiscoverNode(*start)
  369. nodes.put(0, origin)
  370. while nodes:
  371. current = nodes.get()
  372. if current not in found and current != start and key(tuple(current)):
  373. path = []
  374. previous = current
  375. while previous.ancestors:
  376. if previous != start:
  377. path.insert(0, previous)
  378. previous = previous.ancestors[-1]
  379. found.append(path[-1])
  380. paths.append(path)
  381. if len(paths) >= limit:
  382. return paths
  383. neighbors = self.neighbors(*current)
  384. for x, y in neighbors:
  385. its += 1
  386. if its > break_on:
  387. log("<!> discovering ended earlier than expected")
  388. return paths
  389. if (x, y) in current.ancestors:
  390. continue
  391. if not self.can_discover((x, y)):
  392. continue
  393. moving_cost = self.moving_cost((x, y))
  394. cost = current.cost + moving_cost
  395. priority = cost + Grid.manhattan((x, y), start)
  396. ancestors = current.ancestors + [current]
  397. node = DiscoverNode(x, y, ancestors)
  398. node.cost = cost
  399. nodes.put(priority, node)
  400. return paths
  401. def _repr_cell(self, pos):
  402. # return f"{self.control[pos]}/{self.threat[pos]}"
  403. # return self.heat_map[pos]
  404. if pos in self.obstacles:
  405. return "X"
  406. unit = self.index.get(pos, None)
  407. if type(unit) is CultLeader:
  408. return "C"
  409. elif unit is None:
  410. return "."
  411. else:
  412. return "U"
  413. def graph(self):
  414. return "\n".join(
  415. ["|".join([str(self._repr_cell((x, y))) for x in range(self.width)]) for y in range(self.height)])
  416. # Create grid
  417. GRID = Grid(*[int(i) for i in input().split()])
  418. obstacles_input = [input() for y in range(GRID.height)]
  419. GRID.obstacles = [(x, y) for y, row in enumerate(obstacles_input) for x, val in enumerate(row) if val == 'x']
  420. GRID.pre_compute()
  421. while 1:
  422. # TODO: prendre en compte le terrain dans la ligne de visée et les déplacements
  423. log(f"start round {GRID.round}")
  424. GRID.reinit_round()
  425. for _ in range(int(input())):
  426. GRID.update_unit(*[int(j) for j in input().split()])
  427. GRID.update()
  428. actions = GRID.list_actions()
  429. for action in actions.items:
  430. log(f"* {action}")
  431. # print("\n" + GRID.graph(), file=sys.stderr)
  432. try:
  433. action = actions.get()
  434. except IndexError:
  435. log("no action...")
  436. action = ActionWait()
  437. log(f"exec : {action}")
  438. action.exec()
  439. GRID.round += 1