spring2022.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. import sys
  2. import time
  3. import heapq
  4. import math
  5. debug = True
  6. t0 = time.time()
  7. # todo: attacking strategy: place one guy nearer to opponent hq
  8. # todo: considerate shielding monsters threatening opponent hq
  9. # todo: review the order heroes are playing in each strategy
  10. def log(*msg):
  11. if debug:
  12. print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr, flush=True)
  13. def time_to(total, step):
  14. """ number of steps to reach total """
  15. return total // step + (1 if total % step > 0 else 0)
  16. class BaseClass:
  17. def __repr__(self):
  18. return f"<{self.__class__.__name__}: {self.__dict__}>"
  19. class Queue(BaseClass):
  20. def __init__(self):
  21. self.items = []
  22. def __bool__(self):
  23. return bool(self.items)
  24. def __repr__(self):
  25. return str(self.items)
  26. def values(self):
  27. return (v for _, v in self.items)
  28. def put(self, priority, item):
  29. while priority in [p for p, _ in self.items]:
  30. priority += 1
  31. heapq.heappush(self.items, (priority, item))
  32. def get(self):
  33. return heapq.heappop(self.items)[1]
  34. def get_items(self):
  35. return heapq.heappop(self.items)
  36. class GridItem(BaseClass):
  37. def __init__(self, pos=(-1, -1)):
  38. self._previous_pos = None
  39. self._pos = pos
  40. @property
  41. def x(self):
  42. return self.pos[0]
  43. @property
  44. def y(self):
  45. return self.pos[1]
  46. @property
  47. def pos(self):
  48. return self._pos
  49. @pos.setter
  50. def pos(self, pos):
  51. self._previous_pos = self.pos
  52. self._pos = pos
  53. @property
  54. def previous_pos(self):
  55. return self._previous_pos
  56. class HeadQuarter(GridItem):
  57. SIGHT = 6000
  58. def __init__(self, pos):
  59. super().__init__(pos)
  60. self.health = 3
  61. self.mana = 10
  62. class Role:
  63. NAME = "None"
  64. POSITION = (1500, 1500)
  65. ALT_POSITIONS = []
  66. def __repr__(self):
  67. return self.__class__.__name__
  68. class Defender(Role):
  69. NAME = "Defender"
  70. POSITION = (1250, 1250)
  71. class DefenderRight(Defender):
  72. NAME = "Defender (right)"
  73. POSITION = (500, 3000)
  74. class DefenderLeft(Defender):
  75. NAME = "Defender (left)"
  76. POSITION = (3000, 500)
  77. class GateKeeper(Role):
  78. NAME = "Gatekeeper"
  79. POSITION = (4500, 3000)
  80. class GateKeeperRight(GateKeeper):
  81. NAME = "Gatekeeper (right)"
  82. POSITION = (1500, 5000)
  83. class GateKeeperLeft(GateKeeper):
  84. NAME = "Gatekeeper (left)"
  85. POSITION = (5500, 1000)
  86. class Hunter(Role):
  87. NAME = "Hunter"
  88. POSITION = (6000, 4000)
  89. ALT_POSITIONS = [(3500, 6500), (8000, 1500)]
  90. class HunterRight(Hunter):
  91. NAME = "Hunter (right)"
  92. POSITION = (3500, 6500)
  93. ALT_POSITIONS = [(1500, 7500), (6000, 4000)]
  94. class HunterLeft(Hunter):
  95. NAME = "Hunter (left)"
  96. POSITION = (8000, 1500)
  97. ALT_POSITIONS = [(9000, 500), (6000, 4000)]
  98. class Attacker(Role):
  99. NAME = "Attacker"
  100. POSITION = (7500, 4000)
  101. ALT_POSITIONS = [(5000, 6500), (8000, 2000)]
  102. class AttackerRight(Attacker):
  103. NAME = "Attacker (right)"
  104. POSITION = (5000, 6500)
  105. ALT_POSITIONS = [(1500, 7500), (6000, 4000)]
  106. class AttackerLeft(Attacker):
  107. NAME = "Attacker (left)"
  108. POSITION = (8000, 2000)
  109. ALT_POSITIONS = [(9000, 500), (6000, 4000)]
  110. class Saboteur(Role):
  111. NAME = "Saboteur"
  112. POSITION = (15000, 6000)
  113. ALT_POSITIONS = [(12000, 3500), (10000, 6000)]
  114. class Strategy(BaseClass):
  115. def __init__(self, roles, default=Defender):
  116. self.roles = roles
  117. self.pos = 0
  118. self.default = default
  119. def pick(self):
  120. """ Get the next role in line for the hero, or defender by default """
  121. try:
  122. role = self.roles[self.pos]
  123. self.pos += 1
  124. return role
  125. except IndexError:
  126. return self.default
  127. class StrategyFullDefense(Strategy):
  128. NAME = "Full Defense"
  129. def __init__(self):
  130. super().__init__([Defender, DefenderLeft, DefenderRight])
  131. class StrategyDefensive(Strategy):
  132. NAME = "Defensive"
  133. def __init__(self):
  134. super().__init__([Attacker, GateKeeper, Defender])
  135. class StrategyBalanced(Strategy):
  136. NAME = "Balanced"
  137. def __init__(self):
  138. super().__init__([Attacker, Hunter, Defender])
  139. class StrategyPrudentFarm(Strategy):
  140. NAME = "Farm (prudent)"
  141. def __init__(self):
  142. super().__init__([HunterLeft, HunterRight, GateKeeper])
  143. class StrategyFarm(Strategy):
  144. NAME = "Farm"
  145. def __init__(self):
  146. super().__init__([Attacker, HunterLeft, HunterRight])
  147. class StrategyAggressive(Strategy):
  148. NAME = "Aggressive"
  149. def __init__(self):
  150. super().__init__([Saboteur, Attacker, GateKeeper])
  151. class Action(BaseClass):
  152. def __init__(self, priority=1000):
  153. self.priority = priority
  154. self.feasibility = 0
  155. self.done = False
  156. self._pos = None
  157. @property
  158. def pos(self):
  159. return self._pos
  160. def do(self):
  161. self.done = True
  162. def __repr__(self):
  163. return f"{self.__class__.__name__} [{self.priority}/{self.feasibility}]"
  164. class Move(Action):
  165. def __init__(self, pos, priority=1000):
  166. super().__init__(priority)
  167. self._pos = pos
  168. def do(self):
  169. super().do()
  170. print(f"MOVE {self.pos[0]} {self.pos[1]}")
  171. def __repr__(self):
  172. return f"{self.__class__.__name__} {self.pos} [{self.priority}/{self.feasibility}]"
  173. class Explore(Move):
  174. pass
  175. class MoveAt(Action):
  176. def __init__(self, target, priority=1000):
  177. super().__init__(priority)
  178. self.target = target
  179. @property
  180. def pos(self):
  181. return self.target.pos
  182. def do(self):
  183. super().do()
  184. print(f"MOVE {self.pos[0]} {self.pos[1]}")
  185. def __repr__(self):
  186. return f"{self.__class__.__name__} {self.target.id} [{self.priority}/{self.feasibility}]"
  187. class Defend(MoveAt):
  188. pass
  189. class Hunt(MoveAt):
  190. pass
  191. class Reposition(Move):
  192. pass
  193. class Cast(Action):
  194. SPELL = ""
  195. MANA_COST = 10
  196. def __init__(self, priority=1000):
  197. super().__init__(priority)
  198. self.params = []
  199. def do(self):
  200. super().do()
  201. print(f"SPELL {self.SPELL} {' '.join(map(str, self.params))}")
  202. def __repr__(self):
  203. return f"{self.__class__.__name__} {' '.join(map(str, self.params))} [{self.priority}/{self.feasibility}]"
  204. class CastWind(Cast):
  205. SPELL = "WIND"
  206. RADIUS = 1280
  207. DEPTH = 2200
  208. def __init__(self, pos, priority=1000):
  209. super().__init__(priority)
  210. self.params = pos
  211. class CastControl(Cast):
  212. SPELL = "CONTROL"
  213. RANGE = 2200
  214. def __init__(self, target_id, pos, priority=1000):
  215. super().__init__(priority)
  216. x, y = pos
  217. self.params = [target_id, x, y]
  218. class CastShield(Cast):
  219. SPELL = "SHIELD"
  220. RANGE = 2200
  221. DURATION = 12
  222. def __init__(self, target_id, priority=1000):
  223. super().__init__(priority)
  224. self.params = [target_id]
  225. class Wait(Action):
  226. def do(self):
  227. print(f"WAIT")
  228. class Entity(GridItem):
  229. TYPE_MONSTER = 0
  230. TYPE_HERO = 1
  231. TYPE_OPPONENT_HERO = 2
  232. def __init__(self, id_, pos=(-1, -1)):
  233. super().__init__(pos)
  234. self.id = id_ # Unique identifier
  235. self.shield_life = -1 # Count down until shield spell fades
  236. self.health = -1 # Remaining health of this monster
  237. self.is_controlled = False
  238. # Computed properties
  239. self.visible = False
  240. self.distance_to_hq = None
  241. self.moving_time_to_hq = None
  242. self.distance_to_opponent_hq = None
  243. self.moving_time_to_opponent_hq = None
  244. self.distance_from_entity = {}
  245. self.moving_time_from_entity = {}
  246. self.go_toward_hq = False
  247. self.go_toward_opponent_hq = False
  248. self.is_on_my_side = False
  249. self.is_on_opponent_side = False
  250. self.camper = False
  251. def init_round(self):
  252. self.visible = False
  253. self.distance_to_hq = None
  254. self.moving_time_to_hq = None
  255. self.distance_to_opponent_hq = None
  256. self.moving_time_to_opponent_hq = None
  257. self.distance_from_entity = {}
  258. self.moving_time_from_entity = {}
  259. self.go_toward_hq = False
  260. self.go_toward_opponent_hq = False
  261. self.is_on_my_side = False
  262. self.is_on_opponent_side = False
  263. self.camper = False
  264. class Hero(Entity):
  265. ATTACK = 2
  266. ATTACK_RANGE = 800
  267. SPEED = 800
  268. SIGHT = 2200
  269. def __init__(self, id_):
  270. super().__init__(id_)
  271. self.role = Role
  272. self.current_role_pos = None
  273. class OpponentHero(Hero):
  274. def __init__(self, id_):
  275. super().__init__(id_)
  276. class Monster(Entity):
  277. THREAT_FOR_ME = 1
  278. THREAT_FOR_OPPONENT = 2
  279. NO_THREAT = 0
  280. SPEED = 400
  281. THREAT_RANGE = 5000
  282. ATTACK_RANGE = 300
  283. def __init__(self, id_):
  284. super().__init__(id_)
  285. self.near_base = False
  286. self.threat_for = self.NO_THREAT
  287. self.vx = 0 # Trajectory of this monster
  288. self.vy = 0
  289. @property
  290. def next_pos(self):
  291. return self.x + self.vx, self.y + self.vy
  292. @property
  293. def threats_hq(self):
  294. return self.threat_for == self.THREAT_FOR_ME
  295. @property
  296. def threats_opponent_hq(self):
  297. return self.threat_for == self.THREAT_FOR_OPPONENT
  298. @property
  299. def alive(self):
  300. return self.health > 0
  301. class Grid(BaseClass):
  302. MAX_X = 17630
  303. MAX_Y = 9000
  304. def __init__(self):
  305. self.threat_level = 0
  306. self.round = 0
  307. self._index = {}
  308. self.hq = None
  309. self.opponent_hq = None
  310. self.strategy = None
  311. @property
  312. def index(self):
  313. return self._index
  314. def get(self, _id):
  315. return self._index[_id]
  316. @property
  317. def entities(self):
  318. return [e for e in self._index.values() if e.visible]
  319. @property
  320. def heroes(self):
  321. return sorted([v for v in self.entities if type(v) is Hero], key=lambda x: x.id)
  322. @property
  323. def monsters(self):
  324. return [v for v in self.entities if type(v) is Monster]
  325. @property
  326. def opponent_heroes(self):
  327. return [v for v in self.entities if type(v) is OpponentHero]
  328. def init_game(self):
  329. self.hq = HeadQuarter([int(i) for i in input().split()])
  330. self.opponent_hq = HeadQuarter((Grid.MAX_X, Grid.MAX_Y) if self.hq.x == 0 else (0, 0))
  331. _ = int(input()) # Heroes per player, always 3
  332. def update_entity(self, entity):
  333. entity.distance_to_hq = self.distance(self.hq.pos, entity.pos)
  334. entity.moving_time_to_hq = self.moving_time_to(entity, self.hq.pos)
  335. entity.distance_to_opponent_hq = self.distance(self.opponent_hq.pos, entity.pos)
  336. entity.moving_time_to_opponent_hq = self.moving_time_to(entity, self.opponent_hq.pos)
  337. entity.distance_from_entity = {e.id: self.distance(e.pos, entity.pos) for e in self.entities if
  338. e.id != entity.id}
  339. entity.moving_time_from_entity = {e.id: self.moving_time_to(entity, e.pos) for e in self.entities if
  340. e.id != entity.id}
  341. if type(entity) is Monster:
  342. entity.go_toward_hq = (entity.vx <= 0 and entity.vy <= 0 and self.hq.x == 0 or
  343. entity.vx >= 0 and entity.vy >= 0 and self.hq.x != 0)
  344. entity.go_toward_opponent_hq = (entity.vx >= 0 and entity.vy >= 0 and self.opponent_hq.x != 0 or
  345. entity.vx <= 0 and entity.vy <= 0 and self.opponent_hq.x == 0)
  346. entity.is_on_my_side = entity.distance_to_hq < entity.distance_to_opponent_hq
  347. entity.is_on_opponent_side = not entity.is_on_my_side
  348. if type(entity) is OpponentHero:
  349. if entity.previous_pos and entity.pos == entity.previous_pos and entity.is_on_my_side:
  350. entity.camper = True
  351. def init_round(self):
  352. """ get inputs and reinit the state of entities """
  353. self.round += 1
  354. log(f"Round {self.round}")
  355. # *** Update HQ status
  356. self.hq.health, self.hq.mana = [int(j) for j in input().split()]
  357. self.opponent_hq.health, self.opponent_hq.mana = [int(j) for j in input().split()]
  358. # *** Reinit the entities state
  359. for entity in self.entities:
  360. entity.init_round()
  361. # *** Update entities
  362. entity_count = int(input()) # Amount of heroes and monsters you can see
  363. for i in range(entity_count):
  364. _id, _type, _x, _y, shield_life, is_controlled, health, vx, vy, near_base, threat_for = [int(j) for j in
  365. input().split()]
  366. # Entity is not indexed already
  367. if _id not in self.index:
  368. if _type == Entity.TYPE_MONSTER:
  369. entity = Monster(_id)
  370. elif _type == Entity.TYPE_HERO:
  371. entity = Hero(_id)
  372. elif _type == Entity.TYPE_OPPONENT_HERO:
  373. entity = OpponentHero(_id)
  374. else:
  375. log("Error: unknown type ({_type})")
  376. break
  377. self.index[_id] = entity
  378. # Update entity
  379. entity = self.get(_id)
  380. entity.pos = (_x, _y)
  381. entity.shield_life = shield_life
  382. entity.is_controlled = is_controlled
  383. entity.health = health
  384. entity.visible = True
  385. if type(entity) is Monster:
  386. entity.near_base = bool(near_base)
  387. entity.threat_for = threat_for
  388. entity.vx = vx
  389. entity.vy = vy
  390. # purge index
  391. self.purge()
  392. # update entities
  393. for entity in self.entities:
  394. self.update_entity(entity)
  395. # *** define strategy and roles
  396. self.threat_level = self.compute_threat_level()
  397. self.strategy = self.define_strategy()
  398. for hero in self.heroes:
  399. role = self.strategy.pick()
  400. if hero.role != role:
  401. hero.current_role_pos = None
  402. elif hero.pos in self.get_role_positions(hero.role):
  403. # remember as the last occupied role position
  404. hero.current_role_pos = hero.pos
  405. hero.role = role
  406. log(f'## Threat: {self.threat_level} - Strategy: {self.strategy.NAME} ##')
  407. log("Heroes: {}".format(','.join(
  408. [f"<{h.id},P:{h.pos},M:{h.moving_time_to_hq},O:{int(h.is_on_opponent_side)}>" for h in self.heroes])))
  409. log("Monsters: {}".format(','.join(
  410. [f"<{m.id},P:{m.pos},T:{int(m.threats_hq)},V:{int(m.visible)},M:{m.moving_time_to_hq},"
  411. f"N:{int(m.go_toward_hq)},O:{int(m.go_toward_opponent_hq)},S:{int(m.shield_life > 0)}>" for m
  412. in self.monsters])))
  413. log("Opponents: {}".format(','.join(
  414. [f"<{o.id},P:{o.pos},S:{o.shield_life},M:{o.moving_time_to_hq},C:{int(o.camper)}>" for o in self.opponent_heroes])))
  415. def purge(self):
  416. """ remove dead entities from the index """
  417. self._index = {k: e for k, e in self._index.items() if type}
  418. index = {}
  419. for entity in self.entities:
  420. if isinstance(entity, Monster):
  421. if self.visible(entity.next_pos) and not entity.visible:
  422. # entity should be visible, but has not been seen, presumed dead
  423. continue
  424. index[entity.id] = entity
  425. self._index = index
  426. def compute_threat_level(self):
  427. level = 0
  428. for monster in self.monsters:
  429. if monster.threats_hq:
  430. level += (monster.health + monster.shield_life) * max(24 - monster.moving_time_to_hq, 0)
  431. for enemy in self.opponent_heroes:
  432. if enemy.is_on_my_side:
  433. level += 50
  434. return level
  435. def define_strategy(self):
  436. # *** Define strategy
  437. if self.threat_level > 1400:
  438. return StrategyFullDefense()
  439. if self.threat_level > 1000:
  440. return StrategyDefensive()
  441. elif self.threat_level > 800:
  442. return StrategyBalanced()
  443. if self.round < 40:
  444. if self.threat_level:
  445. return StrategyPrudentFarm()
  446. else:
  447. return StrategyFarm()
  448. elif self.round < 100:
  449. return StrategyBalanced()
  450. else:
  451. return StrategyAggressive()
  452. def get_action_for(self, hero):
  453. actions = []
  454. k0_defend = 10
  455. k0_reposition = 250
  456. k0_hunt = 200
  457. k0_cast_wind = 150
  458. k0_cast_offensive_wind = 220
  459. k0_cast_control = 100
  460. k0_cast_control_monster = 100
  461. k0_cast_offensive_control = 200
  462. k0_cast_shield = 150
  463. k0_cast_shield_on_monster = 80
  464. k_attack_time = 5
  465. k_hero_nearer = 30
  466. k_defend_against_shielded = -20
  467. k_defender_priority_bonus = -50
  468. k_go_toward_hq = -30
  469. k_hunt_distance = 5
  470. k_hunt_pv = 2
  471. k_position_distance_from_pos = -8
  472. k_cast_wind_threats = -1
  473. k_cast_wind_enemy = -20
  474. k_cast_wind_enemy_on_my_side = -10
  475. k_cast_wind_enemy_near_hq = -5
  476. k_cast_wind_proximity = 5
  477. k_cast_wind_bad_target = 10
  478. k_cast_wind_last_defense = -30
  479. k_cast_wind_last_defense_limit = 10
  480. k_control_health = -2
  481. k_control_time = 5
  482. k_control_going_toward = -20
  483. k_shield_enemy_near = -20
  484. k_shield_monster = -1
  485. k_shield_monster_distance = 3
  486. k_cast_shield_distance = 10
  487. k_camper_bonus = -50
  488. k_cast_control_on_opponent_defender = 10
  489. k_cast_control_on_opponent_defender_threat = -1
  490. defend_threshold = 18
  491. min_control_hp = 15
  492. min_shield_distance = max(CastWind.RADIUS, CastControl.RANGE)
  493. min_shield_threat = 60
  494. offensive_buff = -1
  495. offensive_buff_control = -2
  496. k_feasibility_moving_time = 10
  497. k_feasibility_casting_cost = 5
  498. k_feasibility_far_from_pos = 5
  499. k_feasibility_defender_far_from_pos = 10
  500. min_mana_priority = 240
  501. # *** Defend
  502. for monster in self.monsters:
  503. if monster.threats_hq and monster.moving_time_to_hq < defend_threshold:
  504. priority = k0_defend
  505. # Réduire la priorité en fonction de la distance
  506. priority += (k_attack_time * monster.moving_time_to_hq)
  507. # TODO: réduire priority pour chaque monstre suppl que l'attaque pourrait toucher
  508. priority += k_defend_against_shielded * int(monster.shield_life > 0)
  509. # Réduire la priorité pour chaque héros plus proche du qg
  510. for other_hero in [h for h in self.heroes if h is not hero]:
  511. if other_hero.moving_time_to_hq < monster.moving_time_to_hq:
  512. priority += k_hero_nearer
  513. if isinstance(hero, (Defender, GateKeeper)):
  514. priority += k_defender_priority_bonus
  515. action = Defend(monster, priority)
  516. actions.append(action)
  517. # *** Position
  518. role_pos = self.get_role_position(hero)
  519. if hero.pos != role_pos:
  520. priority = k0_reposition
  521. priority += k_position_distance_from_pos * self.moving_time_to(hero, role_pos)
  522. action = Reposition(role_pos, priority)
  523. actions.append(action)
  524. # *** Hunt
  525. if hero.is_on_my_side and \
  526. not issubclass(hero.role, (Defender, GateKeeper)) and \
  527. not (issubclass(hero.role, Saboteur) and hero.is_on_my_side):
  528. for monster in self.monsters:
  529. if monster.threats_opponent_hq:
  530. # it's defense domain or not worth it
  531. continue
  532. priority = k0_hunt
  533. priority += k_hunt_distance * hero.moving_time_from_entity[monster.id]
  534. priority += k_go_toward_hq * monster.go_toward_hq
  535. priority += k_hunt_pv * monster.health
  536. action = Hunt(monster, priority)
  537. actions.append(action)
  538. # *** Cast WIND on threats
  539. if self.hq.mana >= CastWind.MANA_COST:
  540. affected = [e for e in self.in_wind_area(hero) if not e.shield_life]
  541. if affected:
  542. priority = k0_cast_wind
  543. # Affected entities
  544. priority += k_cast_wind_threats * sum([m.health for m in affected if type(m) is Monster and m.threats_hq])
  545. priority += k_cast_wind_enemy * len([m for m in affected if type(m) is OpponentHero])
  546. priority += k_cast_wind_enemy_on_my_side * len([m for m in affected if type(m) is OpponentHero and m.is_on_my_side])
  547. priority += k_cast_wind_enemy_near_hq * sum([m.moving_time_to_hq for m in affected if type(m) is OpponentHero if m.distance_to_hq < HeadQuarter.SIGHT])
  548. priority += k_camper_bonus * len([m for m in affected if type(m) is OpponentHero and m.is_on_my_side and m.camper])
  549. # Hq average proximity
  550. priority += k_cast_wind_proximity * hero.moving_time_to_hq
  551. # last defence
  552. priority += sum([k_cast_wind_last_defense * (k_cast_wind_last_defense_limit - m.moving_time_to_hq) for m in affected if type(m) is Monster and m.moving_time_to_hq < k_cast_wind_last_defense_limit])
  553. action = CastWind(self.opponent_hq.pos, priority)
  554. actions.append(action)
  555. # *** Cast CONTROL on approaching monsters
  556. if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST):
  557. possible_targets = [m for m in self.monsters if
  558. m.distance_from_entity[hero.id] < CastControl.RANGE and not m.shield_life]
  559. for monster in possible_targets:
  560. if monster.health < min_control_hp:
  561. # too weak
  562. continue
  563. if monster.go_toward_opponent_hq:
  564. # already targeting enemy
  565. continue
  566. if monster.distance_to_hq <= (monster.THREAT_RANGE + monster.SPEED):
  567. # too late...
  568. continue
  569. if monster.is_controlled:
  570. # already controlled
  571. continue
  572. priority = k0_cast_control
  573. priority += k_control_health * monster.health
  574. priority += k_control_time * min(monster.moving_time_to_opponent_hq, monster.moving_time_to_hq)
  575. priority += k_control_going_toward * monster.go_toward_hq
  576. action = CastControl(monster.id, self.opponent_hq.pos, priority)
  577. actions.append(action)
  578. # *** Cast CONTROL on campers
  579. if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST):
  580. possible_targets = [e for e in self.opponent_heroes if
  581. e.distance_from_entity[hero.id] < CastControl.RANGE
  582. and not e.shield_life and e.camper]
  583. for enemy in possible_targets:
  584. if enemy.is_controlled:
  585. # already controlled
  586. continue
  587. priority = k0_cast_control
  588. action = CastControl(enemy.id, self.opponent_hq.pos, priority)
  589. actions.append(action)
  590. # *** Cast SHIELD on self
  591. opponents_within_casting_range = [o.id for o in self.opponent_heroes if
  592. o.distance_from_entity[hero.id] < min_shield_distance]
  593. if hero.shield_life == 0 and \
  594. self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and \
  595. opponents_within_casting_range\
  596. and self.threat_level > min_shield_threat\
  597. and hero.is_on_my_side:
  598. # log(f"{hero.id}: shield against {[(o.id, o.pos, o.distance_from_entity[hero.id]) for o in self.opponent_heroes]}")
  599. priority = k0_cast_shield
  600. priority += k_shield_enemy_near * len(opponents_within_casting_range)
  601. priority += k_cast_shield_distance * min(hero.moving_time_to_hq, hero.moving_time_to_opponent_hq)
  602. action = CastShield(hero.id, priority)
  603. actions.append(action)
  604. if hero.is_on_opponent_side and not isinstance(self.strategy, (StrategyDefensive, StrategyFullDefense)):
  605. threat_on_opponent_hq = sum(m.health + m.shield_life for m in self.monsters if m.threats_opponent_hq)
  606. threat_on_opponent_hq -= 10 * len([o for o in self.opponent_heroes if o.distance_to_opponent_hq < Monster.THREAT_RANGE])
  607. log(f"Offensive buff: {threat_on_opponent_hq}")
  608. for monster in self.monsters:
  609. # cast control on monsters near opponent hq
  610. if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST) and monster.distance_from_entity[hero.id] < CastControl.RANGE:
  611. if monster.health < min_control_hp:
  612. # too weak
  613. continue
  614. if monster.go_toward_opponent_hq:
  615. # already targeting enemy
  616. continue
  617. if monster.shield_life > 0:
  618. continue
  619. priority = k0_cast_control_monster
  620. action = CastControl(monster.id, self.opponent_hq.pos, priority)
  621. actions.append(action)
  622. if threat_on_opponent_hq > 50:
  623. for monster in self.monsters:
  624. # cast shield on monsters threatening opponent hq
  625. if self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and monster.distance_from_entity[hero.id] < CastShield.RANGE:
  626. if not monster.threats_opponent_hq or not monster.go_toward_opponent_hq:
  627. # not targeting enemy
  628. continue
  629. if monster.shield_life > 0:
  630. continue
  631. priority = k0_cast_shield_on_monster
  632. priority += k_shield_monster_distance * monster.moving_time_to_opponent_hq
  633. priority += k_shield_monster * monster.health
  634. action = CastShield(monster.id, priority)
  635. actions.append(action)
  636. # cast wind on monsters threatening opponent hq
  637. affected = [e for e in self.in_wind_area(hero) if not e.shield_life]
  638. if self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and affected:
  639. priority = k0_cast_offensive_wind
  640. priority += k_cast_wind_threats * sum([m.health for m in affected if type(m) is Monster and m.threats_opponent_hq and not m.shield_life])
  641. priority += k_cast_wind_bad_target * len([e for e in affected if type(e) is OpponentHero and not e.shield_life])
  642. priority += k_cast_wind_proximity * hero.moving_time_to_opponent_hq
  643. priority += offensive_buff * threat_on_opponent_hq
  644. action = CastWind(self.opponent_hq.pos, priority)
  645. actions.append(action)
  646. # cast wind on enemies defending opponent hq
  647. affected = [e for e in self.in_wind_area(hero) if not e.shield_life]
  648. if self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and affected:
  649. priority = k0_cast_offensive_wind
  650. priority += k_cast_wind_enemy * len([e for e in affected if type(e) is OpponentHero and not e.shield_life])
  651. priority += k_cast_wind_bad_target * len([e for e in affected if type(e) is Monster and not e.shield_life])
  652. priority += k_cast_wind_proximity * hero.moving_time_to_opponent_hq
  653. priority += offensive_buff * threat_on_opponent_hq
  654. action = CastWind(self.hq.pos, priority)
  655. actions.append(action)
  656. for enemy in self.opponent_heroes:
  657. if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST) and enemy.distance_from_entity[hero.id] < CastControl.RANGE:
  658. if enemy.is_controlled:
  659. # already controlled
  660. continue
  661. if enemy.shield_life:
  662. continue
  663. priority = k0_cast_offensive_control
  664. priority += k_cast_control_on_opponent_defender * enemy.moving_time_to_opponent_hq
  665. priority += k_cast_control_on_opponent_defender_threat * threat_on_opponent_hq
  666. priority += offensive_buff_control * threat_on_opponent_hq
  667. action = CastControl(enemy.id, self.hq.pos, priority)
  668. actions.append(action)
  669. # Estimate feasibility
  670. for action in actions:
  671. action.feasibility = -1
  672. # ignore impossibles actions
  673. if isinstance(action, Defend):
  674. if action.target.moving_time_to_hq < hero.moving_time_to_hq:
  675. # too late...
  676. continue
  677. action.feasibility = 0
  678. if action.pos and not isinstance(action, Reposition):
  679. action.feasibility += k_feasibility_moving_time * self.moving_time_to(hero, action.pos)
  680. if isinstance(action, Cast):
  681. action.feasibility += k_feasibility_casting_cost * action.MANA_COST
  682. action.feasibility += k_feasibility_far_from_pos * self.moving_time_to(hero, hero.current_role_pos or hero.role.POSITION)
  683. actions = sorted([a for a in actions if a.feasibility >= 0], key=lambda a: a.priority + a.feasibility)
  684. try:
  685. log(actions)
  686. return actions[0]
  687. except IndexError:
  688. return Wait()
  689. def update_after_action(self, hero, action):
  690. if isinstance(action, (Move, MoveAt)):
  691. hero.pos = self.position_after_moving(hero, action.pos, hero.SPEED)
  692. # log(f"{hero.id}: move to {action.pos}, new pos: {hero.pos}")
  693. for monster in self.in_attack_area(hero):
  694. monster.health -= hero.ATTACK
  695. if monster.health <= 0:
  696. del self._index[monster.id]
  697. if isinstance(action, Cast):
  698. self.hq.mana -= action.MANA_COST
  699. if isinstance(action, CastWind):
  700. affected = self.in_wind_area(hero)
  701. for entity in affected:
  702. entity.pos = self.position_after_moving(entity, action.params, action.DEPTH)
  703. self.update_entity(entity)
  704. if isinstance(action, CastShield):
  705. self.index[action.params[0]].shield_life = CastShield.DURATION
  706. if isinstance(action, CastControl):
  707. target_id, x, y = action.params
  708. target = self.index[target_id]
  709. target.pos = self.position_after_moving(target, (x, y), target.SPEED)
  710. self.update_entity(target)
  711. target.is_controlled = True
  712. if type(target) is Monster:
  713. target.go_toward_opponent_hq = True
  714. if not isinstance(action, Reposition):
  715. # reinit alternative positions if hero has acted elsewhere
  716. hero.current_role_pos = None
  717. def play(self, hero):
  718. action = self.get_action_for(hero)
  719. log(f"> {action}")
  720. action.do()
  721. self.update_after_action(hero, action)
  722. @staticmethod
  723. def manhattan(from_, to_):
  724. xa, ya = from_
  725. xb, yb = to_
  726. return abs(xa - xb) + abs(ya - yb)
  727. @staticmethod
  728. def distance(from_, to_):
  729. xa, ya = from_
  730. xb, yb = to_
  731. return int(math.sqrt((xa - xb) ** 2 + abs(ya - yb) ** 2))
  732. @staticmethod
  733. def moving_time_to(entity, target):
  734. distance = Grid.distance(entity.pos, target)
  735. return time_to(distance, entity.SPEED)
  736. @staticmethod
  737. def position_after_moving(entity, pos, speed):
  738. """ get the entity position after it moved toward `pos` from `speed` """
  739. x0, y0 = entity.pos
  740. x1, y1 = pos
  741. dx, dy = (x1 - x0) / (x1 or 1), (y1 - y0) / (y1 or 1)
  742. return x0 + int(dx * speed), y0 + int(dy * speed)
  743. def visible(self, pos):
  744. return self.distance(self.hq.pos, pos) < self.hq.SIGHT or \
  745. any(self.distance(h.pos, pos) < h.SIGHT for h in self.heroes)
  746. def get_role_positions(self, role):
  747. positions = [role.POSITION] + role.ALT_POSITIONS
  748. if self.hq.x != 0:
  749. return [(self.MAX_X - x, self.MAX_Y - y) for x, y in positions]
  750. else:
  751. return positions
  752. def get_role_position(self, hero):
  753. """ get the position for the given role """
  754. role_positions = self.get_role_positions(hero.role)
  755. pos = role_positions[0]
  756. # log(f"{hero.pos} - {hero.current_role_pos} - {role_positions}")
  757. if hero.current_role_pos:
  758. try:
  759. i = role_positions.index(hero.current_role_pos)
  760. pos = role_positions[i + 1]
  761. except (ValueError, IndexError):
  762. pass
  763. return pos
  764. def in_attack_area(self, hero):
  765. return [e for e in self.monsters if e.distance_from_entity[hero.id] <= Hero.ATTACK_RANGE]
  766. def in_wind_area(self, hero):
  767. return [e for e in self.entities if type(e) is not Hero and e.distance_from_entity[hero.id] <= CastWind.RADIUS]
  768. def main():
  769. # *** init game
  770. grid = Grid()
  771. grid.init_game()
  772. while True:
  773. # *** get inputs and init round
  774. grid.init_round()
  775. # *** Compute priorities
  776. for hero in grid.heroes:
  777. grid.play(hero)
  778. main()