script.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  1. '''
  2. >> https://www.codingame.com/ide/173171838252e7c6fd6f3ff9cb8169431a08eec1
  3. @author: olivier.massot, may 2019
  4. '''
  5. import heapq
  6. import sys
  7. import time
  8. # TODO
  9. # * priorize attack on neighbors of pivots (allies'pivots to colonize and ennemies to threat)
  10. # * !!! moves: check before if unit is not on a zone which needs to be protected
  11. # x optimize moves, especially at start! (by introducing dispersion?)
  12. # * take units and towers in account when computing the threat
  13. # x priorize colonization of cells near HQ
  14. # ? when priority needs a lvl3 unit, find a way to spare
  15. # * resurrect the strategic value: number of cells depending of the cell
  16. # * consider defending also cells not owned
  17. # x limit the training of level 3 when it's really needed
  18. # * make a first turn of moves for units deep inside the territory, to avoid unecessary training and computing
  19. debug = True
  20. t0 = time.time()
  21. def log(*msg):
  22. if debug:
  23. print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr)
  24. # OWNER
  25. ME = 0
  26. OPPONENT = 1
  27. # BUILDING TYPE
  28. HQ = 0
  29. class Base():
  30. def __repr__(self):
  31. return f"<{self.__class__.__name__}: {self.__dict__}>"
  32. class Queue(Base):
  33. def __init__(self):
  34. self.items = []
  35. def __bool__(self):
  36. return bool(self.items)
  37. def __repr__(self):
  38. return str(self.items)
  39. def values(self):
  40. return (v for _, v in self.items)
  41. def put(self, item, priority):
  42. heapq.heappush(self.items, (priority, item))
  43. def fput(self, item, priority):
  44. while priority in [p for p, _ in self.items]:
  45. priority += 1
  46. self.put(item, priority)
  47. def get(self):
  48. return heapq.heappop(self.items)[1]
  49. class InterestQueue(Queue):
  50. def __add__(self, other):
  51. self.items += other.items
  52. return self
  53. def put(self, item):
  54. heapq.heappush(self.items, item)
  55. def get(self):
  56. return heapq.heappop(self.items)
  57. class BasePosition(Base):
  58. def __init__(self, cell, *args, **kwargs):
  59. self.interest = 0
  60. self.cell = cell
  61. @property
  62. def x(self):
  63. return self.cell.x
  64. @property
  65. def y(self):
  66. return self.cell.y
  67. @property
  68. def pos(self):
  69. return self.cell.pos
  70. def __lt__(self, other):
  71. return self.interest < other.interest
  72. def eval(self):
  73. raise NotImplementedError
  74. class Position(BasePosition):
  75. def __init__(self, cell):
  76. super().__init__(cell)
  77. self.possession = 0
  78. self.threat = 0
  79. self.pivot = 0
  80. self.union = 0
  81. self.depth = 0
  82. self.hq = 0
  83. self.tower = 0
  84. self.mine = 0
  85. self.dist_to_goal = 0
  86. self.min_level = 1
  87. def pre_eval(self):
  88. # *** Eval interest: the lower the better
  89. self.interest = 0
  90. self.min_level = 1
  91. # eval possession
  92. if self.cell.active_owned:
  93. self.possession = 1
  94. elif self.cell.active_opponent:
  95. self.possession = -1
  96. # eval threat
  97. self.threat = 0
  98. if self.cell.active_owned:
  99. self.threat = self.cell.threat
  100. self.covers = sum([grid[n].owned for n in self.cell.neighbors])
  101. # eval pivot
  102. self.pivot = sum([1 + grid[p].get_unit_level() for p in self.cell.pivot_for])
  103. # distance to the ennemy HQ
  104. self.dist_to_goal = Grid.manhattan(self.cell.pos, opponent.hq.pos)
  105. # distance to the own HQ
  106. self.dist_to_hq = Grid.manhattan(self.cell.pos, player.hq.pos)
  107. # priorize adjacent cells
  108. self.union = len([n for n in self.cell.neighbors if grid[n].active_owned])
  109. # favorize dispersion
  110. self.concentration = len([n for n in self.cell.neighbors if grid[n].unit and grid[n].unit.owned])
  111. self.deadends = len([n for n in self.cell.neighbors if not grid[n].movable])
  112. # include 'depthmap'
  113. self.depth = self.cell.depth
  114. self.under_tower = self.cell.under_tower
  115. self.overlaps_tower = sum([grid[n].under_tower for n in self.cell.neighbors])
  116. # priorize mines or HQ
  117. if self.cell.building:
  118. self.hq = int(self.cell.building.type_ == Building.HQ)
  119. self.tower = int(self.cell.building.type_ == Building.TOWER)
  120. self.mine = int(self.cell.building.type_ == Building.MINE)
  121. # *** Min level to go there
  122. if self.cell.unit and self.cell.unit.opponents:
  123. self.min_level = min([self.cell.unit.level + 1, 3])
  124. if self.cell.under_tower:
  125. self.min_level = 3
  126. def eval(self):
  127. self.pre_eval()
  128. self.interest = 3 * self.depth + self.dist_to_goal + 2 * self.concentration + 2 * self.deadends
  129. def __repr__(self):
  130. detail = [self.possession, self.threat, self.pivot, self.dist_to_goal,
  131. self.union, self.concentration, self.depth, self.hq, self.tower, self.mine]
  132. return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
  133. class Defend(Position):
  134. def __init__(self, cell, emergency = False):
  135. super().__init__(cell)
  136. self.emergency = emergency
  137. def __repr__(self):
  138. detail = [self.threat, self.covers, self.pivot, self.emergency,
  139. self.dist_to_hq, self.cell.danger, self.cell.critical,
  140. self.under_tower, self.overlaps_tower]
  141. return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
  142. def eval(self):
  143. self.pre_eval()
  144. self.interest = (200 if not self.emergency else 50) \
  145. - 2 * self.threat \
  146. - 2 * self.covers \
  147. - 8 * self.pivot \
  148. - 80 * self.cell.critical \
  149. - 25 * self.cell.danger \
  150. + 25 * self.cell.under_tower \
  151. + 10 * self.overlaps_tower
  152. class Attack(Position):
  153. def eval(self):
  154. self.pre_eval()
  155. self.interest = 15 * self.possession \
  156. - 5 * self.pivot \
  157. - 2 * self.union \
  158. + 3 * self.concentration \
  159. + 2 * self.deadends \
  160. + 4 * self.depth \
  161. + self.dist_to_goal \
  162. - 2 * max([0, 11 - self.dist_to_hq]) \
  163. - 30 * self.tower \
  164. - 15 * self.mine \
  165. - 100 * self.hq \
  166. + 10 * (self.min_level - 1)
  167. class MinePosition(BasePosition):
  168. def __init__(self, target):
  169. super().__init__(target)
  170. def eval(self):
  171. # the lower the better
  172. self.interest -= self.cell.depth
  173. class BaseLoc(Base):
  174. def __init__(self, x, y):
  175. self.x = x
  176. self.y = y
  177. @property
  178. def pos(self):
  179. return self.x, self.y
  180. class MineSite(BaseLoc):
  181. def __init__(self, x, y):
  182. super().__init__(x, y)
  183. class BaseOwnedLoc(BaseLoc):
  184. def __init__(self, x, y, owner):
  185. super().__init__(x, y)
  186. self.owner = owner
  187. @property
  188. def owned(self):
  189. return self.owner == ME
  190. @property
  191. def opponents(self):
  192. return self.owner == OPPONENT
  193. class Building(BaseOwnedLoc):
  194. HQ = 0
  195. MINE = 1
  196. TOWER = 2
  197. cost = {0: 0, 1: 20, 2: 15}
  198. maintenance = {0: 0, 1: 0, 2: 0}
  199. def __init__(self, owner, type_, x, y):
  200. super().__init__(x, y, owner)
  201. self.type_ = type_
  202. @property
  203. def hq(self):
  204. return self.type_ == Building.HQ
  205. class Unit(BaseOwnedLoc):
  206. cost = {1: 10, 2: 20, 3: 30}
  207. maintenance = {1: 1, 2: 4, 3: 20}
  208. def __init__(self, owner, id_, level, x, y):
  209. super().__init__(x, y, owner)
  210. self.id_ = id_
  211. self.level = level
  212. self.has_moved = False
  213. class Player(Base):
  214. def __init__(self, id_):
  215. self.id_ = id_
  216. self.gold = 0
  217. self.income = 0
  218. self.units = []
  219. self.buildings = []
  220. self.hq = None
  221. self.spent = 0
  222. self.new_charges = 0
  223. def update(self, gold, income, units, buildings):
  224. self.gold = gold
  225. self.income = income
  226. self.units = [u for u in units if u.owner == self.id_]
  227. self.buildings = [b for b in buildings if b.owner == self.id_]
  228. self.hq = next((b for b in self.buildings if b.type_ == HQ))
  229. self.spent = 0
  230. self.new_charges = 0
  231. @property
  232. def current_gold(self):
  233. return self.gold - self.spent
  234. @property
  235. def current_income(self):
  236. return self.income - self.new_charges
  237. def training_capacity(self):
  238. return min([(self.gold - self.spent) // Unit.cost[1], (self.income - self.new_charges) // Unit.maintenance[1]])
  239. def can_afford(self, lvl):
  240. return (self.gold - self.spent) >= Unit.cost[lvl] and (self.income - self.new_charges) >= Unit.maintenance[lvl]
  241. def max_affordable(self):
  242. for lvl in range(3, 0, -1):
  243. if self.can_afford(lvl):
  244. return lvl
  245. return 0
  246. def can_act(self):
  247. return self.training_capacity() > 0 or self.gold >= 15 or any(not unit.has_moved for unit in self.units)
  248. class Cell(Base):
  249. def __init__(self, x, y):
  250. self.x = x
  251. self.y = y
  252. self._content = "#"
  253. self.neighbors = []
  254. self.unit = None
  255. self.building = None
  256. self.mine_site = None
  257. self.under_tower = False
  258. self.depth = 0
  259. self.pivot_for = []
  260. self.critical = False
  261. self.danger = False
  262. self.threat = 0
  263. self.threat_by = None
  264. @property
  265. def pos(self):
  266. return self.x, self.y
  267. @property
  268. def raw_val(self):
  269. return self._content
  270. def update(self, content, unit = None, building = None):
  271. self._content = content
  272. self.unit = unit
  273. self.building = building
  274. self.under_tower = False
  275. self.depth = 0
  276. self.pivot_for = []
  277. self.threat = 0
  278. self.threat_by = None
  279. self.critical = False
  280. self.danger = False
  281. @property
  282. def movable(self):
  283. return self._content != "#"
  284. @property
  285. def owned(self):
  286. return self._content.lower() == "o"
  287. @property
  288. def opponents(self):
  289. return self._content.lower() == "x"
  290. @property
  291. def owner(self):
  292. if self.owned:
  293. return ME
  294. elif self.opponents:
  295. return OPPONENT
  296. else:
  297. return None
  298. @property
  299. def headquarter(self):
  300. return self.pos in Grid.hqs
  301. @property
  302. def occupied(self):
  303. return self.unit or self.building
  304. @property
  305. def active(self):
  306. return self._content.isupper()
  307. @property
  308. def active_owned(self):
  309. return self._content == "O"
  310. @property
  311. def active_opponent(self):
  312. return self._content == "X"
  313. def is_active_owner(self, player_id):
  314. if player_id == ME:
  315. return self.active_owned
  316. elif player_id == ME:
  317. return self.active_opponent
  318. else:
  319. return False
  320. def owned_unit(self):
  321. if self.unit and self.unit.owned:
  322. return self.unit
  323. def owned_building(self):
  324. if self.building and self.building.owned:
  325. return self.building
  326. def take_possession(self):
  327. self._content = "O"
  328. def desactivate(self):
  329. self._content = self._content.lower()
  330. def get_unit_level(self):
  331. return self.unit.level if self.unit else 0
  332. class Node(Base):
  333. def __init__(self, pos, path=[]):
  334. self.pos = pos
  335. self.path = path
  336. class PathNode(tuple):
  337. def __new__(self, x, y, parent=None):
  338. n = tuple.__new__(self, (x, y))
  339. n.parent = parent
  340. n.cost = 0
  341. return n
  342. def __repr__(self):
  343. return f"<{self[0]}, {self[1]}, c:{self.cost}>"
  344. class Grid(Base):
  345. dim = 12
  346. hqs = [(0,0), (11,11)]
  347. def __init__(self, mines_sites = []):
  348. self.cells = {(x, y): Cell(x, y) for x in range(Grid.dim) for y in range(Grid.dim)}
  349. for pos, cell in self.cells.items():
  350. cell.neighbors = [p for p in self.neighbors(*pos) if p in self.cells]
  351. self.units = []
  352. self.buildings = []
  353. for m in mines_sites:
  354. self.cells[(m.x, m.y)].mine_site = m
  355. self.threat_level = 0
  356. def print_grid(self):
  357. return "\n".join(["".join([c for c in row]) for row in self.grid])
  358. @property
  359. def pos(self):
  360. return self.x, self.y
  361. @property
  362. def grid(self):
  363. return [[self.cells[(x, y)].raw_val for x in range(Grid.dim)] for y in range(Grid.dim)]
  364. def __getitem__(self, key):
  365. return self.cells[key]
  366. def update(self, grid, buildings, units):
  367. buildings_ix = {(b.x, b.y): b for b in buildings}
  368. units_ix= {(u.x, u.y): u for u in units}
  369. self.buildings = list(buildings)
  370. self.units = list(units)
  371. for y, row in enumerate(grid):
  372. for x, c in enumerate(row):
  373. self.cells[(x, y)].update(c,
  374. units_ix.get((x, y), None),
  375. buildings_ix.get((x, y), None))
  376. log(" * update pivots")
  377. self.update_pivots()
  378. log(" * update threats")
  379. self.update_threats()
  380. self.update_state()
  381. def update_state(self):
  382. self.update_tower_areas()
  383. self.update_frontlines()
  384. self.update_depth_map()
  385. @staticmethod
  386. def manhattan(from_, to_):
  387. xa, ya = from_
  388. xb, yb = to_
  389. return abs(xa - xb) + abs(ya - yb)
  390. def neighbors(self, x, y, diags=False):
  391. neighs = [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
  392. if diags:
  393. neighs += [(x - 1, y - 1), (x + 1, y - 1), (x - 1, y + 1), (x + 1, y + 1)]
  394. return [(x, y) for x, y in neighs if 0 <= x < Grid.dim and 0 <= y < Grid.dim]
  395. @classmethod
  396. def zone(cls, center, radius):
  397. return [(x, y) for x in range(0, cls.dim) for y in range(0, cls.dim) if cls.manhattan(center, (x, y)) <= radius]
  398. def zone_set(self, center, radius):
  399. zone = set(center)
  400. for _ in range(radius):
  401. for p in zone:
  402. zone |= self.neighbors(*p)
  403. @staticmethod
  404. def closest(from_, in_):
  405. return min(in_, key=lambda x: Grid.manhattan(from_, x))
  406. def get_hq(self, player_id):
  407. return next((b for b in self.buildings if b.owner == player_id and b.hq))
  408. def update_tower_areas(self):
  409. for b in self.buildings:
  410. if b.type_ == Building.TOWER:
  411. self.cells[b.pos].under_tower = True
  412. for n in self.cells[b.pos].neighbors:
  413. self.cells[n].under_tower = True
  414. def update_frontlines(self):
  415. # update the current frontlines
  416. self.frontline = []
  417. self.frontex = []
  418. for cell in self.cells.values():
  419. if cell.active_owned:
  420. if any(self.cells[c].movable and not self.cells[c].active_owned
  421. for c in cell.neighbors):
  422. self.frontline.append(cell)
  423. def update_depth_map(self):
  424. buffer = [c.pos for c in self.frontline]
  425. for p in buffer:
  426. self.cells[p].depth = 1
  427. next_buffer = []
  428. while buffer:
  429. for p in buffer:
  430. for n in self.cells[p].neighbors:
  431. if self.cells[n].active_owned and not self.cells[n].depth:
  432. self.cells[n].depth = self.cells[p].depth + 1
  433. next_buffer.append(n)
  434. buffer = list(next_buffer)
  435. next_buffer = []
  436. def _active_owned(self, pos, player_id):
  437. return self.cells[pos].is_active_owner(player_id)
  438. def update_pivot_for(self, player_id):
  439. start = self.get_hq(player_id).pos
  440. start_node = Node(start)
  441. buffer = [start_node]
  442. nodes = {start_node}
  443. its, breakdown = 0, 1200
  444. ignored = [p for p, c in self.cells.items() if c.is_active_owner(player_id) and
  445. len([n for n in self.neighbors(*p, diags=True) if self._active_owned(n, player_id)]) == 8]
  446. while buffer:
  447. new_buffer = []
  448. for node in buffer:
  449. its += 1
  450. if its > breakdown:
  451. log("pivots: broken")
  452. return
  453. if node.pos in ignored:
  454. continue
  455. for n in self.neighbors(*node.pos):
  456. if not n in node.path and self._active_owned(n, player_id):
  457. new_node = Node(n, node.path + [node.pos])
  458. nodes.add(new_node)
  459. new_buffer.append(new_node)
  460. buffer = new_buffer
  461. paths_to = {}
  462. for node in nodes:
  463. if not node.pos in paths_to:
  464. paths_to[node.pos] = []
  465. paths_to[node.pos].append(node.path)
  466. pivots = {}
  467. for candidate in paths_to:
  468. if candidate == start:
  469. continue
  470. for p, paths in paths_to.items():
  471. if not paths or not paths[0] or p in ignored:
  472. continue
  473. if all(candidate in path for path in paths):
  474. if not candidate in pivots:
  475. pivots[candidate] = []
  476. pivots[candidate].append(p)
  477. log("player {} pivots: {}".format(player_id, {k: len(v) for k, v in pivots.items()}))
  478. for pivot, pivot_for in pivots.items():
  479. self.cells[pivot].pivot_for = pivot_for
  480. def update_pivots(self):
  481. self.update_pivot_for(ME)
  482. self.update_pivot_for(OPPONENT)
  483. def update_threats(self):
  484. # 1 + max number of units opponents can produce in one turn
  485. self.threat_level = 2 + (opponent.gold + opponent.income) // Unit.cost[1]
  486. ennemy_frontier = [c for c in self.cells.values() if c.active_opponent \
  487. and any(self.cells[n].movable and not self.cells[n].opponents for n in c.neighbors)]
  488. for cell in self.cells.values():
  489. if cell.owned:
  490. closest, dist = min([(o, Grid.manhattan(cell.pos, o.pos)) for o in ennemy_frontier], key=lambda x: x[1])
  491. if cell.unit:
  492. dist += cell.unit.level
  493. if cell.under_tower:
  494. dist += 2
  495. if dist < self.threat_level:
  496. cell.threat = self.threat_level - dist
  497. cell.threat_by = closest
  498. hqcell = grid[player.hq.pos]
  499. self.emergency = hqcell.threat > 0
  500. self.update_threat_path()
  501. # log({c.pos: (c.threat, c.threat_by) for c in self.cells.values() if c.owned and c.threat > 0})
  502. def update_threat_path(self):
  503. hqcell = grid[player.hq.pos]
  504. if not hqcell.threat > 0:
  505. return
  506. closest_threat = hqcell.threat_by
  507. # log(f"* {closest_threat.pos} threats HQ")
  508. path = self.path(closest_threat.pos, player.hq.pos)
  509. if path:
  510. # log(f"* Path to HQ: {path}")
  511. extended_path = set()
  512. for p in path:
  513. extended_path |= set(self.neighbors(*p))
  514. extended_path -= set(path)
  515. for p in path:
  516. if self._active_owned(p, ME):
  517. self.cells[p].critical = True
  518. for p in extended_path:
  519. if self._active_owned(p, ME):
  520. self.cells[p].danger = True
  521. def path(self, start, target):
  522. nodes = Queue()
  523. its, break_on = 0, 2000
  524. origin = PathNode(*start)
  525. nodes.put(origin, 0)
  526. neighbors = []
  527. while nodes:
  528. current = nodes.get()
  529. if current == target:
  530. path = []
  531. previous = current
  532. while previous:
  533. if previous != start:
  534. path.insert(0, previous)
  535. previous = previous.parent
  536. return path
  537. neighbors = self.neighbors(*current)
  538. for x, y in neighbors:
  539. its += 1
  540. if its > break_on:
  541. log("<!> pathfinding broken")
  542. return None
  543. if (x, y) == current.parent:
  544. continue
  545. cell = self.cells[(x, y)]
  546. if not cell.movable:
  547. continue
  548. moving_cost = 1
  549. if cell.unit and cell.unit.owned:
  550. moving_cost += cell.unit.level
  551. if cell.under_tower:
  552. moving_cost += 2
  553. cost = current.cost + moving_cost
  554. priority = cost + 10 * Grid.manhattan((x, y), target)
  555. node = PathNode(x, y, current)
  556. node.cost = cost
  557. nodes.put(node, priority)
  558. return None
  559. def influence_zone(self, player_id):
  560. owned, neighbors = {p for p, c in self.cells.items() if c.owner == player_id and c.active}, set()
  561. for p in owned:
  562. neighbors |= set(self.neighbors(*p))
  563. return (self.cells[p] for p in (owned | neighbors) if self.cells[p].movable)
  564. def training_places(self):
  565. return (c for c in self.influence_zone(ME) if self.can_move(c.pos))
  566. def get_next_training(self, max_level=3):
  567. q = InterestQueue()
  568. for cell in self.training_places():
  569. q.put(Position(cell))
  570. if not q:
  571. return None
  572. action = q.get()
  573. if max_level < 3:
  574. while action.cell.under_tower:
  575. try:
  576. action = q.get()
  577. except IndexError:
  578. return None
  579. level = 1
  580. for ennemy in opponent.units:
  581. if Grid.manhattan(action.cell.pos, ennemy.pos) < 3:
  582. level = min(ennemy.level + 1, max_level)
  583. break
  584. action.level = level
  585. return action
  586. def can_move(self, pos, level=1):
  587. cell = self.cells[pos]
  588. can_move = True
  589. can_move &= cell.movable
  590. can_move &= not cell.owned_unit()
  591. can_move &= not cell.owned_building()
  592. if level != 3:
  593. can_move &= (cell.unit is None or cell.unit.level < level)
  594. can_move &= cell.owned or not cell.under_tower
  595. return can_move
  596. def moving_zone(self, unit):
  597. return (self.cells[p] for p in self.cells[unit.pos].neighbors
  598. if self.can_move(p, unit.level))
  599. def get_next_move(self, unit):
  600. q = InterestQueue()
  601. for cell in self.moving_zone(unit):
  602. o = Position(cell)
  603. o.eval()
  604. q.put(o)
  605. if not q:
  606. return None
  607. objective = q.get()
  608. return objective
  609. def building_zone(self, type_):
  610. if type_ == Building.MINE:
  611. return [cell for cell in self.cells.values() if cell.mine_site and cell.depth > 3]
  612. else:
  613. return []
  614. def get_building_site(self, type_):
  615. q = InterestQueue()
  616. for cell in self.building_zone(type_):
  617. q.put(MinePosition(cell))
  618. if not q:
  619. return None
  620. return q.get()
  621. def remove_unit_from(self, cell):
  622. opponent.units.remove(cell.unit)
  623. self.units.remove(cell.unit)
  624. cell.unit = None
  625. def cost_for_new_mine(self):
  626. return Building.cost[Building.MINE] + 4 * len([b for b in player.buildings if b.type_ == Building.MINE])
  627. def apply(self, action):
  628. if type(action) is Move:
  629. old_cell, new_cell = self.cells[action.unit.pos], action.cell
  630. if new_cell.unit:
  631. if new_cell.unit.owned:
  632. log("ERROR: impossible move (owned unit here)")
  633. return
  634. if action.unit.level < 3 and new_cell.unit.level >= action.unit.level:
  635. log("ERROR: impossible move (unit here, level too low)")
  636. return
  637. # cell is occupied by an opponent's unit with an inferior level
  638. self.remove_unit_from(new_cell)
  639. if new_cell.building and new_cell.building.type_ == Building.TOWER:
  640. if new_cell.owned:
  641. log("ERROR: impossible move (allied tower here)")
  642. if action.unit.level < 3:
  643. log("ERROR: impossible move (tower here, level too low)")
  644. opponent.buildings.remove(new_cell.building)
  645. self.buildings.remove(new_cell.building)
  646. new_cell.building = None
  647. old_cell.unit = None
  648. action.unit.x, action.unit.y = new_cell.pos
  649. new_cell.unit = action.unit
  650. action.unit.has_moved = True
  651. if not new_cell.owned:
  652. if new_cell.opponents:
  653. for p in new_cell.pivot_for:
  654. self.cells[p].desactivate()
  655. if self.cells[p].unit and self.cells[p].unit.opponents:
  656. self.remove_unit_from(self.cells[p])
  657. new_cell.take_possession()
  658. self.update_state()
  659. elif type(action) is Train:
  660. new_cell = action.cell
  661. unit = Unit(ME, None, action.level, *new_cell.pos)
  662. unit.has_moved = True
  663. player.spent += Unit.cost[action.level]
  664. player.new_charges += Unit.maintenance[action.level]
  665. if new_cell.unit:
  666. if new_cell.unit.owned:
  667. log("ERROR: impossible training")
  668. return
  669. if unit.level < 3 and new_cell.unit.level >= unit.level:
  670. log("ERROR: impossible training")
  671. return
  672. # cell is occupied by an opponent's unit with an inferior level
  673. self.remove_unit_from(new_cell)
  674. if new_cell.building and new_cell.building.opponents and new_cell.building.type_ == Building.TOWER:
  675. if unit.level < 3:
  676. log("ERROR: impossible training")
  677. opponent.buildings.remove(new_cell.building)
  678. self.buildings.remove(new_cell.building)
  679. new_cell.building = None
  680. new_cell.unit = unit
  681. if not new_cell.owned:
  682. if new_cell.opponents:
  683. for p in new_cell.pivot_for:
  684. self.cells[p].desactivate()
  685. if self.cells[p].unit and self.cells[p].unit.opponents:
  686. self.remove_unit_from(self.cells[p])
  687. new_cell.take_possession()
  688. self.update_state()
  689. elif type(action) is BuildTower:
  690. new_cell = action.cell
  691. building = Building(ME, Building.TOWER, *new_cell.pos)
  692. new_cell.building = building
  693. player.buildings.append(building)
  694. player.spent += Building.cost[Building.TOWER]
  695. if building.type_ == Building.TOWER:
  696. new_cell.under_tower = True
  697. for n in new_cell.neighbors:
  698. self.cells[n].under_tower = True
  699. self.update_state()
  700. if self.emergency:
  701. self.update_threat_path()
  702. elif type(action) is BuildMine:
  703. player.spent += self.cost_for_new_mine()
  704. new_cell = action.cell
  705. building = Building(ME, Building.MINE, *new_cell.pos)
  706. new_cell.building = building
  707. player.buildings.append(building)
  708. class Action(Base):
  709. def command(self):
  710. raise NotImplementedError()
  711. class Wait(Action):
  712. def command(self):
  713. return "WAIT"
  714. class Move(Action):
  715. def __init__(self, unit, cell):
  716. self.unit = unit
  717. self.cell = cell
  718. def __repr__(self):
  719. return f"<{self.__class__.__name__}: {self.unit.id_} {self.cell.pos}>"
  720. def command(self):
  721. return f"MOVE {self.unit.id_} {self.cell.x} {self.cell.y}"
  722. class Train(Action):
  723. def __init__(self, level, cell):
  724. self.level = level
  725. self.cell = cell
  726. def __repr__(self):
  727. return f"<{self.__class__.__name__}: {self.level} {self.cell.pos}>"
  728. def command(self):
  729. return f"TRAIN {self.level} {self.cell.x} {self.cell.y}"
  730. class Build(Action):
  731. str_types = {1: "MINE", 2: "TOWER"}
  732. def __init__(self, type_, cell):
  733. self.type_ = type_
  734. self.cell = cell
  735. def __repr__(self):
  736. return f"<{self.__class__.__name__}: {self.str_types[self.type_]} {self.cell.pos}>"
  737. def command(self):
  738. return f"BUILD {self.str_types[self.type_]} {self.cell.x} {self.cell.y}"
  739. class BuildTower(Build):
  740. def __init__(self, cell):
  741. super().__init__(Building.TOWER, cell)
  742. class BuildMine(Build):
  743. def __init__(self, cell):
  744. super().__init__(Building.MINE, cell)
  745. # ******** MAIN *************
  746. test = False
  747. if test:
  748. mines_input = []
  749. else:
  750. mines_input = [input() for _ in range(int(input()))]
  751. mines_sites = [MineSite(*[int(j) for j in item.split()]) for item in mines_input]
  752. # log(f"* mines: {mines_sites}")
  753. grid = Grid(mines_sites)
  754. player = Player(ME)
  755. opponent = Player(OPPONENT)
  756. def cmd_wait():
  757. return "WAIT"
  758. def get_input():
  759. if test:
  760. gold, income = 20, 1
  761. opponent_gold, opponent_income = 20, 1
  762. new_grid_input = ['O..#########', '...###...###', '...###....##', '#..##.....##', '...#......##', '#.........##', '##.........#', '##......#...', '##.....##..#', '##....###...', '###...###...', '#########..X']
  763. buildings_input = ['0 0 0 0', '1 0 11 11']
  764. units_input = []
  765. else:
  766. gold, income = int(input()), int(input())
  767. opponent_gold, opponent_income = int(input()), int(input())
  768. new_grid_input = [input() for _ in range(12)]
  769. buildings_input = [input() for _ in range(int(input()))]
  770. units_input = [input() for _ in range(int(input()))]
  771. # log(gold, income, opponent_gold, opponent_income)
  772. # log(new_grid_input)
  773. # log(buildings_input)
  774. # log(units_input)
  775. return gold, income, opponent_gold, opponent_income, new_grid_input, buildings_input, units_input
  776. while True:
  777. # <--- get and parse input
  778. gold, income, opponent_gold, opponent_income, new_grid_input, buildings_input, units_input = get_input()
  779. new_grid = [list(row) for row in new_grid_input]
  780. buildings = [Building(*[int(j) for j in item.split()]) for item in buildings_input]
  781. # log(f"* buildings: {buildings}")
  782. units = [Unit(*[int(j) for j in item.split()]) for item in units_input]
  783. # log(f"* units: {units}")
  784. # --->
  785. log("## Start ##")
  786. log("* Update")
  787. # <--- update
  788. player.update(gold, income, units, buildings)
  789. # log(f"player: {player}")
  790. opponent.update(opponent_gold, opponent_income, units, buildings)
  791. # log(f"opponent: {opponent}")
  792. grid.update(new_grid, buildings, units)
  793. # log(f"grid:\n{grid.print_grid()}")
  794. # --->
  795. commands = []
  796. # start
  797. log(f"Threat level: {grid.threat_level}")
  798. if grid.emergency:
  799. log("<!> HQ is threaten!")
  800. todo = []
  801. abandonned = []
  802. acted = True
  803. defs, max_def = 0, 8
  804. while player.can_act():
  805. if acted:
  806. # only re-eval if an action occured
  807. q = InterestQueue()
  808. for cell in grid.influence_zone(ME):
  809. if cell.movable and not cell in abandonned:
  810. if cell.owned and cell.threat > 0 and cell.pos != player.hq.pos:
  811. if defs > max_def:
  812. continue
  813. p = Defend(cell, grid.emergency)
  814. elif not cell.owned:
  815. p = Attack(cell)
  816. else:
  817. continue
  818. p.eval()
  819. q.put(p)
  820. if not q:
  821. break
  822. acted = False
  823. objective = q.get()
  824. if type(objective) is Defend:
  825. defs += 1
  826. if player.current_gold > Building.cost[Building.TOWER] \
  827. and not objective.cell.mine_site \
  828. and not objective.cell.building:
  829. action = BuildTower(objective.cell)
  830. elif objective.cell.unit and objective.cell.unit.owned:
  831. # already a unit here: stay on place
  832. objective.cell.unit.has_moved = True
  833. continue
  834. elif player.can_afford(1):
  835. action = Train(1, objective.cell)
  836. else:
  837. near_unit = next((grid[n].unit for n in objective.cell.neighbors
  838. if grid[n].unit
  839. and grid[n].unit.owned
  840. and grid[n].unit.level == 1
  841. and not grid[n].unit.has_moved),
  842. None)
  843. if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
  844. action = Move(near_unit, objective.cell)
  845. else:
  846. abandonned.append(objective.cell)
  847. continue
  848. elif type(objective) is Attack:
  849. near_units = [grid[n].unit for n in objective.cell.neighbors if grid[n].unit \
  850. and grid[n].unit.owned \
  851. and not grid[n].unit.has_moved \
  852. and grid[n].unit.level >= objective.min_level]
  853. if len(near_units) == 1:
  854. near_unit = near_units[0]
  855. elif len(near_units) > 1:
  856. near_unit = min(near_units, key=lambda u: len([u.pos == o.cell.pos for o in q.items]))
  857. else:
  858. near_unit = None
  859. if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
  860. action = Move(near_unit, objective.cell)
  861. else:
  862. if objective.min_level > player.max_affordable():
  863. abandonned.append(objective.cell)
  864. continue
  865. action = Train(objective.min_level, objective.cell)
  866. log(f"priority: {objective}")
  867. # a unit occupy the cell
  868. already_there = action.cell.unit
  869. if already_there and already_there.owned:
  870. if already_there.has_moved:
  871. log("cell is occupied: abandon")
  872. abandonned.append(objective.cell)
  873. continue
  874. log(f"* needs to evacuate unit {already_there.id_} before")
  875. evacuate = grid.get_next_move(already_there)
  876. if evacuate:
  877. evac_action = Move(already_there, evacuate.cell)
  878. log(f"forced: {evac_action}")
  879. grid.apply(evac_action)
  880. todo.append(evac_action)
  881. else:
  882. log(f"<!> No move available for unit {already_there.id_}")
  883. abandonned.append(objective.cell)
  884. continue
  885. acted = True
  886. log(f"> Apply action {action}")
  887. grid.apply(action)
  888. todo.append(action)
  889. # units which did not move
  890. for unit in player.units:
  891. if not unit.has_moved:
  892. objective = grid.get_next_move(unit)
  893. if objective:
  894. action = Move(unit, objective.cell)
  895. log(f"default: {action}")
  896. grid.apply(action)
  897. todo.append(action)
  898. else:
  899. log(f"<!> No move available for unit {unit.id_}")
  900. unit.has_moved = True
  901. # can build mines?
  902. if player.current_gold > grid.cost_for_new_mine():
  903. site = grid.get_building_site(Building.MINE)
  904. if site:
  905. action = BuildMine(site.cell)
  906. if action:
  907. log(f"default: {action}")
  908. grid.apply(action)
  909. todo.append(action)
  910. log(f"* todo: {todo}")
  911. commands = [action.command() for action in todo]
  912. if not commands:
  913. log("nothing to do: wait")
  914. commands = ["WAIT"]
  915. log(f"* commands: {commands}")
  916. print(";".join(commands))