script.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293
  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. # * take units and towers in account when computing the threat
  10. # ? when priority needs a lvl3 unit, find a way to spare
  11. # * resurrect the strategic value: number of cells depending of the cell
  12. # * consider defending also cells not owned
  13. # * make a first turn of moves for units deep inside the territory, to avoid unecessary training and computing
  14. # x reduce interest of defend a pivot it has no movable cell around
  15. # ! ensure lvl3 units are working!
  16. debug = True
  17. t0 = time.time()
  18. def log(*msg):
  19. if debug:
  20. print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr)
  21. # OWNER
  22. ME = 0
  23. OPPONENT = 1
  24. # BUILDING TYPE
  25. HQ = 0
  26. class Base():
  27. def __repr__(self):
  28. return f"<{self.__class__.__name__}: {self.__dict__}>"
  29. class Queue(Base):
  30. def __init__(self):
  31. self.items = []
  32. def __bool__(self):
  33. return bool(self.items)
  34. def __repr__(self):
  35. return str(self.items)
  36. def values(self):
  37. return (v for _, v in self.items)
  38. def put(self, item, priority):
  39. heapq.heappush(self.items, (priority, item))
  40. def fput(self, item, priority):
  41. while priority in [p for p, _ in self.items]:
  42. priority += 1
  43. self.put(item, priority)
  44. def get(self):
  45. return heapq.heappop(self.items)[1]
  46. class InterestQueue(Queue):
  47. def __add__(self, other):
  48. self.items += other.items
  49. return self
  50. def put(self, item):
  51. heapq.heappush(self.items, item)
  52. def get(self):
  53. return heapq.heappop(self.items)
  54. class BasePosition(Base):
  55. def __init__(self, cell, *args, **kwargs):
  56. self.interest = 0
  57. self.cell = cell
  58. @property
  59. def x(self):
  60. return self.cell.x
  61. @property
  62. def y(self):
  63. return self.cell.y
  64. @property
  65. def pos(self):
  66. return self.cell.pos
  67. def __lt__(self, other):
  68. return self.interest < other.interest
  69. def eval(self):
  70. raise NotImplementedError
  71. class Position(BasePosition):
  72. def __init__(self, cell):
  73. super().__init__(cell)
  74. self.possession = 0
  75. self.threat = 0
  76. self.pivot = 0
  77. self.union = 0
  78. self.depth = 0
  79. self.hq = 0
  80. self.tower = 0
  81. self.mine = 0
  82. self.dist_to_goal = 0
  83. self.min_level = 1
  84. def pre_eval(self):
  85. # *** Eval interest: the lower the better
  86. self.interest = 0
  87. self.min_level = 1
  88. # eval possession
  89. if self.cell.active_owned:
  90. self.possession = 1
  91. elif self.cell.active_opponent:
  92. self.possession = -1
  93. # eval threat
  94. self.threat = 0
  95. if self.cell.active_owned:
  96. self.threat = self.cell.threat
  97. self.covers = sum([grid[n].owned for n in self.cell.neighbors])
  98. # eval pivot
  99. self.pivot = self.cell.pivot_value
  100. self.next_to_pivot = any(grid[n].pivot_value for n in self.cell.neighbors)
  101. # cell is not easily reachable
  102. self.protected = len([n for n in grid.neighbors(*self.cell.pos, True) if not grid[n].movable]) == 6
  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. # distance to the center of the map
  108. self.dist_to_center = Grid.manhattan(self.cell.pos, (6,6))
  109. # priorize adjacent cells
  110. self.union = len([n for n in self.cell.neighbors if grid[n].active_owned])
  111. # favorize dispersion
  112. self.concentration = len([n for n in self.cell.neighbors if grid[n].unit and grid[n].unit.owned])
  113. self.deadends = len([n for n in self.cell.neighbors if not grid[n].movable])
  114. # include 'depthmap'
  115. self.depth = self.cell.depth
  116. self.under_tower = self.cell.under_tower
  117. self.overlaps_tower = sum([grid[n].under_tower for n in self.cell.neighbors])
  118. # priorize mines or HQ
  119. if self.cell.building:
  120. self.hq = int(self.cell.building.type_ == Building.HQ)
  121. self.tower = int(self.cell.building.type_ == Building.TOWER)
  122. self.mine = int(self.cell.building.type_ == Building.MINE)
  123. # *** Min level to go there
  124. if self.cell.unit and self.cell.unit.opponents:
  125. self.min_level = min([self.cell.unit.level + 1, 3])
  126. if self.cell.under_tower:
  127. self.min_level = 3
  128. def eval(self):
  129. self.pre_eval()
  130. self.interest = 3 * self.depth + self.dist_to_goal + 2 * self.concentration + 2 * self.deadends
  131. def __repr__(self):
  132. detail = [self.possession, self.threat, self.pivot, self.dist_to_goal,
  133. self.union, self.concentration, self.depth, self.hq, self.tower, self.mine]
  134. return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
  135. class Defend(Position):
  136. def __init__(self, cell):
  137. super().__init__(cell)
  138. def __repr__(self):
  139. detail = [self.threat, self.covers, self.pivot,
  140. self.dist_to_hq, self.cell.danger, self.cell.critical,
  141. self.under_tower, self.overlaps_tower]
  142. return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
  143. def eval(self):
  144. self.pre_eval()
  145. self.interest = 20 \
  146. - 2 * self.covers \
  147. - (8 * self.pivot if not self.protected else 4 * self.pivot) \
  148. - 80 * self.cell.critical \
  149. - 25 * self.cell.danger \
  150. + 25 * self.cell.under_tower \
  151. + 10 * self.overlaps_tower \
  152. + self.dist_to_hq
  153. class Secure(Position):
  154. def __init__(self, cell, emergency = False):
  155. super().__init__(cell)
  156. self.emergency = emergency
  157. def __repr__(self):
  158. detail = [self.threat, self.covers, self.pivot,
  159. self.under_tower, self.overlaps_tower]
  160. return "<{} {}: {}, {} ({})>".format(self.__class__.__name__, self.pos, self.interest, self.min_level, detail)
  161. def eval(self):
  162. self.pre_eval()
  163. self.interest = 30 \
  164. - 2 * self.threat \
  165. - 2 * self.covers \
  166. - (8 * self.pivot if not self.protected else 2 * self.pivot) \
  167. + 25 * self.cell.under_tower \
  168. + 10 * self.overlaps_tower \
  169. + self.dist_to_center
  170. class Attack(Position):
  171. def eval(self):
  172. self.pre_eval()
  173. self.interest = 15 * self.possession \
  174. - 5 * self.pivot \
  175. - 3 * self.next_to_pivot \
  176. - 2 * self.union \
  177. + 4 * self.dist_to_goal \
  178. - 25 * self.tower \
  179. - 15 * self.mine \
  180. - 100 * self.hq \
  181. - 50 * self.cell.critical \
  182. - 20 * self.cell.danger \
  183. + 10 * (self.min_level - 1) \
  184. - 15 * (self.cell.pos in sum(grid.divide, []))
  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. + 3 * self.concentration \
  192. + 2 * self.deadends \
  193. + 4 * self.depth \
  194. + 2 * self.dist_to_goal \
  195. - max([0, 3 - self.dist_to_hq]) \
  196. + 2 * 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. self.update_divide_and_conquer()
  417. def update_state(self):
  418. self.update_tower_areas()
  419. self.update_frontlines()
  420. self.update_depth_map()
  421. @staticmethod
  422. def manhattan(from_, to_):
  423. xa, ya = from_
  424. xb, yb = to_
  425. return abs(xa - xb) + abs(ya - yb)
  426. def neighbors(self, x, y, diags=False):
  427. neighs = [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
  428. if diags:
  429. neighs += [(x - 1, y - 1), (x + 1, y - 1), (x - 1, y + 1), (x + 1, y + 1)]
  430. return [(x, y) for x, y in neighs if 0 <= x < Grid.dim and 0 <= y < Grid.dim]
  431. @classmethod
  432. def zone(cls, center, radius):
  433. return [(x, y) for x in range(0, cls.dim) for y in range(0, cls.dim) if cls.manhattan(center, (x, y)) <= radius]
  434. def zone_set(self, center, radius):
  435. zone = set(center)
  436. for _ in range(radius):
  437. for p in zone:
  438. zone |= self.neighbors(*p)
  439. @staticmethod
  440. def closest(from_, in_):
  441. return min(in_, key=lambda x: Grid.manhattan(from_, x))
  442. def get_hq(self, player_id):
  443. return next((b for b in self.buildings if b.owner == player_id and b.hq))
  444. def update_tower_areas(self):
  445. for b in self.buildings:
  446. if b.type_ == Building.TOWER:
  447. self.cells[b.pos].under_tower = True
  448. for n in self.cells[b.pos].neighbors:
  449. self.cells[n].under_tower = True
  450. def update_frontlines(self):
  451. # update the current frontlines
  452. self.frontline = []
  453. self.frontex = []
  454. for cell in self.cells.values():
  455. if cell.active_owned:
  456. if any(self.cells[c].movable and not self.cells[c].active_owned
  457. for c in cell.neighbors):
  458. self.frontline.append(cell)
  459. def update_depth_map(self):
  460. buffer = [c.pos for c in self.frontline]
  461. for p in buffer:
  462. self.cells[p].depth = 1
  463. next_buffer = []
  464. while buffer:
  465. for p in buffer:
  466. for n in self.cells[p].neighbors:
  467. if self.cells[n].active_owned and not self.cells[n].depth:
  468. self.cells[n].depth = self.cells[p].depth + 1
  469. next_buffer.append(n)
  470. buffer = list(next_buffer)
  471. next_buffer = []
  472. def _active_owned(self, pos, player_id):
  473. try:
  474. return self.cells[pos].is_active_owner(player_id)
  475. except KeyError:
  476. return False
  477. def update_propagation(self, player_id):
  478. start = self.get_hq(player_id).pos
  479. lvl = 0
  480. propagation = {start: (lvl, [])}
  481. pivots_cells = []
  482. for x, y in self.cells:
  483. if (x, y) != start and self._active_owned((x, y), player_id):
  484. around = [(x, y - 1), (x + 1, y - 1), (x + 1, y), (x + 1, y + 1),
  485. (x, y + 1), (x - 1, y + 1), (x - 1, y), (x - 1, y - 1)]
  486. owned = [self._active_owned(p, player_id) for p in around]
  487. changes = [x for x in zip(owned, owned[1:]) if x == (True, False)]
  488. if len(changes) > 1:
  489. pivots_cells.append((x, y))
  490. pivots = {p: [] for p in pivots_cells}
  491. buffer = [start]
  492. while buffer:
  493. new_buffer = []
  494. lvl += 1
  495. for pos in buffer:
  496. for n in self.neighbors(*pos):
  497. if self._active_owned(n, player_id):
  498. if not n in propagation:
  499. propagation[n] = (lvl, [pos])
  500. new_buffer.append(n)
  501. else:
  502. # already visited
  503. if propagation[pos][1] != [n] and propagation[n][0] >= propagation[pos][0]:
  504. propagation[n][1].append(pos)
  505. buffer = new_buffer
  506. self.propagation = propagation
  507. children = {}
  508. for p, data in self.propagation.items():
  509. _, parents = data
  510. for parent in parents:
  511. if not parent in children:
  512. children[parent] = []
  513. children[parent].append(p)
  514. for pivot in pivots:
  515. buffer = set(children.get(pivot, []))
  516. while buffer:
  517. new_buffer = set()
  518. for child in buffer:
  519. new_buffer |= set(children.get(child, []))
  520. pivots[pivot] += list(buffer)
  521. buffer = new_buffer
  522. # cleaning 'false children'
  523. for pivot, children in pivots.items():
  524. invalid = []
  525. for child in children:
  526. parents = self.propagation[child][1]
  527. if any((p != pivot and p not in children) or p in invalid for p in parents):
  528. invalid.append(child)
  529. for p in invalid:
  530. children.remove(p)
  531. log("player {}, pivots: {}".format(player_id, {k: len(v) for k, v in pivots.items()}))
  532. for pivot, pivot_for in pivots.items():
  533. self.cells[pivot].pivot_for = pivot_for
  534. self.cells[pivot].pivot_value = sum([1 + grid[p].get_unit_level() for p in pivot_for])
  535. def update_divide_and_conquer(self):
  536. segs = []
  537. for c in self.frontline:
  538. for n in c.neighbors:
  539. if n in self.cells and self.cells[n].active_opponent:
  540. seg = []
  541. x, y = c.pos
  542. xn, yn = n
  543. if xn > x:
  544. dx = 1
  545. elif xn < x:
  546. dx = -1
  547. else:
  548. dx = 0
  549. if yn > y:
  550. dy = 1
  551. elif yn < y:
  552. dy = -1
  553. else:
  554. dy = 0
  555. x, y = x + dx, y + dy
  556. while (x, y) in self.cells and self.cells[(x, y)].active_opponent:
  557. seg.append((x, y))
  558. x, y = x + dx, y + dy
  559. if seg:
  560. segs.append(seg)
  561. self.divide = [s for s in segs if len(s) <= 4]
  562. log(self.divide)
  563. def update_threats(self):
  564. # 1 + max number of units opponents can produce in one turn
  565. self.threat_level = 2 + (opponent.gold + opponent.income) // Unit.cost[1]
  566. ennemy_frontier = [c for c in self.cells.values() if c.active_opponent \
  567. and any(self.cells[n].movable and not self.cells[n].opponents for n in c.neighbors)]
  568. for cell in self.cells.values():
  569. if cell.owned:
  570. closest, dist = min([(o, Grid.manhattan(cell.pos, o.pos)) for o in ennemy_frontier], key=lambda x: x[1])
  571. if cell.unit:
  572. dist += cell.unit.level
  573. if cell.under_tower:
  574. dist += 2
  575. if dist < self.threat_level:
  576. cell.threat = self.threat_level - dist
  577. cell.threat_by = closest
  578. hqcell = grid[player.hq.pos]
  579. self.emergency = hqcell.threat > 0
  580. self.update_threat_path()
  581. def update_threat_path(self):
  582. hqcell = grid[player.hq.pos]
  583. if not hqcell.threat > 0:
  584. return
  585. closest_threat = hqcell.threat_by
  586. log(f"* {closest_threat.pos} threats HQ")
  587. path = self.path(closest_threat.pos, player.hq.pos)
  588. if path:
  589. log(f"* Path to HQ: {path}")
  590. extended_path = set()
  591. for p in path:
  592. extended_path |= set(self.neighbors(*p))
  593. extended_path -= set(path)
  594. for p in path:
  595. self.cells[p].critical = True
  596. for p in extended_path:
  597. self.cells[p].danger = True
  598. def path(self, start, target):
  599. nodes = Queue()
  600. its, break_on = 0, 2000
  601. origin = PathNode(*start)
  602. nodes.put(origin, 0)
  603. neighbors = []
  604. while nodes:
  605. current = nodes.get()
  606. if current == target:
  607. path = []
  608. previous = current
  609. while previous:
  610. if previous != start:
  611. path.insert(0, previous)
  612. previous = previous.parent
  613. return path
  614. neighbors = self.neighbors(*current)
  615. for x, y in neighbors:
  616. its += 1
  617. if its > break_on:
  618. log("<!> pathfinding broken")
  619. return None
  620. if (x, y) == current.parent:
  621. continue
  622. cell = self.cells[(x, y)]
  623. if not cell.movable:
  624. continue
  625. moving_cost = 1
  626. if cell.unit and cell.unit.owned:
  627. moving_cost += cell.unit.level
  628. if cell.under_tower:
  629. moving_cost += 2
  630. cost = current.cost + moving_cost
  631. priority = cost + 10 * Grid.manhattan((x, y), target)
  632. node = PathNode(x, y, current)
  633. node.cost = cost
  634. nodes.put(node, priority)
  635. return None
  636. def influence_zone(self, player_id):
  637. owned, neighbors = {p for p, c in self.cells.items() if c.owner == player_id and c.active}, set()
  638. for p in owned:
  639. neighbors |= set(self.neighbors(*p))
  640. return (self.cells[p] for p in (owned | neighbors) if self.cells[p].movable)
  641. def training_places(self):
  642. return (c for c in self.influence_zone(ME) if self.can_move(c.pos))
  643. def get_next_training(self, max_level=3):
  644. q = InterestQueue()
  645. for cell in self.training_places():
  646. q.put(Position(cell))
  647. if not q:
  648. return None
  649. action = q.get()
  650. if max_level < 3:
  651. while action.cell.under_tower:
  652. try:
  653. action = q.get()
  654. except IndexError:
  655. return None
  656. level = 1
  657. for ennemy in opponent.units:
  658. if Grid.manhattan(action.cell.pos, ennemy.pos) < 3:
  659. level = min(ennemy.level + 1, max_level)
  660. break
  661. action.level = level
  662. return action
  663. def can_move(self, pos, level=1):
  664. cell = self.cells[pos]
  665. can_move = True
  666. can_move &= cell.movable
  667. can_move &= not cell.owned_unit()
  668. can_move &= not cell.owned_building()
  669. if level != 3:
  670. can_move &= (cell.unit is None or cell.unit.level < level)
  671. can_move &= cell.owned or not cell.under_tower
  672. return can_move
  673. def moving_zone(self, unit):
  674. return (self.cells[p] for p in self.cells[unit.pos].neighbors
  675. if self.can_move(p, unit.level))
  676. def get_next_move(self, unit):
  677. q = InterestQueue()
  678. for cell in self.moving_zone(unit):
  679. o = Position(cell)
  680. o.eval()
  681. q.put(o)
  682. if not q:
  683. return None
  684. objective = q.get()
  685. return objective
  686. def building_zone(self, type_):
  687. if type_ == Building.MINE:
  688. return [cell for cell in self.cells.values() if cell.mine_site and cell.depth > 3]
  689. else:
  690. return []
  691. def get_building_site(self, type_):
  692. q = InterestQueue()
  693. for cell in self.building_zone(type_):
  694. q.put(MinePosition(cell))
  695. if not q:
  696. return None
  697. return q.get()
  698. def remove_unit_from(self, cell):
  699. opponent.units.remove(cell.unit)
  700. self.units.remove(cell.unit)
  701. cell.unit = None
  702. def cost_for_new_mine(self):
  703. return Building.cost[Building.MINE] + 4 * len([b for b in player.buildings if b.type_ == Building.MINE])
  704. def apply(self, action):
  705. if type(action) is Move:
  706. old_cell, new_cell = self.cells[action.unit.pos], action.cell
  707. if new_cell.unit:
  708. if new_cell.unit.owned:
  709. log("ERROR: impossible move (owned unit here)")
  710. return
  711. if action.unit.level < 3 and new_cell.unit.level >= action.unit.level:
  712. log("ERROR: impossible move (unit here, level too low)")
  713. return
  714. # cell is occupied by an opponent's unit with an inferior level
  715. self.remove_unit_from(new_cell)
  716. if new_cell.building and new_cell.building.type_ == Building.TOWER:
  717. if new_cell.owned:
  718. log("ERROR: impossible move (allied tower here)")
  719. if action.unit.level < 3:
  720. log("ERROR: impossible move (tower here, level too low)")
  721. opponent.buildings.remove(new_cell.building)
  722. self.buildings.remove(new_cell.building)
  723. new_cell.building = None
  724. old_cell.unit = None
  725. action.unit.x, action.unit.y = new_cell.pos
  726. new_cell.unit = action.unit
  727. action.unit.has_moved = True
  728. if not new_cell.owned:
  729. if new_cell.opponents:
  730. for p in new_cell.pivot_for:
  731. self.cells[p].desactivate()
  732. if self.cells[p].unit and self.cells[p].unit.opponents:
  733. self.remove_unit_from(self.cells[p])
  734. new_cell.take_possession()
  735. self.update_state()
  736. elif type(action) is Train:
  737. new_cell = action.cell
  738. unit = Unit(ME, None, action.level, *new_cell.pos)
  739. unit.has_moved = True
  740. player.spent += Unit.cost[action.level]
  741. player.new_charges += Unit.maintenance[action.level]
  742. if new_cell.unit:
  743. if new_cell.unit.owned:
  744. log("ERROR: impossible training")
  745. return
  746. if unit.level < 3 and new_cell.unit.level >= unit.level:
  747. log("ERROR: impossible training")
  748. return
  749. # cell is occupied by an opponent's unit with an inferior level
  750. self.remove_unit_from(new_cell)
  751. if new_cell.building and new_cell.building.opponents and new_cell.building.type_ == Building.TOWER:
  752. if unit.level < 3:
  753. log("ERROR: impossible training")
  754. opponent.buildings.remove(new_cell.building)
  755. self.buildings.remove(new_cell.building)
  756. new_cell.building = None
  757. new_cell.unit = unit
  758. if not new_cell.owned:
  759. if new_cell.opponents:
  760. for p in new_cell.pivot_for:
  761. self.cells[p].desactivate()
  762. if self.cells[p].unit and self.cells[p].unit.opponents:
  763. self.remove_unit_from(self.cells[p])
  764. new_cell.take_possession()
  765. self.update_state()
  766. elif type(action) is BuildTower:
  767. new_cell = action.cell
  768. building = Building(ME, Building.TOWER, *new_cell.pos)
  769. new_cell.building = building
  770. player.buildings.append(building)
  771. player.spent += Building.cost[Building.TOWER]
  772. if building.type_ == Building.TOWER:
  773. new_cell.under_tower = True
  774. for n in new_cell.neighbors:
  775. self.cells[n].under_tower = True
  776. self.update_state()
  777. if self.emergency:
  778. self.update_threat_path()
  779. elif type(action) is BuildMine:
  780. player.spent += self.cost_for_new_mine()
  781. new_cell = action.cell
  782. building = Building(ME, Building.MINE, *new_cell.pos)
  783. new_cell.building = building
  784. player.buildings.append(building)
  785. class Action(Base):
  786. def command(self):
  787. raise NotImplementedError()
  788. class Wait(Action):
  789. def command(self):
  790. return "WAIT"
  791. class Move(Action):
  792. def __init__(self, unit, cell):
  793. self.unit = unit
  794. self.cell = cell
  795. def __repr__(self):
  796. return f"<{self.__class__.__name__}: {self.unit.id_} {self.cell.pos}>"
  797. def command(self):
  798. return f"MOVE {self.unit.id_} {self.cell.x} {self.cell.y}"
  799. class Train(Action):
  800. def __init__(self, level, cell):
  801. self.level = level
  802. self.cell = cell
  803. def __repr__(self):
  804. return f"<{self.__class__.__name__}: {self.level} {self.cell.pos}>"
  805. def command(self):
  806. return f"TRAIN {self.level} {self.cell.x} {self.cell.y}"
  807. class Build(Action):
  808. str_types = {1: "MINE", 2: "TOWER"}
  809. def __init__(self, type_, cell):
  810. self.type_ = type_
  811. self.cell = cell
  812. def __repr__(self):
  813. return f"<{self.__class__.__name__}: {self.str_types[self.type_]} {self.cell.pos}>"
  814. def command(self):
  815. return f"BUILD {self.str_types[self.type_]} {self.cell.x} {self.cell.y}"
  816. class BuildTower(Build):
  817. def __init__(self, cell):
  818. super().__init__(Building.TOWER, cell)
  819. class BuildMine(Build):
  820. def __init__(self, cell):
  821. super().__init__(Building.MINE, cell)
  822. # ******** MAIN *************
  823. test = False
  824. if test:
  825. mines_input = []
  826. else:
  827. mines_input = [input() for _ in range(int(input()))]
  828. mines_sites = [MineSite(*[int(j) for j in item.split()]) for item in mines_input]
  829. # log(f"* mines: {mines_sites}")
  830. grid = Grid(mines_sites)
  831. player = Player(ME)
  832. opponent = Player(OPPONENT)
  833. def cmd_wait():
  834. return "WAIT"
  835. def get_input():
  836. if test:
  837. gold, income = 20, 1
  838. opponent_gold, opponent_income = 20, 1
  839. new_grid_input = ['O..#########', '...###...###', '...###....##', '#..##.....##', '...#......##', '#.........##', '##.........#', '##......#...', '##.....##..#', '##....###...', '###...###...', '#########..X']
  840. buildings_input = ['0 0 0 0', '1 0 11 11']
  841. units_input = []
  842. else:
  843. gold, income = int(input()), int(input())
  844. opponent_gold, opponent_income = int(input()), int(input())
  845. new_grid_input = [input() for _ in range(12)]
  846. buildings_input = [input() for _ in range(int(input()))]
  847. units_input = [input() for _ in range(int(input()))]
  848. # log(gold, income, opponent_gold, opponent_income)
  849. # log(new_grid_input)
  850. # log(buildings_input)
  851. # log(units_input)
  852. return gold, income, opponent_gold, opponent_income, new_grid_input, buildings_input, units_input
  853. while True:
  854. # <--- get and parse input
  855. gold, income, opponent_gold, opponent_income, new_grid_input, buildings_input, units_input = get_input()
  856. new_grid = [list(row) for row in new_grid_input]
  857. buildings = [Building(*[int(j) for j in item.split()]) for item in buildings_input]
  858. # log(f"* buildings: {buildings}")
  859. units = [Unit(*[int(j) for j in item.split()]) for item in units_input]
  860. # log(f"* units: {units}")
  861. # --->
  862. log("## Start ##")
  863. log("* Update")
  864. # <--- update
  865. player.update(gold, income, units, buildings)
  866. # log(f"player: {player}")
  867. opponent.update(opponent_gold, opponent_income, units, buildings)
  868. # log(f"opponent: {opponent}")
  869. grid.update(new_grid, buildings, units)
  870. # log(f"grid:\n{grid.print_grid()}")
  871. # --->
  872. commands = []
  873. # start
  874. log(f"Threat level: {grid.threat_level}")
  875. if grid.emergency:
  876. log("<!> HQ is threaten!")
  877. todo = []
  878. played, abandonned = [], []
  879. acted = True
  880. defs, max_def = 0, 8
  881. to_spare = len([c for c in grid.cells.values() if c.owned])
  882. while player.can_act():
  883. if acted:
  884. # only re-eval if an action occured
  885. q = InterestQueue()
  886. for cell in grid.influence_zone(ME):
  887. if cell.movable and not cell in abandonned and not cell in played:
  888. if cell.owned and cell.threat > 0 and cell.pos != player.hq.pos:
  889. if defs > max_def:
  890. continue
  891. if grid.emergency:
  892. p = Defend(cell)
  893. else:
  894. p = Secure(cell)
  895. elif cell.opponents:
  896. p = Attack(cell)
  897. elif not cell.owned:
  898. p = Colonize(cell)
  899. else:
  900. continue
  901. p.eval()
  902. q.put(p)
  903. if not q:
  904. break
  905. acted = False
  906. objective = q.get()
  907. if type(objective) is Secure:
  908. if (player.current_gold - to_spare) > Building.cost[Building.TOWER] \
  909. and not objective.cell.mine_site \
  910. and not objective.cell.building:
  911. action = BuildTower(objective.cell)
  912. defs += 1
  913. else:
  914. abandonned.append(objective.cell)
  915. continue
  916. elif type(objective) is Defend:
  917. if player.current_gold > Building.cost[Building.TOWER] \
  918. and not objective.cell.mine_site \
  919. and not objective.cell.building:
  920. action = BuildTower(objective.cell)
  921. defs += 1
  922. elif objective.cell.unit and objective.cell.unit.owned:
  923. # already a unit here: stay on place
  924. objective.cell.unit.has_moved = True
  925. continue
  926. elif player.can_afford(1):
  927. action = Train(1, objective.cell)
  928. defs += 1
  929. else:
  930. near_unit = next((grid[n].unit for n in objective.cell.neighbors
  931. if grid[n].unit
  932. and grid[n].unit.owned
  933. and grid[n].unit.level == 1
  934. and not grid[n].unit.has_moved),
  935. None)
  936. if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
  937. action = Move(near_unit, objective.cell)
  938. defs += 1
  939. else:
  940. abandonned.append(objective.cell)
  941. continue
  942. elif type(objective) in (Colonize, Attack):
  943. near_units = [grid[n].unit for n in objective.cell.neighbors if grid[n].unit \
  944. and grid[n].unit.owned \
  945. and not grid[n].unit.has_moved \
  946. and grid[n].unit.level == objective.min_level]
  947. if len(near_units) == 1:
  948. near_unit = near_units[0]
  949. elif len(near_units) > 1:
  950. near_unit = min(near_units, key=lambda u: len([u.pos == o.cell.pos for o in q.items]))
  951. else:
  952. near_unit = None
  953. if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
  954. action = Move(near_unit, objective.cell)
  955. else:
  956. if objective.min_level > player.max_affordable() or player.gold < (to_spare + Unit.cost[objective.min_level]):
  957. abandonned.append(objective.cell)
  958. continue
  959. action = Train(objective.min_level, objective.cell)
  960. log(f"priority: {objective}")
  961. # a unit occupy the cell
  962. already_there = action.cell.unit
  963. if already_there and already_there.owned:
  964. if already_there.has_moved:
  965. log("cell is occupied: abandon")
  966. abandonned.append(objective.cell)
  967. continue
  968. log(f"* needs to evacuate unit {already_there.id_} before")
  969. evacuate = grid.get_next_move(already_there)
  970. if evacuate:
  971. evac_action = Move(already_there, evacuate.cell)
  972. log(f"forced: {evac_action}")
  973. grid.apply(evac_action)
  974. todo.append(evac_action)
  975. else:
  976. log(f"<!> No move available for unit {already_there.id_}")
  977. abandonned.append(objective.cell)
  978. continue
  979. acted = True
  980. log(f"> Apply action {action}")
  981. played.append(action.cell)
  982. grid.apply(action)
  983. todo.append(action)
  984. # units which did not move
  985. for unit in player.units:
  986. if not unit.has_moved:
  987. objective = grid.get_next_move(unit)
  988. if objective:
  989. action = Move(unit, objective.cell)
  990. log(f"default: {action}")
  991. grid.apply(action)
  992. todo.append(action)
  993. else:
  994. log(f"<!> No move available for unit {unit.id_}")
  995. unit.has_moved = True
  996. # can build mines?
  997. if player.current_gold > grid.cost_for_new_mine():
  998. site = grid.get_building_site(Building.MINE)
  999. if site:
  1000. action = BuildMine(site.cell)
  1001. if action:
  1002. log(f"default: {action}")
  1003. grid.apply(action)
  1004. todo.append(action)
  1005. log(f"* todo: {todo}")
  1006. commands = [action.command() for action in todo]
  1007. if not commands:
  1008. log("nothing to do: wait")
  1009. commands = ["WAIT"]
  1010. log(f"* commands: {commands}")
  1011. print(";".join(commands))