main.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  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 Unit(BaseClass):
  94. TYPE_CULTIST = 0
  95. TYPE_CULT_LEADER = 1
  96. OWNER_0 = 0
  97. OWNER_1 = 1
  98. NO_OWNER = 2
  99. SHOOTING_RANGE = 6
  100. SHOOTING_MAX_DAMAGE = 7
  101. def __init__(self, id_):
  102. self.id = id_
  103. self.hp = 10
  104. self.x = None
  105. self.y = None
  106. self.owner = None
  107. @property
  108. def pos(self):
  109. return self.x, self.y
  110. @property
  111. def owned(self):
  112. return self.owner == ME.id
  113. @property
  114. def opponent(self):
  115. return self.owner == OPPONENT.id
  116. @property
  117. def neutral(self):
  118. return self.owner == self.NO_OWNER
  119. class CultLeader(Unit):
  120. pass
  121. class Action(BaseClass):
  122. pass
  123. class ActionWait(Action):
  124. def __repr__(self):
  125. return f"<ActionWait>"
  126. def exec(self):
  127. print("WAIT")
  128. class ActionMove(Action):
  129. def __init__(self, unit, pos, message=''):
  130. self.unit = unit
  131. self.pos = pos
  132. self.message = message
  133. def __repr__(self):
  134. return f"<ActionMove: {self.unit.id} to {self.pos} ({self.message})>"
  135. def exec(self):
  136. print(f"{self.unit.id} MOVE {self.pos[0]} {self.pos[1]}")
  137. class ActionShoot(Action):
  138. def __init__(self, unit, target, message=''):
  139. self.unit = unit
  140. self.target = target
  141. self.message = message
  142. def __repr__(self):
  143. return f"<ActionShoot: {self.unit.id} to {self.target.id} ({self.message})>"
  144. def exec(self):
  145. print(f"{self.unit.id} SHOOT {self.target.id}")
  146. class ActionConvert(Action):
  147. def __init__(self, unit, target):
  148. self.unit = unit
  149. self.target = target
  150. def __repr__(self):
  151. return f"<ActionConvert: {self.unit.id} to {self.target.id}>"
  152. def exec(self):
  153. print(f"{self.unit.id} CONVERT {self.target.id}")
  154. class Grid(BaseClass):
  155. def __init__(self, width, height):
  156. self.width = width
  157. self.height = height
  158. self.round = 1
  159. self.cells = []
  160. self.obstacles = []
  161. self._neighbors = {}
  162. self.index = {}
  163. self.units = {}
  164. self.threat = {}
  165. self.conversion_path = ConversionPath()
  166. self.cult_leader = None
  167. self.opponent_cult_leader = None
  168. self.allied_cultists = []
  169. self.owned_units = []
  170. self.opponent_units = []
  171. self.opponent_cultists = []
  172. self.neutrals = []
  173. def pre_compute(self):
  174. self.cells = [(x, y) for x in range(self.width) for y in range(self.height)]
  175. for x, y in self.cells:
  176. self._neighbors[(x, y)] = [(xn, yn) for xn, yn in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if
  177. 0 <= xn < self.width and 0 <= yn < self.height]
  178. def reinit_round(self):
  179. self.units = {}
  180. def update_unit(self, id_, type_, hp, x, y, owner):
  181. self.units[id_] = Unit(id_) if type_ != Unit.TYPE_CULT_LEADER else CultLeader(id_)
  182. unit = self.units[id_]
  183. unit.hp = hp
  184. unit.x = x
  185. unit.y = y
  186. unit.owner = owner
  187. def update_index(self):
  188. self.index = {}
  189. self.cult_leader = None
  190. self.opponent_cult_leader = None
  191. self.allied_cultists = []
  192. self.owned_units = []
  193. self.opponent_units = []
  194. self.opponent_cultists = []
  195. self.neutrals = []
  196. for unit in self.units.values():
  197. self.index[(unit.x, unit.y)] = unit
  198. if unit.owner == ME.id:
  199. self.owned_units.append(unit)
  200. if type(unit) is CultLeader:
  201. self.cult_leader = unit
  202. else:
  203. self.allied_cultists.append(unit)
  204. elif unit.owner == OPPONENT.id:
  205. self.opponent_units.append(unit)
  206. if type(unit) is CultLeader:
  207. self.opponent_cult_leader = unit
  208. else:
  209. self.opponent_cultists.append(unit)
  210. else:
  211. self.neutrals.append(unit)
  212. def update_threat_map(self):
  213. self.threat = {(x, y): 0 for x in range(self.width) for y in range(self.height)}
  214. sources = self.opponent_cultists
  215. # On ajoute les neutres voisins du leader ennemi aux sources de menace possible
  216. if self.opponent_cult_leader:
  217. for pos in self.neighbors(*self.opponent_cult_leader.pos):
  218. if pos in self.index and self.index[pos].neutral:
  219. sources.append(self.index[pos])
  220. for u in sources:
  221. shooting_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
  222. for x, y in shooting_zone:
  223. dist = shooting_zone[(x, y)]
  224. if not self.line_of_sight(u.pos, (x, y)):
  225. continue
  226. threat = Unit.SHOOTING_RANGE + 1 - dist
  227. if u.neutral:
  228. threat //= 2
  229. if threat > self.threat[(x, y)]:
  230. self.threat[(x, y)] = threat
  231. def compute_conversion_path(self):
  232. conversion_path = ConversionPath()
  233. if self.cult_leader and self.neutrals:
  234. conversion_path = self.get_conversion_path(
  235. self.cult_leader,
  236. key=(lambda pos: pos in self.index and self.index[pos].neutral),
  237. limit=min(4, len(self.neutrals))
  238. )
  239. log(f"conversion : {conversion_path}")
  240. self.conversion_path = conversion_path
  241. def update(self):
  242. log('update indexes')
  243. self.update_index()
  244. self.update_threat_map()
  245. self.compute_conversion_path()
  246. # log(self.obstacles + [u.pos for u in self.allied_cultists])
  247. # log([n.pos for n in self.neutrals])
  248. def build_actions(self):
  249. actions = Queue()
  250. # Leader take cover
  251. k0_protect_cult_leader = 30
  252. k_protect_threat_level = -5
  253. k_cover_threat = 10
  254. k_cover_interest = -10
  255. if self.cult_leader:
  256. current_threat = self.threat[self.cult_leader.pos]
  257. if current_threat:
  258. covers = [n for n in self.neighbors(*self.cult_leader.pos) if self.can_move_on(self.cult_leader, n)]
  259. for pos in covers:
  260. action = ActionMove(self.cult_leader, pos, f'take cover (t: {current_threat})')
  261. interest = bool(self.conversion_path and self.conversion_path.steps and pos in [s.pos for s in
  262. self.conversion_path.steps])
  263. priority = k0_protect_cult_leader
  264. priority += k_protect_threat_level * current_threat
  265. priority += k_cover_threat * self.threat[pos]
  266. priority += k_cover_interest * interest
  267. actions.put(priority, action)
  268. # Convert
  269. k_convert_number = -10
  270. k_convert_distance = 10
  271. k_convert_danger = 20
  272. if self.cult_leader and self.conversion_path:
  273. path = self.conversion_path.next_candidate()
  274. if path:
  275. targets = [self.index[c] for c in path[-1].candidates]
  276. priority = 0
  277. priority += k_convert_number * len(targets)
  278. priority += k_convert_distance * len(path)
  279. priority += k_convert_danger * sum([self.threat[s.pos] for s in path])
  280. if len(path) == 1:
  281. action = ActionConvert(self.cult_leader, targets[0])
  282. else:
  283. action = ActionMove(self.cult_leader, path[1].pos,
  284. f'go convert {",".join([str(t.id) for t in targets])}')
  285. actions.put(priority, action)
  286. # Shoot opponent units
  287. k_shoot_opponent_cultist = 8
  288. k_shoot_opponent_leader = 4
  289. k_shoot_movement_needed = 15
  290. k0_shoot_in_danger = -10
  291. k_shoot_sacrifice = 50
  292. k0_runaway = 10
  293. k_runaway_threat = 10
  294. for a in self.allied_cultists:
  295. targets = {}
  296. for u in self.opponent_units:
  297. line_of_sight = self.line_of_sight(a.pos, u.pos)
  298. if not len(line_of_sight):
  299. continue
  300. if len(line_of_sight) <= u.SHOOTING_RANGE:
  301. targets[u] = line_of_sight
  302. else:
  303. # la cible est hors de portée, la poursuivre si plus faible et sans soutien?
  304. # TODO: implémenter
  305. pass
  306. if not targets:
  307. continue
  308. # Il est probable que l'unité se fera tirer dessus par l'ennemi le plus proche: quel résultat?
  309. dist_to_nearest_target = min([len(line) for line in targets.values()])
  310. damages_received = Unit.SHOOTING_MAX_DAMAGE - dist_to_nearest_target
  311. unit_in_danger = damages_received >= a.hp or (len(targets) > 1 and (damages_received + 3) >= a.hp)
  312. for u, line in targets.items():
  313. dist = len(line)
  314. damages = Unit.SHOOTING_MAX_DAMAGE - dist
  315. target_die = damages >= u.hp
  316. priority = 0
  317. if unit_in_danger and not target_die:
  318. # run away!
  319. covers = [n for n in self.neighbors(*a.pos)
  320. if self.threat[n] < self.threat[a.pos] and self.can_move_on(a, n)]
  321. for pos in covers:
  322. action = ActionMove(a, pos, f'run away!')
  323. priority += k0_runaway
  324. priority += k_runaway_threat * self.threat[pos]
  325. actions.put(priority, action)
  326. continue
  327. elif unit_in_danger and target_die and len(targets) > 1:
  328. # bof, un échange, pas sûr que ça puisse être intéressant...
  329. priority += k_shoot_sacrifice
  330. k = k_shoot_opponent_leader if u is self.opponent_cult_leader else k_shoot_opponent_cultist
  331. priority += k * dist
  332. priority += k0_shoot_in_danger * unit_in_danger
  333. action = ActionShoot(a, u, f'shoot from {a.pos} ({line})')
  334. actions.put(priority, action)
  335. # Position
  336. k0_position = 30
  337. k_advantage = 40
  338. k_position_distance = 3 # on veut privilégier les plus près, mais pas non plus décourager ceux de l'arrière...
  339. hp_min = 5
  340. advantage = len(self.owned_units) > len(self.opponent_units) + len(self.neutrals)
  341. for a in self.allied_cultists:
  342. if a.hp < hp_min:
  343. continue
  344. if self.threat[a.pos]:
  345. # l'unité est déjà dans une zone à risque
  346. # TODO: envisager un retrait
  347. continue
  348. else:
  349. # l'unité semble en sécurité, go to front-line
  350. nearest_frontline = self.discover(a, key=lambda x: self.can_move_on(a, x) and self.threat[x] == 1, limit=1)
  351. if not nearest_frontline:
  352. log(f"<!> {a.id} can not join nearest frontline")
  353. continue
  354. path, target = nearest_frontline[0]
  355. if path:
  356. # log(f"{a.id} - {path} - {target}")
  357. # already in place
  358. priority = k0_position
  359. priority += k_position_distance * len(path)
  360. priority += k_advantage * advantage
  361. action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
  362. actions.put(priority, action)
  363. # Shoot neutral units:
  364. k_shoot_dangerous_neutral_threat = 2
  365. k_shoot_dangerous_neutral_distance = 8
  366. dangerous_neutral_distance_limit = 3
  367. if self.opponent_cult_leader:
  368. discovered_by_opponent = self.discover(self.opponent_cult_leader, key=lambda x: x in self.index and self.index[x].neutral, limit=3)
  369. neutrals_threaten = {}
  370. for path, target_pos in discovered_by_opponent:
  371. if len(path) > dangerous_neutral_distance_limit:
  372. continue
  373. if target_pos not in neutrals_threaten or neutrals_threaten[target_pos] < len(path):
  374. neutrals_threaten[target_pos] = len(path)
  375. threaten_neutrals_from_leader = {}
  376. if self.cult_leader:
  377. discovered_by_leader = self.discover(self.cult_leader, key=lambda x: x in neutrals_threaten, limit=3)
  378. for path, target_pos in discovered_by_leader:
  379. if target_pos not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target_pos] < len(path):
  380. threaten_neutrals_from_leader[target_pos] = len(path)
  381. log(f"Nearest from opp. leader: {neutrals_threaten}")
  382. log(f"Nearest from own leader: {threaten_neutrals_from_leader}")
  383. lost_causes = {}
  384. for target, dist in neutrals_threaten.items():
  385. if target not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target] <= neutrals_threaten[target]:
  386. lost_causes[target] = (dist - threaten_neutrals_from_leader.get(target, 0)) # la distance retenue est la différence entre la distance entre la cible
  387. # et le leader ennemi, et celle entre la cible et le leader allié
  388. for a in self.allied_cultists:
  389. for pos, dist in lost_causes.items():
  390. if pos not in self.index:
  391. log(f"<!> {pos} is not in the index!!")
  392. continue
  393. u = self.index[pos]
  394. line_of_sight = self.line_of_sight(a.pos, u.pos)
  395. shooting_distance = len(line_of_sight)
  396. if not shooting_distance:
  397. continue
  398. if shooting_distance <= u.SHOOTING_RANGE:
  399. # la cible est à portée
  400. action = ActionShoot(a, u, f"peace keeping, shoot from {a.pos} ({line_of_sight})")
  401. priority = k_shoot_dangerous_neutral_distance * shooting_distance
  402. priority += k_shoot_dangerous_neutral_threat * dist
  403. # log(self.line_of_sight(a.pos, u.pos))
  404. actions.put(priority, action)
  405. # TODO: action 'take cover' pour les unités aussi
  406. # TODO: action 'peace-keeping': tirer sur les neutres qu'on ne pourra pas convertir avant l'ennemi
  407. # TODO: action 'intercept': une unité se place entre un tireur ennemi et le leader; en dernier recours
  408. # TODO: action 'do nothing' : parfois, c'est la meilleure chose à faire
  409. return actions
  410. def in_grid(self, pos):
  411. return 0 <= pos[0] < self.width and 0 <= pos[1] < self.height
  412. def can_see_trough(self, pos):
  413. return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
  414. def can_move_on(self, unit, pos):
  415. return self.in_grid(pos) and pos not in self.obstacles and (pos not in self.index or self.index[pos] is unit)
  416. def can_discover(self, pos):
  417. return self.in_grid(pos) and pos not in self.obstacles
  418. def moving_cost(self, unit, pos):
  419. if not self.can_move_on(unit, pos):
  420. return -1
  421. return 1 + self.threat[pos]
  422. @staticmethod
  423. def manhattan(from_, to_):
  424. xa, ya = from_
  425. xb, yb = to_
  426. return abs(xa - xb) + abs(ya - yb)
  427. def neighbors(self, x, y):
  428. return self._neighbors[(x, y)]
  429. def zone(self, pos, radius):
  430. x0, y0 = pos
  431. zone = {}
  432. for x in range(max(x0 - radius, 0), min(x0 + radius, self.width)):
  433. for y in range(max(y0 - radius, 0), min(y0 + radius, self.height)):
  434. dist = self.manhattan(pos, (x, y))
  435. if dist <= radius:
  436. zone[(x, y)] = dist
  437. return zone
  438. @classmethod
  439. def line(cls, start, target):
  440. """
  441. adapted from https://github.com/fragkakis/bresenham/blob/master/src/main/java/org/fragkakis/Bresenham.java
  442. if strict is true, None is return if an obstacle interrupted the line; else a partial line is returned (from start to obstacle)
  443. """
  444. line = []
  445. x0, y0 = start
  446. x1, y1 = target
  447. reversed = y0 > y1
  448. if reversed:
  449. # on fait toujours de bas en haut, du coup on inverse au besoin
  450. x0, y0, x1, y1 = x1, y1, x0, y0
  451. dx = abs(x1 - x0)
  452. dy = abs(y1 - y0)
  453. sx = 1 if x0 < x1 else -1
  454. sy = 1 if y0 < y1 else -1
  455. err = dx - dy
  456. x, y = x0, y0
  457. while 1:
  458. e2 = 2 * err
  459. if e2 > -1 * dy:
  460. err -= dy
  461. x += sx
  462. if e2 < dx:
  463. err += dx
  464. y += sy
  465. if x == x1 and y == y1:
  466. break
  467. line.append((x, y))
  468. if reversed:
  469. line = line[::-1]
  470. line = [start] + line + [target]
  471. return line
  472. def line_of_sight(self, from_, to_):
  473. line = self.line(from_, to_)[1:]
  474. return line if all(self.can_see_trough(c) for c in line[:-1]) else []
  475. def shooting_distance(self, from_, to_):
  476. return len(self.line_of_sight(from_, to_))
  477. def path(self, unit, target):
  478. nodes = Queue()
  479. its, break_on = 0, 400
  480. origin = PathNode(*unit.pos)
  481. nodes.put(0, origin)
  482. while nodes:
  483. current = nodes.get()
  484. if current == target:
  485. path = []
  486. previous = current
  487. while previous:
  488. if previous != unit.pos:
  489. path.insert(0, previous)
  490. previous = previous.parent
  491. return path
  492. neighbors = self.neighbors(*current)
  493. for x, y in neighbors:
  494. its += 1
  495. if its > break_on:
  496. log("<!> pathfinding broken")
  497. return None
  498. if (x, y) == current.parent:
  499. continue
  500. if not self.can_move_on(unit, (x, y)):
  501. continue
  502. moving_cost = self.moving_cost(unit, (x, y))
  503. cost = current.cost + moving_cost
  504. priority = cost + 10 * Grid.manhattan((x, y), target)
  505. node = PathNode(x, y, current)
  506. node.cost = cost
  507. nodes.put(priority, node)
  508. return None
  509. def discover(self, unit, key, limit=5):
  510. paths = []
  511. nodes = []
  512. its, break_on = 0, 2000
  513. origin = DiscoveryNode(*unit.pos)
  514. nodes.append(origin)
  515. while nodes:
  516. current = nodes.pop(0) # l'ordre est important, pour que les premiers indexés soient les premiers analysés
  517. neighbors = self.neighbors(*current)
  518. for pos in neighbors:
  519. its += 1
  520. if its > break_on:
  521. log(f"<!> discovery broke, {len(paths)} results")
  522. return paths
  523. if current != unit.pos and key(pos):
  524. # TODO: actuellement, l'algo pourra retourner x chemins différents vers la même cible
  525. # il faudrait retourner les trois meilleurs chemins vers x cibles (selon 'limit')
  526. path = current.ancestors[1:] + [current]
  527. paths.append((path, pos))
  528. if len(paths) >= limit:
  529. return paths
  530. continue
  531. if pos in current.ancestors:
  532. continue
  533. if not self.can_move_on(unit, pos):
  534. continue
  535. node = DiscoveryNode(*pos, 0, current.ancestors + [current])
  536. nodes.append(node)
  537. return paths
  538. def get_conversion_path(self, unit, key, limit=5):
  539. """ essaies de trouver le meilleur chemin pour relier des cases dont au moins une voisine valide
  540. la condition 'key' (dans la limite de 'limit')"""
  541. nodes = Queue()
  542. winners = Queue()
  543. its, break_on = 0, 1000
  544. origin = DiscoveryNode(*unit.pos)
  545. abandon_at = 120 # number of paths explored
  546. nodes.put(0, origin)
  547. for n in self.neighbors(*unit.pos):
  548. if key(n):
  549. origin.matches.append(n)
  550. while nodes:
  551. its += 1
  552. if its > break_on:
  553. log(f"<!> get_conversion_path broke")
  554. break
  555. if len(nodes.items) > abandon_at:
  556. log("> get_conversion_path early exit")
  557. break
  558. current = nodes.get()
  559. for pos in self.neighbors(*current):
  560. if not self.can_move_on(unit, pos):
  561. continue
  562. moving_cost = 1
  563. matches = []
  564. for n in self.neighbors(*pos):
  565. if n not in current.matches and key(n):
  566. matches.append(n)
  567. cost = current.cost + moving_cost
  568. priority = 1000 * cost - (2000 * (len(current.matches) + len(matches)))
  569. priority += 100 * len(
  570. [a for a in current.ancestors if a == pos]) # décourage de revenir à une case visitée
  571. node = DiscoveryNode(
  572. pos[0],
  573. pos[1],
  574. cost,
  575. current.ancestors + [current],
  576. current.matches + matches
  577. )
  578. if matches:
  579. winners.put(40 * node.cost - 100 * len(node.matches), node)
  580. nodes.put(priority, node)
  581. if len(current.matches) >= limit or its > break_on:
  582. break
  583. try:
  584. best_node = winners.get()
  585. except IndexError:
  586. if nodes:
  587. best_node = nodes.get()
  588. else:
  589. best_node = origin
  590. return ConversionPath.make_from_discovery_node(best_node)
  591. def _repr_cell(self, pos):
  592. return f"{self.threat[pos]}"
  593. # return f"{self.control[pos]}/{self.threat[pos]}"
  594. # return self.heat_map[pos]
  595. if pos in self.obstacles:
  596. return "X"
  597. unit = self.index.get(pos, None)
  598. if type(unit) is CultLeader:
  599. return "C"
  600. elif unit is None:
  601. return "."
  602. else:
  603. return "U"
  604. def graph(self):
  605. return "\n".join(
  606. ["|".join([str(self._repr_cell((x, y))) for x in range(self.width)]) for y in range(self.height)])
  607. # Create grid
  608. GRID = Grid(*[int(i) for i in input().split()])
  609. obstacles_input = [input() for y in range(GRID.height)]
  610. GRID.obstacles = [(x, y) for y, row in enumerate(obstacles_input) for x, val in enumerate(row) if val == 'x']
  611. GRID.pre_compute()
  612. while 1:
  613. GRID.reinit_round()
  614. for _ in range(int(input())):
  615. GRID.update_unit(*[int(j) for j in input().split()])
  616. log(f"start round {GRID.round}")
  617. GRID.update()
  618. actions = GRID.build_actions()
  619. # print("\n" + GRID.graph(), file=sys.stderr)
  620. for action in actions.items:
  621. log(f"* {action}")
  622. try:
  623. action = actions.get()
  624. except IndexError:
  625. log("no action...")
  626. action = ActionWait()
  627. log(f"exec : {action}")
  628. action.exec()
  629. GRID.round += 1