main.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  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, pos, line):
  95. self.unit = unit
  96. self.damages = damages
  97. self.pos = pos
  98. self.line = line if line else []
  99. self.target = None
  100. self.fatal = False
  101. self.neutral = False
  102. def __repr__(self):
  103. return f"<Threat (unit: {self.unit.id}, damages={self.damages}, pos={self.pos}, target={self.target.id if self.target else None}, fatal={self.fatal}, line={self.line})>"
  104. def fatal_for(self, unit):
  105. return self.damages >= unit.hp
  106. class Unit(BaseClass):
  107. TYPE_CULTIST = 0
  108. TYPE_CULT_LEADER = 1
  109. OWNER_0 = 0
  110. OWNER_1 = 1
  111. NO_OWNER = 2
  112. MAX_HP = 10
  113. SHOOTING_RANGE = 6
  114. SHOOTING_MAX_DAMAGE = 7
  115. def __init__(self, id_):
  116. self.id = id_
  117. self.hp = Unit.MAX_HP
  118. self.x = None
  119. self.y = None
  120. self.owner = None
  121. self.targets = []
  122. self.threaten_by = []
  123. self.has_been_shooted = False
  124. @property
  125. def pos(self):
  126. return self.x, self.y
  127. @property
  128. def owned(self):
  129. return self.owner == ME.id
  130. @property
  131. def opponent(self):
  132. return self.owner == OPPONENT.id
  133. @property
  134. def neutral(self):
  135. return self.owner == self.NO_OWNER
  136. class CultLeader(Unit):
  137. pass
  138. class Action(BaseClass):
  139. pass
  140. class ActionWait(Action):
  141. def __repr__(self):
  142. return f"<ActionWait>"
  143. def exec(self):
  144. print("WAIT")
  145. class ActionMove(Action):
  146. def __init__(self, unit, pos, message=''):
  147. self.unit = unit
  148. self.pos = pos
  149. self.message = message
  150. def __repr__(self):
  151. return f"<ActionMove: {self.unit.id} to {self.pos} ({self.message})>"
  152. def exec(self):
  153. print(f"{self.unit.id} MOVE {self.pos[0]} {self.pos[1]}")
  154. class ActionShoot(Action):
  155. def __init__(self, unit, target, message=''):
  156. self.unit = unit
  157. self.target = target
  158. self.message = message
  159. def __repr__(self):
  160. return f"<ActionShoot: {self.unit.id} to {self.target.id} ({self.message})>"
  161. def exec(self):
  162. print(f"{self.unit.id} SHOOT {self.target.id}")
  163. class ActionConvert(Action):
  164. def __init__(self, unit, target):
  165. self.unit = unit
  166. self.target = target
  167. def __repr__(self):
  168. return f"<ActionConvert: {self.unit.id} to {self.target.id}>"
  169. def exec(self):
  170. print(f"{self.unit.id} CONVERT {self.target.id}")
  171. class Grid(BaseClass):
  172. def __init__(self, width, height):
  173. self.width = width
  174. self.height = height
  175. self.round = 1
  176. self.cells = []
  177. self.obstacles = []
  178. self._neighbors = {}
  179. self.index = {}
  180. self.units = {}
  181. self.threats_on = {}
  182. self.max_threat = {}
  183. self.conversion_path = ConversionPath()
  184. self.ideal_conversion_path = ConversionPath()
  185. self.cult_leader = None
  186. self.opponent_cult_leader = None
  187. self.allied_cultists = []
  188. self.owned_units = []
  189. self.opponent_units = []
  190. self.opponent_cultists = []
  191. self.neutrals = []
  192. self.previous_hps = {}
  193. def pre_compute(self):
  194. self.cells = [(x, y) for x in range(self.width) for y in range(self.height)]
  195. for x, y in self.cells:
  196. self._neighbors[(x, y)] = [(xn, yn) for xn, yn in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if
  197. 0 <= xn < self.width and 0 <= yn < self.height]
  198. def reinit_round(self):
  199. self.previous_hps = {u.id: u.hp for u in self.owned_units}
  200. self.units = {}
  201. self.threats_on = {pos: [] for pos in self.cells}
  202. self.max_threat = {pos: 0 for pos in self.cells}
  203. self.conversion_path = ConversionPath()
  204. self.ideal_conversion_path = ConversionPath()
  205. def update_unit(self, id_, type_, hp, x, y, owner):
  206. # quand le bresenham foire, histoire de "corriger le tir", on retient le coup pour en tenir compte
  207. has_been_shooted = False
  208. if id_ in self.previous_hps:
  209. if hp < self.previous_hps[id_]:
  210. has_been_shooted = True
  211. self.units[id_] = Unit(id_) if type_ != Unit.TYPE_CULT_LEADER else CultLeader(id_)
  212. unit = self.units[id_]
  213. unit.hp = hp
  214. unit.x = x
  215. unit.y = y
  216. unit.owner = owner
  217. unit.targets = []
  218. unit.threaten_by = []
  219. unit.has_been_shooted = has_been_shooted
  220. def update_index(self):
  221. self.index = {}
  222. self.cult_leader = None
  223. self.opponent_cult_leader = None
  224. self.allied_cultists = []
  225. self.owned_units = []
  226. self.opponent_units = []
  227. self.opponent_cultists = []
  228. self.neutrals = []
  229. for unit in self.units.values():
  230. self.index[(unit.x, unit.y)] = unit
  231. if unit.owner == ME.id:
  232. self.owned_units.append(unit)
  233. if type(unit) is CultLeader:
  234. self.cult_leader = unit
  235. else:
  236. self.allied_cultists.append(unit)
  237. elif unit.owner == OPPONENT.id:
  238. self.opponent_units.append(unit)
  239. if type(unit) is CultLeader:
  240. self.opponent_cult_leader = unit
  241. else:
  242. self.opponent_cultists.append(unit)
  243. else:
  244. self.neutrals.append(unit)
  245. def update_threat(self):
  246. sources = self.allied_cultists + self.opponent_cultists
  247. if self.opponent_cult_leader:
  248. for n in self.neighbors(*self.opponent_cult_leader.pos):
  249. if n in self.index and self.index[n].neutral:
  250. sources.append(self.index[n])
  251. for u in sources:
  252. threat_zone = self.zone(u.pos, Unit.SHOOTING_RANGE)
  253. for pos in threat_zone:
  254. if pos in self.obstacles:
  255. continue
  256. line_of_sight = self.line_of_sight(u.pos, pos)
  257. if not line_of_sight:
  258. continue
  259. dist = self.manhattan(u.pos, pos)
  260. damages = u.SHOOTING_MAX_DAMAGE - dist
  261. threat = Threat(u, damages, pos, line_of_sight)
  262. self.threats_on[pos].append(threat)
  263. if not u.owned and self.max_threat[pos] < damages:
  264. self.max_threat[pos] = damages
  265. if pos in self.index:
  266. target = self.index[pos]
  267. if target.owner == u.owner:
  268. continue
  269. threat.target = target
  270. threat.fatal = damages >= target.hp
  271. threat.neutral = u.neutral
  272. u.targets.append(threat)
  273. target.threaten_by.append(threat)
  274. # fallback pour compenser le bresenham inégal
  275. for a in self.owned_units:
  276. for t in a.threaten_by:
  277. if not t in a.targets:
  278. threat = Threat(a, t.damages, a.pos, t.line[::-1])
  279. threat.target = t.unit
  280. threat.neutral = False
  281. a.targets.append(threat)
  282. def compute_conversion_path(self):
  283. conversion_path = ConversionPath()
  284. ideal_conversion_path = ConversionPath()
  285. if self.cult_leader and self.neutrals:
  286. conversion_path = self.get_conversion_path(
  287. self.cult_leader,
  288. key=(lambda pos: pos in self.index and self.index[pos].neutral),
  289. moving_key=lambda pos: self.can_move_on(self.cult_leader, pos),
  290. limit=min(4, len(self.neutrals))
  291. )
  292. log(f"conversion : {conversion_path}")
  293. ideal_conversion_path = self.get_conversion_path(
  294. self.cult_leader,
  295. key=(lambda pos: pos in self.index and self.index[pos].neutral),
  296. moving_key=lambda pos: self.can_move_on(self.cult_leader, pos) or (pos in self.index and self.index[pos].owned),
  297. limit=min(4, len(self.neutrals))
  298. )
  299. log(f"ideal conversion : {ideal_conversion_path}")
  300. self.conversion_path = conversion_path
  301. self.ideal_conversion_path = ideal_conversion_path
  302. def update(self):
  303. self.update_index()
  304. self.update_threat()
  305. self.compute_conversion_path()
  306. # log(self.obstacles + [u.pos for u in self.allied_cultists])
  307. # log([n.pos for n in self.neutrals])
  308. def build_actions(self):
  309. actions = Queue()
  310. advantage = len(self.owned_units) > len(self.opponent_units)
  311. sure_advantage = len(self.owned_units) > (len(self.opponent_units) + len(self.neutrals))
  312. for u in self.owned_units:
  313. if u.threaten_by:
  314. log(f"Threats : {u.targets}")
  315. log(f"Threats : {u.threaten_by}")
  316. elif u.has_been_shooted:
  317. log(f"<!!!> {u.id} has been shooted by unknown threat!")
  318. # Leader take cover
  319. k0_protect_cult_leader = 25
  320. k_protect_threat_level = -5
  321. k_protect_worth_risk = 3
  322. k_cover_interest = -10
  323. if self.cult_leader:
  324. threat_level = self.max_threat[self.cult_leader.pos]
  325. # pas de menace apparente, mais le leader a reçu un tir depuis le tour précédent (pbm de bresenham)
  326. if not threat_level and self.cult_leader.has_been_shooted:
  327. log("<!!> leader seems not threaten but has been hurted!")
  328. threat_level = 10
  329. # une unité voisine peut être convertie, on laisse ça à l'action convert
  330. threats = [t
  331. for t in self.cult_leader.threaten_by
  332. if t.unit not in self.neighbors(*self.cult_leader.pos)]
  333. if threats or self.cult_leader.has_been_shooted:
  334. covers = [n for n in self.neighbors(*self.cult_leader.pos) if self.can_move_on(self.cult_leader, n)]
  335. for pos in covers:
  336. interest = bool(self.conversion_path and self.conversion_path.steps and pos in [s.pos for s in
  337. self.conversion_path.steps])
  338. priority = k0_protect_cult_leader
  339. priority += k_protect_threat_level * (threat_level - self.max_threat[pos])
  340. priority += k_cover_interest * interest
  341. priority += k_protect_worth_risk * (self.cult_leader.hp - (threat_level + 1))
  342. action = ActionMove(self.cult_leader, pos,
  343. f'take cover (current: {threat_level}, new: {self.max_threat[pos]})')
  344. actions.put(priority, action)
  345. # Convert
  346. k0_convert = 15
  347. k_convert_number = -10
  348. k_convert_danger = 20
  349. k_convert_threat_on = 3
  350. k_convert_hurted = 1
  351. k_convert_neighbor_threat = -15
  352. k_convert_neighbor_opponent = -20
  353. k_convert_distance = 10
  354. if self.cult_leader:
  355. immediate_targets = [
  356. self.index[n] for n in self.neighbors(*self.cult_leader.pos)
  357. if n in self.index and self.index[n] not in self.owned_units and self.index[n] is not self.opponent_cult_leader
  358. ]
  359. for target in immediate_targets:
  360. priority = k0_convert
  361. priority += k_convert_neighbor_threat * (target.neutral and target in [t.unit for t in self.cult_leader.threaten_by])
  362. priority += k_convert_neighbor_opponent * target.opponent
  363. priority += k_convert_threat_on * max([t.damages for t in self.threats_on[target.pos] if t.unit.opponent], default=0)
  364. action = ActionConvert(self.cult_leader, target)
  365. actions.put(priority, action)
  366. # Go Convert
  367. if not immediate_targets and self.conversion_path:
  368. path = self.conversion_path.next_candidate()
  369. if path:
  370. targets = [self.index[c] for c in path[-1].candidates]
  371. priority = k0_convert
  372. priority += k_convert_number * len(targets)
  373. priority += k_convert_distance * len(path)
  374. priority += k_convert_danger * sum([self.max_threat[s.pos] for s in path])
  375. for target in targets:
  376. priority += k_convert_hurted * (Unit.MAX_HP - target.hp)
  377. priority += k_convert_threat_on * max([t.damages for t in self.threats_on[target.pos] if t.unit.opponent], default=0)
  378. action = ActionMove(self.cult_leader, path[1].pos,
  379. f'go convert {",".join([str(t.id) for t in targets])}')
  380. actions.put(priority, action)
  381. # Shoot opponent units
  382. k0_shoot = 0
  383. k_shoot_opponent_cultist = 6
  384. k_shoot_opponent_leader = 6 if self.neutrals else 10
  385. k_shoot_fatal = -5
  386. k_shoot_kill_before_he_kills = -5
  387. k_shoot_sacrifice = 30
  388. k0_runaway = 10
  389. k_runaway_threat = 10
  390. for a in self.allied_cultists:
  391. if not a.targets:
  392. continue
  393. for threat in a.targets:
  394. priority = k0_shoot
  395. if threat.target.neutral:
  396. continue
  397. # Il est probable que l'unité se fera tirer dessus par l'ennemi le plus proche: quel résultat?
  398. deadly_threaten_by = [t for t in a.threaten_by if t.fatal]
  399. if not a.threaten_by and a.has_been_shooted:
  400. log(f"<!!> unit {a.id} seems not threaten but has been hurted!")
  401. deadly_threaten_by.append(Threat(self.opponent_cult_leader, 10, a.pos, []))
  402. if not a.threaten_by and a.has_been_shooted:
  403. deadly_threaten_by.append(Threat(self.opponent_cult_leader, 10, a.pos, []))
  404. if deadly_threaten_by and not threat.fatal:
  405. # run away! (si possible)
  406. covers = [n for n in self.neighbors(*a.pos) if self.can_move_on(a, n)]
  407. if covers:
  408. for cover in covers:
  409. threats_on_cover = [t for t in self.threats_on[cover] if not t.unit.owned]
  410. if not any(t.fatal_for(a) for t in threats_on_cover):
  411. action = ActionMove(a, cover, f'run away!')
  412. priority += k0_runaway
  413. priority += k_runaway_threat * self.max_threat[cover]
  414. actions.put(priority, action)
  415. continue
  416. log(f'<!> {a.id} seems lost')
  417. elif threat.fatal and deadly_threaten_by:
  418. # l'unité peut tuer la cible, mais sera probablement tuée par une autre unité ennemie
  419. priority += k_shoot_sacrifice
  420. if not threat.fatal:
  421. k = k_shoot_opponent_leader if threat.target is self.opponent_cult_leader else k_shoot_opponent_cultist
  422. priority += k * len(threat.line)
  423. else:
  424. priority += k_shoot_fatal
  425. # peut tuer un adversaire avant qu'il ne tue un allié
  426. priority += k_shoot_kill_before_he_kills * any(t.fatal and t.target is not a for t in threat.target.targets)
  427. action = ActionShoot(a, threat.target,
  428. f'shoot from {a.pos} (damages: {threat.damages}, pos: {threat.pos}, target: {threat.target}, fatal: {threat.fatal}, line: {threat.line})')
  429. actions.put(priority, action)
  430. # Intercept
  431. k0_intercept = 20
  432. position_hp_min = 5
  433. k_intercept_diff = 3
  434. for a in self.allied_cultists:
  435. if a.hp < position_hp_min:
  436. continue
  437. for o in (u for u in self.owned_units if u is not a):
  438. neighbors = self.neighbors(*a.pos)
  439. for threat in o.threaten_by:
  440. if threat.fatal and not threat.fatal_for(a):
  441. for n in neighbors:
  442. if n in threat.line:
  443. action = ActionMove(a, n, f'intercept {threat}')
  444. priority = k0_intercept
  445. priority = k_intercept_diff * (a.hp - o.hp)
  446. actions.put(priority, action)
  447. # Position: go to front line
  448. k0_position = 50
  449. k_position_advantage = 40
  450. k_position_distance = 3 # on veut privilégier les plus près, mais pas non plus décourager ceux de l'arrière...
  451. for a in self.allied_cultists:
  452. if a.hp < position_hp_min:
  453. continue
  454. if self.max_threat[a.pos]:
  455. # unit already under threat, let shoot action manage this
  456. continue
  457. if any(self.max_threat[n] for n in self.neighbors(*a.pos)):
  458. # unit already on frontline
  459. continue
  460. # l'unité semble en sécurité, go to front-line
  461. nearest_frontline = self.discover(a, key=lambda x: self.can_move_on(a, x) and self.max_threat[x] >= 1,
  462. limit=1)
  463. # log(f"{a.id} - {nearest_frontline}")
  464. if not nearest_frontline:
  465. log(f"<!> {a.id} can not join nearest frontline")
  466. continue
  467. path, target = nearest_frontline[0]
  468. if path:
  469. # log(f"{a.id} - {path} - {target}")
  470. priority = k0_position
  471. priority += k_position_distance * len(path)
  472. priority += k_position_advantage * advantage
  473. action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
  474. actions.put(priority, action)
  475. # Move out
  476. k0_move_out = 20
  477. k_move_out_gain = -3
  478. k_move_out_still_in_path_but_later = 5
  479. k_move_out_default_gain = 4
  480. if self.cult_leader:
  481. actual_path = [s.pos for s in (self.conversion_path.next_candidate() or [])]
  482. ideal_path = [s.pos for s in (self.ideal_conversion_path.next_candidate() or [])]
  483. if ideal_path:
  484. for a in self.allied_cultists:
  485. if a.pos in ideal_path:
  486. gain = len(actual_path) - len(ideal_path) if actual_path else k_move_out_default_gain
  487. if gain > 0:
  488. for n in self.neighbors(*a.pos):
  489. if not self.can_move_on(a, n):
  490. continue
  491. priority = k0_move_out
  492. still_in_the_way = (n in ideal_path)
  493. if still_in_the_way:
  494. if ideal_path.index(n) > ideal_path.index(a.pos):
  495. priority += k_move_out_still_in_path_but_later
  496. else:
  497. continue
  498. priority += k_move_out_gain * gain
  499. action = ActionMove(a, n, f'move out of the way (gain: {gain}, in the way: {still_in_the_way})')
  500. actions.put(priority, action)
  501. # Shoot neutral units:
  502. k_shoot_dangerous_neutral_threat = 3
  503. k_shoot_dangerous_neutral_distance = 8
  504. dangerous_neutral_distance_limit = 3
  505. if self.opponent_cult_leader:
  506. discovered_by_opponent = self.discover(self.opponent_cult_leader,
  507. key=lambda x: x in self.index and self.index[x].neutral, limit=3)
  508. neutrals_threaten = {}
  509. for path, target_pos in discovered_by_opponent:
  510. if len(path) > dangerous_neutral_distance_limit:
  511. continue
  512. if target_pos not in neutrals_threaten or neutrals_threaten[target_pos] < len(path):
  513. neutrals_threaten[target_pos] = len(path)
  514. threaten_neutrals_from_leader = {}
  515. if self.cult_leader:
  516. discovered_by_leader = self.discover(self.cult_leader, key=lambda x: x in neutrals_threaten, limit=3)
  517. for path, target_pos in discovered_by_leader:
  518. if target_pos not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[
  519. target_pos] < len(path):
  520. threaten_neutrals_from_leader[target_pos] = len(path)
  521. # log(f"Nearest from opp. leader: {neutrals_threaten}")
  522. # log(f"Nearest from own leader: {threaten_neutrals_from_leader}")
  523. lost_causes = {}
  524. for target, dist in neutrals_threaten.items():
  525. if target not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target] <= \
  526. neutrals_threaten[target]:
  527. lost_causes[target] = (dist - threaten_neutrals_from_leader.get(target,
  528. 0)) # la distance retenue est la différence entre la distance entre la cible
  529. # et le leader ennemi, et celle entre la cible et le leader allié
  530. for a in self.allied_cultists:
  531. for pos, dist in lost_causes.items():
  532. if pos not in self.index:
  533. log(f"<!> {pos} is not in the index!!")
  534. continue
  535. u = self.index[pos]
  536. line_of_sight = self.line_of_sight(a.pos, u.pos)
  537. shooting_distance = len(line_of_sight)
  538. if not shooting_distance:
  539. continue
  540. if shooting_distance <= u.SHOOTING_RANGE:
  541. # la cible est à portée
  542. action = ActionShoot(a, u, f"peace keeping, shoot from {a.pos} ({line_of_sight})")
  543. priority = k_shoot_dangerous_neutral_distance * shooting_distance
  544. priority += k_shoot_dangerous_neutral_threat * dist
  545. actions.put(priority, action)
  546. # Last hope: take risks
  547. if not advantage and not actions:
  548. k0_last_chance = 80
  549. k_last_chance_danger = 5
  550. last_chance_hp_min = 5
  551. for a in self.allied_cultists:
  552. if a.hp < last_chance_hp_min:
  553. continue
  554. for n in self.neighbors(*a.pos):
  555. if not self.max_threat[n]:
  556. continue
  557. ongoing_fights = []
  558. for u in self.opponent_cultists:
  559. line_of_sight = self.line_of_sight(u.pos, n)
  560. if not line_of_sight:
  561. continue
  562. dist = self.manhattan(u.pos, n)
  563. if dist <= u.SHOOTING_RANGE:
  564. damages = u.SHOOTING_MAX_DAMAGE - dist
  565. ongoing_fights.append((u, damages))
  566. if any(d >= a.hp for _, d in ongoing_fights):
  567. # c'est du suicide: sortir revient à se prendre un coup fatal
  568. continue
  569. action = ActionMove(a, n, f"last hope, moving")
  570. priority = k0_last_chance
  571. priority += k_last_chance_danger * max([d for _, d in ongoing_fights], default=0)
  572. actions.put(priority, action)
  573. # TODO: prévoir une action intercept, où une unité alliée se place sur le chemin d'une menace fatale, si l'unité a suffisamment de pv pour affronter la menace sans mourir
  574. # 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);
  575. # TODO: augmenter la prise de risque pour la conversion, parfois ça peut changer le résultat si le leader a tous ses pvs
  576. # 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
  577. # 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
  578. # 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 :(
  579. # TODO: abandonner l'idée d'aller convertir si la menace est mortelle...
  580. return actions
  581. def in_grid(self, pos):
  582. return 0 <= pos[0] < self.width and 0 <= pos[1] < self.height
  583. def can_see_trough(self, pos):
  584. return self.in_grid(pos) and pos not in self.obstacles and pos not in self.index
  585. def can_move_on(self, unit, pos):
  586. return self.in_grid(pos) and pos not in self.obstacles and (pos not in self.index or self.index[pos] is unit)
  587. def can_discover(self, pos):
  588. return self.in_grid(pos) and pos not in self.obstacles
  589. def moving_cost(self, unit, pos):
  590. if not self.can_move_on(unit, pos):
  591. return -1
  592. return 1 + self.max_threat[pos]
  593. @staticmethod
  594. def manhattan(from_, to_):
  595. xa, ya = from_
  596. xb, yb = to_
  597. return abs(xa - xb) + abs(ya - yb)
  598. def neighbors(self, x, y):
  599. return self._neighbors[(x, y)]
  600. def zone(self, pos, radius):
  601. x0, y0 = pos
  602. zone = []
  603. for x in range(max(x0 - radius, 0), min(x0 + radius, self.width)):
  604. for y in range(max(y0 - radius, 0), min(y0 + radius, self.height)):
  605. dist = self.manhattan(pos, (x, y))
  606. if dist <= radius:
  607. zone.append((x, y))
  608. return zone
  609. @classmethod
  610. def line(cls, start, target):
  611. """
  612. adapted from https://github.com/fragkakis/bresenham/blob/master/src/main/java/org/fragkakis/Bresenham.java
  613. if strict is true, None is return if an obstacle interrupted the line; else a partial line is returned (from start to obstacle)
  614. """
  615. line = []
  616. x0, y0 = start
  617. x1, y1 = target
  618. reversed = y0 > y1
  619. if reversed:
  620. # on fait toujours de bas en haut, du coup on inverse au besoin
  621. x0, y0, x1, y1 = x1, y1, x0, y0
  622. dx = abs(x1 - x0)
  623. dy = abs(y1 - y0)
  624. sx = 1 if x0 < x1 else -1
  625. sy = 1 if y0 < y1 else -1
  626. err = dx - dy
  627. x, y = x0, y0
  628. while 1:
  629. e2 = 2 * err
  630. if e2 > -1 * dy:
  631. err -= dy
  632. x += sx
  633. if e2 < dx:
  634. err += dx
  635. y += sy
  636. if x == x1 and y == y1:
  637. break
  638. line.append((x, y))
  639. if reversed:
  640. line = line[::-1]
  641. line = [start] + line + [target]
  642. return line
  643. def line_of_sight(self, from_, to_):
  644. line = self.line(from_, to_)[1:]
  645. return line if all(self.can_see_trough(c) for c in line[:-1]) else []
  646. def shooting_distance(self, from_, to_):
  647. return len(self.line_of_sight(from_, to_))
  648. def path(self, unit, target):
  649. nodes = Queue()
  650. its, break_on = 0, 400
  651. origin = PathNode(*unit.pos)
  652. nodes.put(0, origin)
  653. while nodes:
  654. current = nodes.get()
  655. if current == target:
  656. path = []
  657. previous = current
  658. while previous:
  659. if previous != unit.pos:
  660. path.insert(0, previous)
  661. previous = previous.parent
  662. return path
  663. neighbors = self.neighbors(*current)
  664. for x, y in neighbors:
  665. its += 1
  666. if its > break_on:
  667. log("<!> pathfinding broken")
  668. return None
  669. if (x, y) == current.parent:
  670. continue
  671. if not self.can_move_on(unit, (x, y)):
  672. continue
  673. moving_cost = self.moving_cost(unit, (x, y))
  674. cost = current.cost + moving_cost
  675. priority = cost + 10 * Grid.manhattan((x, y), target)
  676. node = PathNode(x, y, current)
  677. node.cost = cost
  678. nodes.put(priority, node)
  679. return None
  680. def discover(self, unit, key, limit=5):
  681. paths = []
  682. nodes = []
  683. its, break_on = 0, 2000
  684. origin = DiscoveryNode(*unit.pos)
  685. nodes.append(origin)
  686. while nodes:
  687. current = nodes.pop(0) # l'ordre est important, pour que les premiers indexés soient les premiers analysés
  688. neighbors = self.neighbors(*current)
  689. for pos in neighbors:
  690. its += 1
  691. if its > break_on:
  692. log(f"<!> discovery broke, {len(paths)} results")
  693. return paths
  694. if current != unit.pos and key(pos):
  695. # TODO: actuellement, l'algo pourra retourner x chemins différents vers la même cible
  696. # il faudrait retourner les trois meilleurs chemins vers x cibles (selon 'limit')
  697. path = current.ancestors[1:] + [current]
  698. paths.append((path, pos))
  699. if len(paths) >= limit:
  700. return paths
  701. continue
  702. if pos in current.ancestors:
  703. continue
  704. if not self.can_move_on(unit, pos):
  705. continue
  706. node = DiscoveryNode(*pos, 0, current.ancestors + [current])
  707. nodes.append(node)
  708. return paths
  709. def get_conversion_path(self, unit, key, moving_key, limit=5):
  710. """ essaies de trouver le meilleur chemin pour relier des cases dont au moins une voisine valide
  711. la condition 'key' (dans la limite de 'limit')"""
  712. nodes = Queue()
  713. winners = Queue()
  714. its, break_on = 0, 1000
  715. origin = DiscoveryNode(*unit.pos)
  716. abandon_at = 120 # number of paths explored
  717. nodes.put(0, origin)
  718. for n in self.neighbors(*unit.pos):
  719. if key(n):
  720. origin.matches.append(n)
  721. while nodes:
  722. its += 1
  723. if its > break_on:
  724. log(f"<!> get_conversion_path broke")
  725. break
  726. if len(nodes.items) > abandon_at:
  727. log("> get_conversion_path early exit")
  728. break
  729. current = nodes.get()
  730. for pos in self.neighbors(*current):
  731. if not moving_key(pos):
  732. continue
  733. moving_cost = 1 + self.max_threat[pos]
  734. matches = []
  735. for n in self.neighbors(*pos):
  736. if n not in current.matches and key(n):
  737. matches.append(n)
  738. cost = current.cost + moving_cost
  739. priority = 1000 * cost - (2000 * (len(current.matches) + len(matches)))
  740. priority += 100 * len(
  741. [a for a in current.ancestors if a == pos]) # décourage de revenir à une case visitée
  742. node = DiscoveryNode(
  743. pos[0],
  744. pos[1],
  745. cost,
  746. current.ancestors + [current],
  747. current.matches + matches
  748. )
  749. if matches:
  750. winners.put(40 * node.cost - 100 * len(node.matches), node)
  751. nodes.put(priority, node)
  752. if len(current.matches) >= limit or its > break_on:
  753. break
  754. try:
  755. best_node = winners.get()
  756. except IndexError:
  757. if nodes:
  758. best_node = nodes.get()
  759. else:
  760. best_node = origin
  761. return ConversionPath.make_from_discovery_node(best_node)
  762. def _repr_cell(self, pos):
  763. return f"{self.max_threat[pos]}"
  764. # return f"{self.control[pos]}/{self.max_threat[pos]}"
  765. # return self.heat_map[pos]
  766. if pos in self.obstacles:
  767. return "X"
  768. unit = self.index.get(pos, None)
  769. if type(unit) is CultLeader:
  770. return "C"
  771. elif unit is None:
  772. return "."
  773. else:
  774. return "U"
  775. def graph(self):
  776. return "\n".join(
  777. ["|".join([str(self._repr_cell((x, y))) for x in range(self.width)]) for y in range(self.height)])
  778. # Create grid
  779. GRID = Grid(*[int(i) for i in input().split()])
  780. obstacles_input = [input() for y in range(GRID.height)]
  781. GRID.obstacles = [(x, y) for y, row in enumerate(obstacles_input) for x, val in enumerate(row) if val == 'x']
  782. GRID.pre_compute()
  783. while 1:
  784. GRID.reinit_round()
  785. for _ in range(int(input())):
  786. GRID.update_unit(*[int(j) for j in input().split()])
  787. log(f"start round {GRID.round}")
  788. GRID.update()
  789. actions = GRID.build_actions()
  790. # print("\n" + GRID.graph(), file=sys.stderr)
  791. for action in actions.items:
  792. log(f"* {action}")
  793. try:
  794. action = actions.get()
  795. except IndexError:
  796. log("no action...")
  797. action = ActionWait()
  798. log(f"exec : {action}")
  799. action.exec()
  800. GRID.round += 1