main.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  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, cost=0, ancestors=None, matches=None):
  29. n = tuple.__new__(cls, (x, y))
  30. n.cost = cost
  31. n.ancestors = ancestors if ancestors is not None else []
  32. n.matches = matches if matches is not None else []
  33. return n
  34. def __repr__(self):
  35. return f"<{self[0]}, {self[1]}>"
  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 ConversionStep:
  54. def __init__(self):
  55. self.pos = None
  56. self.candidates = []
  57. def __repr__(self):
  58. return f"<{self.pos}, c:{self.candidates}>"
  59. class ConversionPath:
  60. def __init__(self):
  61. self.steps = []
  62. def __repr__(self):
  63. return f"<{self.steps}>"
  64. @classmethod
  65. def make_from_discovery_node(cls, node):
  66. nodes = node.ancestors + [node]
  67. path = cls()
  68. found = []
  69. for node in nodes:
  70. step = ConversionStep()
  71. step.pos = tuple(node)
  72. for m in node.matches:
  73. if m in found:
  74. continue
  75. step.candidates.append(m)
  76. found.append(m)
  77. path.steps.append(step)
  78. return path
  79. def next_candidate(self):
  80. path = []
  81. for step in self.steps:
  82. path.append(step)
  83. if step.candidates:
  84. return path
  85. return None
  86. class Player(BaseClass):
  87. def __init__(self, id_):
  88. self.id = id_
  89. ME = Player(int(input())) # Input gives the player id: 0 plays first
  90. OPPONENT = Player(1 - ME.id)
  91. PLAYERS_INDEX = {p.id: p for p in [ME, OPPONENT]}
  92. PLAYERS_ORDER = sorted([ME, OPPONENT], key=lambda p: p.id)
  93. class Threat(BaseClass):
  94. def __init__(self, unit, damages, target, line):
  95. self.unit = unit
  96. self.damages = damages
  97. self.target = target
  98. self.line = line if line else []
  99. self.fatal = damages >= target.hp
  100. def __repr__(self):
  101. return f"<Threat (unit: {self.unit.id}, damages={self.damages}, target={self.target.id}, fatal={self.fatal}, line={self.line})>"
  102. class Unit(BaseClass):
  103. TYPE_CULTIST = 0
  104. TYPE_CULT_LEADER = 1
  105. OWNER_0 = 0
  106. OWNER_1 = 1
  107. NO_OWNER = 2
  108. SHOOTING_RANGE = 6
  109. SHOOTING_MAX_DAMAGE = 7
  110. def __init__(self, id_):
  111. self.id = id_
  112. self.hp = 10
  113. self.x = None
  114. self.y = None
  115. self.owner = None
  116. self.threats = []
  117. self.threaten_by = []
  118. @property
  119. def pos(self):
  120. return self.x, self.y
  121. @property
  122. def owned(self):
  123. return self.owner == ME.id
  124. @property
  125. def opponent(self):
  126. return self.owner == OPPONENT.id
  127. @property
  128. def neutral(self):
  129. return self.owner == self.NO_OWNER
  130. class CultLeader(Unit):
  131. pass
  132. class Action(BaseClass):
  133. pass
  134. class ActionWait(Action):
  135. def __repr__(self):
  136. return f"<ActionWait>"
  137. def exec(self):
  138. print("WAIT")
  139. class ActionMove(Action):
  140. def __init__(self, unit, pos, message=''):
  141. self.unit = unit
  142. self.pos = pos
  143. self.message = message
  144. def __repr__(self):
  145. return f"<ActionMove: {self.unit.id} to {self.pos} ({self.message})>"
  146. def exec(self):
  147. print(f"{self.unit.id} MOVE {self.pos[0]} {self.pos[1]}")
  148. class ActionShoot(Action):
  149. def __init__(self, unit, target, message=''):
  150. self.unit = unit
  151. self.target = target
  152. self.message = message
  153. def __repr__(self):
  154. return f"<ActionShoot: {self.unit.id} to {self.target.id} ({self.message})>"
  155. def exec(self):
  156. print(f"{self.unit.id} SHOOT {self.target.id}")
  157. class ActionConvert(Action):
  158. def __init__(self, unit, target):
  159. self.unit = unit
  160. self.target = target
  161. def __repr__(self):
  162. return f"<ActionConvert: {self.unit.id} to {self.target.id}>"
  163. def exec(self):
  164. print(f"{self.unit.id} CONVERT {self.target.id}")
  165. class Grid(BaseClass):
  166. def __init__(self, width, height):
  167. self.width = width
  168. self.height = height
  169. self.round = 1
  170. self.cells = []
  171. self.obstacles = []
  172. self._neighbors = {}
  173. self.index = {}
  174. self.units = {}
  175. self.threat = {}
  176. self.conversion_path = ConversionPath()
  177. self.cult_leader = None
  178. self.opponent_cult_leader = None
  179. self.allied_cultists = []
  180. self.owned_units = []
  181. self.opponent_units = []
  182. self.opponent_cultists = []
  183. self.neutrals = []
  184. def pre_compute(self):
  185. self.cells = [(x, y) for x in range(self.width) for y in range(self.height)]
  186. for x, y in self.cells:
  187. self._neighbors[(x, y)] = [(xn, yn) for xn, yn in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if
  188. 0 <= xn < self.width and 0 <= yn < self.height]
  189. def reinit_round(self):
  190. self.units = {}
  191. def update_unit(self, id_, type_, hp, x, y, owner):
  192. self.units[id_] = Unit(id_) if type_ != Unit.TYPE_CULT_LEADER else CultLeader(id_)
  193. unit = self.units[id_]
  194. unit.hp = hp
  195. unit.x = x
  196. unit.y = y
  197. unit.owner = owner
  198. def update_index(self):
  199. self.index = {}
  200. self.cult_leader = None
  201. self.opponent_cult_leader = None
  202. self.allied_cultists = []
  203. self.owned_units = []
  204. self.opponent_units = []
  205. self.opponent_cultists = []
  206. self.neutrals = []
  207. for unit in self.units.values():
  208. self.index[(unit.x, unit.y)] = unit
  209. if unit.owner == ME.id:
  210. self.owned_units.append(unit)
  211. if type(unit) is CultLeader:
  212. self.cult_leader = unit
  213. else:
  214. self.allied_cultists.append(unit)
  215. elif unit.owner == OPPONENT.id:
  216. self.opponent_units.append(unit)
  217. if type(unit) is CultLeader:
  218. self.opponent_cult_leader = unit
  219. else:
  220. self.opponent_cultists.append(unit)
  221. else:
  222. self.neutrals.append(unit)
  223. def update_threat_map(self):
  224. self.threat = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
  225. sources = self.opponent_cultists
  226. # On ajoute les neutres voisins du leader ennemi aux sources de menace possible
  227. if self.opponent_cult_leader:
  228. for pos in self.neighbors(*self.opponent_cult_leader.pos):
  229. if pos in self.index and self.index[pos].neutral:
  230. sources.append(self.index[pos])
  231. for u in sources:
  232. shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
  233. for x, y in shooting_zone:
  234. dist = shooting_zone[(x, y)]
  235. if not self.line_of_sight(u.pos, (x, y)):
  236. continue
  237. threat = Unit.SHOOTING_RANGE + 1 - dist
  238. if u.neutral:
  239. threat //= 2
  240. if threat > self.threat[(x, y)]:
  241. self.threat[(x, y)] = threat
  242. for u in self.opponent_units:
  243. u.threat = []
  244. for a in self.owned_units:
  245. line_of_sight = self.line_of_sight(u.pos, a.pos)
  246. if not line_of_sight:
  247. continue
  248. dist = self.manhattan(u.pos, a.pos)
  249. if dist <= u.SHOOTING_RANGE:
  250. damages = u.SHOOTING_MAX_DAMAGE - dist
  251. threat = Threat(u, damages, a, line_of_sight)
  252. u.threats.append(threat)
  253. a.threaten_by.append(threat)
  254. for a in self.owned_units:
  255. a.threat = []
  256. for u in self.opponent_units:
  257. line_of_sight = self.line_of_sight(a.pos, u.pos)
  258. if not line_of_sight:
  259. continue
  260. dist = self.manhattan(u.pos, a.pos)
  261. if dist <= u.SHOOTING_RANGE:
  262. damages = u.SHOOTING_MAX_DAMAGE - dist
  263. threat = Threat(a, damages, u, line_of_sight)
  264. a.threats.append(threat)
  265. u.threaten_by.append(threat)
  266. def compute_conversion_path(self):
  267. conversion_path = ConversionPath()
  268. if self.cult_leader and self.neutrals:
  269. conversion_path = self.get_conversion_path(
  270. self.cult_leader,
  271. key=(lambda pos: pos in self.index and self.index[pos].neutral),
  272. limit=min(4, len(self.neutrals))
  273. )
  274. log(f"conversion : {conversion_path}")
  275. self.conversion_path = conversion_path
  276. def update(self):
  277. log('update indexes')
  278. self.update_index()
  279. self.update_threat_map()
  280. self.compute_conversion_path()
  281. log('indexes updated')
  282. # log(self.obstacles + [u.pos for u in self.allied_cultists])
  283. # log([n.pos for n in self.neutrals])
  284. def build_actions(self):
  285. actions = Queue()
  286. advantage = len(self.owned_units) > len(self.opponent_units)
  287. sure_advantage = len(self.owned_units) > (len(self.opponent_units) + len(self.neutrals))
  288. for u in self.allied_cultists:
  289. if u.threats:
  290. log(f"Threats : {u.id} - {u.threats}")
  291. if u.threaten_by:
  292. log(f"Threaten : {u.id} - {u.threaten_by}")
  293. # Leader take cover
  294. k0_protect_cult_leader = 30
  295. k_protect_threat_level = -5
  296. k_protect_worth_risk = 3
  297. k_cover_interest = -10
  298. if self.cult_leader:
  299. current_threat = self.threat[self.cult_leader.pos]
  300. if current_threat:
  301. covers = [n for n in self.neighbors(*self.cult_leader.pos) if self.can_move_on(self.cult_leader, n)]
  302. for pos in covers:
  303. interest = bool(self.conversion_path and self.conversion_path.steps and pos in [s.pos for s in
  304. self.conversion_path.steps])
  305. priority = k0_protect_cult_leader
  306. priority += k_protect_threat_level * (current_threat - self.threat[pos])
  307. priority += k_cover_interest * interest
  308. priority += k_protect_worth_risk * (self.cult_leader.hp - (current_threat + 1))
  309. action = ActionMove(self.cult_leader, pos, f'take cover (current: {current_threat}, new: {self.threat[pos]})')
  310. actions.put(priority, action)
  311. # Convert
  312. k0_convert = 15
  313. k_convert_number = -10
  314. k_convert_distance = 10
  315. k_convert_danger = 20
  316. if self.cult_leader and self.conversion_path:
  317. path = self.conversion_path.next_candidate()
  318. if path:
  319. targets = [self.index[c] for c in path[-1].candidates]
  320. priority = k0_convert
  321. priority += k_convert_number * len(targets)
  322. priority += k_convert_distance * len(path)
  323. priority += k_convert_danger * sum([self.threat[s.pos] for s in path])
  324. if len(path) == 1:
  325. action = ActionConvert(self.cult_leader, targets[0])
  326. else:
  327. action = ActionMove(self.cult_leader, path[1].pos,
  328. f'go convert {",".join([str(t.id) for t in targets])}')
  329. actions.put(priority, action)
  330. # Shoot opponent units
  331. k0_shoot = 0
  332. k_shoot_opponent_cultist = 6
  333. k_shoot_opponent_leader = 5
  334. k_shoot_kill_before_he_kills = -5
  335. k_shoot_sacrifice = 30
  336. k0_runaway = 10
  337. k_runaway_threat = 10
  338. for a in self.allied_cultists:
  339. if not a.threats:
  340. continue
  341. for threat in a.threats:
  342. priority = k0_shoot
  343. # Il est probable que l'unité se fera tirer dessus par l'ennemi le plus proche: quel résultat?
  344. can_be_killed_by = [t for t in a.threaten_by if t.fatal]
  345. if can_be_killed_by and not threat.fatal:
  346. # run away! (si possible)
  347. worth_sacrificing = any(t.target is self.cult_leader or (t.fatal and t.target is not a) for t in threat.target.threats)
  348. covers = [n for n in self.neighbors(*a.pos) if self.can_move_on(a, n)]
  349. if covers and not worth_sacrificing:
  350. for cover in covers:
  351. if self.threat[cover] < a.hp and not any(self.line_of_sight(u.unit.pos, cover) for u in can_be_killed_by):
  352. action = ActionMove(a, cover, f'run away!')
  353. priority += k0_runaway
  354. priority += k_runaway_threat * self.threat[cover]
  355. actions.put(priority, action)
  356. continue
  357. log(f'<!> {a.id} seems lost')
  358. elif any(t for t in can_be_killed_by if t.unit != threat.target) and threat.fatal:
  359. # l'unité peut tuer la cible, mais sera probablement tuée par une autre unité ennemie
  360. priority += k_shoot_sacrifice
  361. if not threat.fatal:
  362. k = k_shoot_opponent_leader if threat.target is self.opponent_cult_leader else k_shoot_opponent_cultist
  363. priority += k * len(threat.line)
  364. # peut tuer un adversaire avant qu'il ne tue un allié
  365. priority += k_shoot_kill_before_he_kills * any(t.fatal and t.target is not a for t in threat.target.threats)
  366. action = ActionShoot(a, threat.target, f'shoot from {a.pos} (damages: {threat.damages}, fatal: {threat.fatal}, line: {threat.line})')
  367. actions.put(priority, action)
  368. # Position: go to front line
  369. k0_position = 50
  370. k_position_advantage = 40
  371. k_position_distance = 3 # on veut privilégier les plus près, mais pas non plus décourager ceux de l'arrière...
  372. position_hp_min = 5
  373. for a in self.allied_cultists:
  374. if a.hp < position_hp_min:
  375. continue
  376. if self.threat[a.pos]:
  377. # unit already under threat, let shoot action manage this
  378. continue
  379. if any(self.threat[n] for n in self.neighbors(*a.pos)):
  380. # unit already on frontline
  381. continue
  382. # l'unité semble en sécurité, go to front-line
  383. nearest_frontline = self.discover(a, key=lambda x: self.can_move_on(a, x) and self.threat[x] == 1,
  384. limit=1)
  385. if not nearest_frontline:
  386. log(f"<!> {a.id} can not join nearest frontline")
  387. continue
  388. path, target = nearest_frontline[0]
  389. if path:
  390. # log(f"{a.id} - {path} - {target}")
  391. priority = k0_position
  392. priority += k_position_distance * len(path)
  393. priority += k_position_advantage * advantage
  394. action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
  395. actions.put(priority, action)
  396. # Shoot neutral units:
  397. k_shoot_dangerous_neutral_threat = 2
  398. k_shoot_dangerous_neutral_distance = 8
  399. dangerous_neutral_distance_limit = 3
  400. if self.opponent_cult_leader:
  401. discovered_by_opponent = self.discover(self.opponent_cult_leader,
  402. key=lambda x: x in self.index and self.index[x].neutral, limit=3)
  403. neutrals_threaten = {}
  404. for path, target_pos in discovered_by_opponent:
  405. if len(path) > dangerous_neutral_distance_limit:
  406. continue
  407. if target_pos not in neutrals_threaten or neutrals_threaten[target_pos] < len(path):
  408. neutrals_threaten[target_pos] = len(path)
  409. threaten_neutrals_from_leader = {}
  410. if self.cult_leader:
  411. discovered_by_leader = self.discover(self.cult_leader, key=lambda x: x in neutrals_threaten, limit=3)
  412. for path, target_pos in discovered_by_leader:
  413. if target_pos not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[
  414. target_pos] < len(path):
  415. threaten_neutrals_from_leader[target_pos] = len(path)
  416. log(f"Nearest from opp. leader: {neutrals_threaten}")
  417. log(f"Nearest from own leader: {threaten_neutrals_from_leader}")
  418. lost_causes = {}
  419. for target, dist in neutrals_threaten.items():
  420. if target not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target] <= \
  421. neutrals_threaten[target]:
  422. lost_causes[target] = (dist - threaten_neutrals_from_leader.get(target,
  423. 0)) # la distance retenue est la différence entre la distance entre la cible
  424. # et le leader ennemi, et celle entre la cible et le leader allié
  425. for a in self.allied_cultists:
  426. for pos, dist in lost_causes.items():
  427. if pos not in self.index:
  428. log(f"<!> {pos} is not in the index!!")
  429. continue
  430. u = self.index[pos]
  431. line_of_sight = self.line_of_sight(a.pos, u.pos)
  432. shooting_distance = len(line_of_sight)
  433. if not shooting_distance:
  434. continue
  435. if shooting_distance <= u.SHOOTING_RANGE:
  436. # la cible est à portée
  437. action = ActionShoot(a, u, f"peace keeping, shoot from {a.pos} ({line_of_sight})")
  438. priority = k_shoot_dangerous_neutral_distance * shooting_distance
  439. priority += k_shoot_dangerous_neutral_threat * dist
  440. # log(self.line_of_sight(a.pos, u.pos))
  441. actions.put(priority, action)
  442. # Last hope: take risks
  443. if not advantage and not actions:
  444. k0_last_chance = 80
  445. k_last_chance_danger = 5
  446. last_chance_hp_min = 5
  447. for a in self.allied_cultists:
  448. if a.hp < last_chance_hp_min:
  449. continue
  450. for n in self.neighbors(*a.pos):
  451. if not self.threat[n]:
  452. continue
  453. ongoing_fights = []
  454. for u in self.opponent_cultists:
  455. line_of_sight = self.line_of_sight(u.pos, n)
  456. if not line_of_sight:
  457. continue
  458. dist = self.manhattan(u.pos, n)
  459. if dist <= u.SHOOTING_RANGE:
  460. damages = u.SHOOTING_MAX_DAMAGE - dist
  461. ongoing_fights.append((u, damages))
  462. if any(d >= a.hp for _, d in ongoing_fights):
  463. # c'est du suicide: sortir revient à se prendre un coup fatal
  464. continue
  465. action = ActionMove(a, n, f"last hope, moving")
  466. priority = k0_last_chance
  467. priority += k_last_chance_danger * max(d for _, d in ongoing_fights)
  468. actions.put(priority, action)
  469. # TODO: si les deux leaders sont de chaque côté d'un neutre, même si notre leader pourrait convertir le premier, la menace l'emporte et il se barre... repenser le mécanisme de 'take cover'
  470. # TODO: ne pas aller convertir les neutres qui sont sous le feu ennemi, ou au moins baisser leur priorité (surtout s'ils sont la seule chose entre le leader et les ennemis)
  471. # TODO: augmenter la prise de risque pour la conversion, parfois ça peut changer le résultat si le leader a tous ses pvs
  472. # TODO: cf Capture d’écran 2022-09-24 014757.png -> même si pas de ligne entre deux pos, la position peut se retrouver sur une ligne de mire avec une autre cible... snif
  473. # TODO: l'algo actuel va privilégier un tir sur un neutre lointain voisin du leader ennemi, plutôt que sur un ennemi en train de nous tirer dessus
  474. # TODO: toujours le pbm des tirs supposés possibles mais non, ça coince quand le tir est en diagonale entre deux obstables; et autres pbm avec cet algo :(
  475. # TODO: abandonner l'idée d'aller convertir si la menace est mortelle...
  476. return actions
  477. def in_grid(self, pos):
  478. return 0 <= pos[0] < self.width and 0 <= pos[1] < self.height
  479. def can_see_trough(self, pos):
  480. return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
  481. def can_move_on(self, unit, pos):
  482. return self.in_grid(pos) and pos not in self.obstacles and (pos not in self.index or self.index[pos] is unit)
  483. def can_discover(self, pos):
  484. return self.in_grid(pos) and pos not in self.obstacles
  485. def moving_cost(self, unit, pos):
  486. if not self.can_move_on(unit, pos):
  487. return -1
  488. return 1 + self.threat[pos]
  489. @staticmethod
  490. def manhattan(from_, to_):
  491. xa, ya = from_
  492. xb, yb = to_
  493. return abs(xa - xb) + abs(ya - yb)
  494. def neighbors(self, x, y):
  495. return self._neighbors[(x, y)]
  496. def zone(self, pos, radius):
  497. x0, y0 = pos
  498. zone = {}
  499. for x in range(max(x0 - radius, 0), min(x0 + radius, self.width)):
  500. for y in range(max(y0 - radius, 0), min(y0 + radius, self.height)):
  501. dist = self.manhattan(pos, (x, y))
  502. if dist <= radius:
  503. zone[(x, y)] = dist
  504. return zone
  505. @classmethod
  506. def line(cls, start, target):
  507. """
  508. adapted from https://github.com/fragkakis/bresenham/blob/master/src/main/java/org/fragkakis/Bresenham.java
  509. if strict is true, None is return if an obstacle interrupted the line; else a partial line is returned (from start to obstacle)
  510. """
  511. line = []
  512. x0, y0 = start
  513. x1, y1 = target
  514. reversed = y0 > y1
  515. if reversed:
  516. # on fait toujours de bas en haut, du coup on inverse au besoin
  517. x0, y0, x1, y1 = x1, y1, x0, y0
  518. dx = abs(x1 - x0)
  519. dy = abs(y1 - y0)
  520. sx = 1 if x0 < x1 else -1
  521. sy = 1 if y0 < y1 else -1
  522. err = dx - dy
  523. x, y = x0, y0
  524. while 1:
  525. e2 = 2 * err
  526. if e2 > -1 * dy:
  527. err -= dy
  528. x += sx
  529. if e2 < dx:
  530. err += dx
  531. y += sy
  532. if x == x1 and y == y1:
  533. break
  534. line.append((x, y))
  535. if reversed:
  536. line = line[::-1]
  537. line = [start] + line + [target]
  538. return line
  539. def line_of_sight(self, from_, to_):
  540. line = self.line(from_, to_)[1:]
  541. return line if all(self.can_see_trough(c) for c in line[:-1]) else []
  542. def shooting_distance(self, from_, to_):
  543. return len(self.line_of_sight(from_, to_))
  544. def path(self, unit, target):
  545. nodes = Queue()
  546. its, break_on = 0, 400
  547. origin = PathNode(*unit.pos)
  548. nodes.put(0, origin)
  549. while nodes:
  550. current = nodes.get()
  551. if current == target:
  552. path = []
  553. previous = current
  554. while previous:
  555. if previous != unit.pos:
  556. path.insert(0, previous)
  557. previous = previous.parent
  558. return path
  559. neighbors = self.neighbors(*current)
  560. for x, y in neighbors:
  561. its += 1
  562. if its > break_on:
  563. log("<!> pathfinding broken")
  564. return None
  565. if (x, y) == current.parent:
  566. continue
  567. if not self.can_move_on(unit, (x, y)):
  568. continue
  569. moving_cost = self.moving_cost(unit, (x, y))
  570. cost = current.cost + moving_cost
  571. priority = cost + 10 * Grid.manhattan((x, y), target)
  572. node = PathNode(x, y, current)
  573. node.cost = cost
  574. nodes.put(priority, node)
  575. return None
  576. def discover(self, unit, key, limit=5):
  577. paths = []
  578. nodes = []
  579. its, break_on = 0, 2000
  580. origin = DiscoveryNode(*unit.pos)
  581. nodes.append(origin)
  582. while nodes:
  583. current = nodes.pop(0) # l'ordre est important, pour que les premiers indexés soient les premiers analysés
  584. neighbors = self.neighbors(*current)
  585. for pos in neighbors:
  586. its += 1
  587. if its > break_on:
  588. log(f"<!> discovery broke, {len(paths)} results")
  589. return paths
  590. if current != unit.pos and key(pos):
  591. # TODO: actuellement, l'algo pourra retourner x chemins différents vers la même cible
  592. # il faudrait retourner les trois meilleurs chemins vers x cibles (selon 'limit')
  593. path = current.ancestors[1:] + [current]
  594. paths.append((path, pos))
  595. if len(paths) >= limit:
  596. return paths
  597. continue
  598. if pos in current.ancestors:
  599. continue
  600. if not self.can_move_on(unit, pos):
  601. continue
  602. node = DiscoveryNode(*pos, 0, current.ancestors + [current])
  603. nodes.append(node)
  604. return paths
  605. def get_conversion_path(self, unit, key, limit=5):
  606. """ essaies de trouver le meilleur chemin pour relier des cases dont au moins une voisine valide
  607. la condition 'key' (dans la limite de 'limit')"""
  608. nodes = Queue()
  609. winners = Queue()
  610. its, break_on = 0, 1000
  611. origin = DiscoveryNode(*unit.pos)
  612. abandon_at = 120 # number of paths explored
  613. nodes.put(0, origin)
  614. for n in self.neighbors(*unit.pos):
  615. if key(n):
  616. origin.matches.append(n)
  617. while nodes:
  618. its += 1
  619. if its > break_on:
  620. log(f"<!> get_conversion_path broke")
  621. break
  622. if len(nodes.items) > abandon_at:
  623. log("> get_conversion_path early exit")
  624. break
  625. current = nodes.get()
  626. for pos in self.neighbors(*current):
  627. if not self.can_move_on(unit, pos):
  628. continue
  629. moving_cost = 1 + self.threat[pos]
  630. matches = []
  631. for n in self.neighbors(*pos):
  632. if n not in current.matches and key(n):
  633. matches.append(n)
  634. cost = current.cost + moving_cost
  635. priority = 1000 * cost - (2000 * (len(current.matches) + len(matches)))
  636. priority += 100 * len(
  637. [a for a in current.ancestors if a == pos]) # décourage de revenir à une case visitée
  638. node = DiscoveryNode(
  639. pos[0],
  640. pos[1],
  641. cost,
  642. current.ancestors + [current],
  643. current.matches + matches
  644. )
  645. if matches:
  646. winners.put(40 * node.cost - 100 * len(node.matches), node)
  647. nodes.put(priority, node)
  648. if len(current.matches) >= limit or its > break_on:
  649. break
  650. try:
  651. best_node = winners.get()
  652. except IndexError:
  653. if nodes:
  654. best_node = nodes.get()
  655. else:
  656. best_node = origin
  657. return ConversionPath.make_from_discovery_node(best_node)
  658. def _repr_cell(self, pos):
  659. return f"{self.threat[pos]}"
  660. # return f"{self.control[pos]}/{self.threat[pos]}"
  661. # return self.heat_map[pos]
  662. if pos in self.obstacles:
  663. return "X"
  664. unit = self.index.get(pos, None)
  665. if type(unit) is CultLeader:
  666. return "C"
  667. elif unit is None:
  668. return "."
  669. else:
  670. return "U"
  671. def graph(self):
  672. return "\n".join(
  673. ["|".join([str(self._repr_cell((x, y))) for x in range(self.width)]) for y in range(self.height)])
  674. # Create grid
  675. GRID = Grid(*[int(i) for i in input().split()])
  676. obstacles_input = [input() for y in range(GRID.height)]
  677. GRID.obstacles = [(x, y) for y, row in enumerate(obstacles_input) for x, val in enumerate(row) if val == 'x']
  678. GRID.pre_compute()
  679. while 1:
  680. GRID.reinit_round()
  681. for _ in range(int(input())):
  682. GRID.update_unit(*[int(j) for j in input().split()])
  683. log(f"start round {GRID.round}")
  684. GRID.update()
  685. actions = GRID.build_actions()
  686. # print("\n" + GRID.graph(), file=sys.stderr)
  687. for action in actions.items:
  688. log(f"* {action}")
  689. try:
  690. action = actions.get()
  691. except IndexError:
  692. log("no action...")
  693. action = ActionWait()
  694. log(f"exec : {action}")
  695. action.exec()
  696. GRID.round += 1