script.py 41 KB

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