script.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317
  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_pivot_for(self, player_id):
  535. start = self.get_hq(player_id).pos
  536. start_node = Node(start)
  537. buffer = [start_node]
  538. nodes = {start_node}
  539. its, breakdown = 0, 1200
  540. ignored = [p for p, c in self.cells.items() if c.is_active_owner(player_id) and
  541. len([n for n in self.neighbors(*p, diags=True) if self._active_owned(n, player_id)]) == 8]
  542. while buffer:
  543. new_buffer = []
  544. for node in buffer:
  545. its += 1
  546. if its > breakdown:
  547. log("pivots: broken")
  548. return
  549. if node.pos in ignored:
  550. continue
  551. for n in self.neighbors(*node.pos):
  552. if not n in node.path and self._active_owned(n, player_id):
  553. new_node = Node(n, node.path + [node.pos])
  554. nodes.add(new_node)
  555. new_buffer.append(new_node)
  556. buffer = new_buffer
  557. paths_to = {}
  558. for node in nodes:
  559. if not node.pos in paths_to:
  560. paths_to[node.pos] = []
  561. paths_to[node.pos].append(node.path)
  562. pivots = {}
  563. for candidate in paths_to:
  564. if candidate == start:
  565. continue
  566. for p, paths in paths_to.items():
  567. if not paths or not paths[0] or p in ignored:
  568. continue
  569. if all(candidate in path for path in paths):
  570. if not candidate in pivots:
  571. pivots[candidate] = []
  572. pivots[candidate].append(p)
  573. log("player {} pivots: {}".format(player_id, {k: len(v) for k, v in pivots.items()}))
  574. for pivot, pivot_for in pivots.items():
  575. self.cells[pivot].pivot_for = pivot_for
  576. def update_pivots(self):
  577. self.update_pivot_for(ME)
  578. self.update_pivot_for(OPPONENT)
  579. def update_threats(self):
  580. # 1 + max number of units opponents can produce in one turn
  581. self.threat_level = 2 + (opponent.gold + opponent.income) // Unit.cost[1]
  582. ennemy_frontier = [c for c in self.cells.values() if c.active_opponent \
  583. and any(self.cells[n].movable and not self.cells[n].opponents for n in c.neighbors)]
  584. for cell in self.cells.values():
  585. if cell.owned:
  586. closest, dist = min([(o, Grid.manhattan(cell.pos, o.pos)) for o in ennemy_frontier], key=lambda x: x[1])
  587. if cell.unit:
  588. dist += cell.unit.level
  589. if cell.under_tower:
  590. dist += 2
  591. if dist < self.threat_level:
  592. cell.threat = self.threat_level - dist
  593. cell.threat_by = closest
  594. hqcell = grid[player.hq.pos]
  595. self.emergency = hqcell.threat > 0
  596. self.update_threat_path()
  597. # log({c.pos: (c.threat, c.threat_by) for c in self.cells.values() if c.owned and c.threat > 0})
  598. def update_threat_path(self):
  599. hqcell = grid[player.hq.pos]
  600. if not hqcell.threat > 0:
  601. return
  602. closest_threat = hqcell.threat_by
  603. log(f"* {closest_threat.pos} threats HQ")
  604. path = self.path(closest_threat.pos, player.hq.pos)
  605. if path:
  606. log(f"* Path to HQ: {path}")
  607. extended_path = set()
  608. for p in path:
  609. extended_path |= set(self.neighbors(*p))
  610. extended_path -= set(path)
  611. for p in path:
  612. self.cells[p].critical = True
  613. for p in extended_path:
  614. self.cells[p].danger = True
  615. def path(self, start, target):
  616. nodes = Queue()
  617. its, break_on = 0, 2000
  618. origin = PathNode(*start)
  619. nodes.put(origin, 0)
  620. neighbors = []
  621. while nodes:
  622. current = nodes.get()
  623. if current == target:
  624. path = []
  625. previous = current
  626. while previous:
  627. if previous != start:
  628. path.insert(0, previous)
  629. previous = previous.parent
  630. return path
  631. neighbors = self.neighbors(*current)
  632. for x, y in neighbors:
  633. its += 1
  634. if its > break_on:
  635. log("<!> pathfinding broken")
  636. return None
  637. if (x, y) == current.parent:
  638. continue
  639. cell = self.cells[(x, y)]
  640. if not cell.movable:
  641. continue
  642. moving_cost = 1
  643. if cell.unit and cell.unit.owned:
  644. moving_cost += cell.unit.level
  645. if cell.under_tower:
  646. moving_cost += 2
  647. cost = current.cost + moving_cost
  648. priority = cost + 10 * Grid.manhattan((x, y), target)
  649. node = PathNode(x, y, current)
  650. node.cost = cost
  651. nodes.put(node, priority)
  652. return None
  653. def influence_zone(self, player_id):
  654. owned, neighbors = {p for p, c in self.cells.items() if c.owner == player_id and c.active}, set()
  655. for p in owned:
  656. neighbors |= set(self.neighbors(*p))
  657. return (self.cells[p] for p in (owned | neighbors) if self.cells[p].movable)
  658. def training_places(self):
  659. return (c for c in self.influence_zone(ME) if self.can_move(c.pos))
  660. def get_next_training(self, max_level=3):
  661. q = InterestQueue()
  662. for cell in self.training_places():
  663. q.put(Position(cell))
  664. if not q:
  665. return None
  666. action = q.get()
  667. if max_level < 3:
  668. while action.cell.under_tower:
  669. try:
  670. action = q.get()
  671. except IndexError:
  672. return None
  673. level = 1
  674. for ennemy in opponent.units:
  675. if Grid.manhattan(action.cell.pos, ennemy.pos) < 3:
  676. level = min(ennemy.level + 1, max_level)
  677. break
  678. action.level = level
  679. return action
  680. def can_move(self, pos, level=1):
  681. cell = self.cells[pos]
  682. can_move = True
  683. can_move &= cell.movable
  684. can_move &= not cell.owned_unit()
  685. can_move &= not cell.owned_building()
  686. if level != 3:
  687. can_move &= (cell.unit is None or cell.unit.level < level)
  688. can_move &= cell.owned or not cell.under_tower
  689. return can_move
  690. def moving_zone(self, unit):
  691. return (self.cells[p] for p in self.cells[unit.pos].neighbors
  692. if self.can_move(p, unit.level))
  693. def get_next_move(self, unit):
  694. q = InterestQueue()
  695. for cell in self.moving_zone(unit):
  696. o = Position(cell)
  697. o.eval()
  698. q.put(o)
  699. if not q:
  700. return None
  701. objective = q.get()
  702. return objective
  703. def building_zone(self, type_):
  704. if type_ == Building.MINE:
  705. return [cell for cell in self.cells.values() if cell.mine_site and cell.depth > 3]
  706. else:
  707. return []
  708. def get_building_site(self, type_):
  709. q = InterestQueue()
  710. for cell in self.building_zone(type_):
  711. q.put(MinePosition(cell))
  712. if not q:
  713. return None
  714. return q.get()
  715. def remove_unit_from(self, cell):
  716. opponent.units.remove(cell.unit)
  717. self.units.remove(cell.unit)
  718. cell.unit = None
  719. def cost_for_new_mine(self):
  720. return Building.cost[Building.MINE] + 4 * len([b for b in player.buildings if b.type_ == Building.MINE])
  721. def apply(self, action):
  722. if type(action) is Move:
  723. old_cell, new_cell = self.cells[action.unit.pos], action.cell
  724. if new_cell.unit:
  725. if new_cell.unit.owned:
  726. log("ERROR: impossible move (owned unit here)")
  727. return
  728. if action.unit.level < 3 and new_cell.unit.level >= action.unit.level:
  729. log("ERROR: impossible move (unit here, level too low)")
  730. return
  731. # cell is occupied by an opponent's unit with an inferior level
  732. self.remove_unit_from(new_cell)
  733. if new_cell.building and new_cell.building.type_ == Building.TOWER:
  734. if new_cell.owned:
  735. log("ERROR: impossible move (allied tower here)")
  736. if action.unit.level < 3:
  737. log("ERROR: impossible move (tower here, level too low)")
  738. opponent.buildings.remove(new_cell.building)
  739. self.buildings.remove(new_cell.building)
  740. new_cell.building = None
  741. old_cell.unit = None
  742. action.unit.x, action.unit.y = new_cell.pos
  743. new_cell.unit = action.unit
  744. action.unit.has_moved = True
  745. if not new_cell.owned:
  746. if new_cell.opponents:
  747. for p in new_cell.pivot_for:
  748. self.cells[p].desactivate()
  749. if self.cells[p].unit and self.cells[p].unit.opponents:
  750. self.remove_unit_from(self.cells[p])
  751. new_cell.take_possession()
  752. self.update_state()
  753. elif type(action) is Train:
  754. new_cell = action.cell
  755. unit = Unit(ME, None, action.level, *new_cell.pos)
  756. unit.has_moved = True
  757. player.spent += Unit.cost[action.level]
  758. player.new_charges += Unit.maintenance[action.level]
  759. if new_cell.unit:
  760. if new_cell.unit.owned:
  761. log("ERROR: impossible training")
  762. return
  763. if unit.level < 3 and new_cell.unit.level >= unit.level:
  764. log("ERROR: impossible training")
  765. return
  766. # cell is occupied by an opponent's unit with an inferior level
  767. self.remove_unit_from(new_cell)
  768. if new_cell.building and new_cell.building.opponents and new_cell.building.type_ == Building.TOWER:
  769. if unit.level < 3:
  770. log("ERROR: impossible training")
  771. opponent.buildings.remove(new_cell.building)
  772. self.buildings.remove(new_cell.building)
  773. new_cell.building = None
  774. new_cell.unit = unit
  775. if not new_cell.owned:
  776. if new_cell.opponents:
  777. for p in new_cell.pivot_for:
  778. self.cells[p].desactivate()
  779. if self.cells[p].unit and self.cells[p].unit.opponents:
  780. self.remove_unit_from(self.cells[p])
  781. new_cell.take_possession()
  782. self.update_state()
  783. elif type(action) is BuildTower:
  784. new_cell = action.cell
  785. building = Building(ME, Building.TOWER, *new_cell.pos)
  786. new_cell.building = building
  787. player.buildings.append(building)
  788. player.spent += Building.cost[Building.TOWER]
  789. if building.type_ == Building.TOWER:
  790. new_cell.under_tower = True
  791. for n in new_cell.neighbors:
  792. self.cells[n].under_tower = True
  793. self.update_state()
  794. if self.emergency:
  795. self.update_threat_path()
  796. elif type(action) is BuildMine:
  797. player.spent += self.cost_for_new_mine()
  798. new_cell = action.cell
  799. building = Building(ME, Building.MINE, *new_cell.pos)
  800. new_cell.building = building
  801. player.buildings.append(building)
  802. class Action(Base):
  803. def command(self):
  804. raise NotImplementedError()
  805. class Wait(Action):
  806. def command(self):
  807. return "WAIT"
  808. class Move(Action):
  809. def __init__(self, unit, cell):
  810. self.unit = unit
  811. self.cell = cell
  812. def __repr__(self):
  813. return f"<{self.__class__.__name__}: {self.unit.id_} {self.cell.pos}>"
  814. def command(self):
  815. return f"MOVE {self.unit.id_} {self.cell.x} {self.cell.y}"
  816. class Train(Action):
  817. def __init__(self, level, cell):
  818. self.level = level
  819. self.cell = cell
  820. def __repr__(self):
  821. return f"<{self.__class__.__name__}: {self.level} {self.cell.pos}>"
  822. def command(self):
  823. return f"TRAIN {self.level} {self.cell.x} {self.cell.y}"
  824. class Build(Action):
  825. str_types = {1: "MINE", 2: "TOWER"}
  826. def __init__(self, type_, cell):
  827. self.type_ = type_
  828. self.cell = cell
  829. def __repr__(self):
  830. return f"<{self.__class__.__name__}: {self.str_types[self.type_]} {self.cell.pos}>"
  831. def command(self):
  832. return f"BUILD {self.str_types[self.type_]} {self.cell.x} {self.cell.y}"
  833. class BuildTower(Build):
  834. def __init__(self, cell):
  835. super().__init__(Building.TOWER, cell)
  836. class BuildMine(Build):
  837. def __init__(self, cell):
  838. super().__init__(Building.MINE, cell)
  839. # ******** MAIN *************
  840. test = False
  841. if test:
  842. mines_input = []
  843. else:
  844. mines_input = [input() for _ in range(int(input()))]
  845. mines_sites = [MineSite(*[int(j) for j in item.split()]) for item in mines_input]
  846. # log(f"* mines: {mines_sites}")
  847. grid = Grid(mines_sites)
  848. player = Player(ME)
  849. opponent = Player(OPPONENT)
  850. def cmd_wait():
  851. return "WAIT"
  852. def get_input():
  853. if test:
  854. gold, income = 20, 1
  855. opponent_gold, opponent_income = 20, 1
  856. new_grid_input = ['O..#########', '...###...###', '...###....##', '#..##.....##', '...#......##', '#.........##', '##.........#', '##......#...', '##.....##..#', '##....###...', '###...###...', '#########..X']
  857. buildings_input = ['0 0 0 0', '1 0 11 11']
  858. units_input = []
  859. else:
  860. gold, income = int(input()), int(input())
  861. opponent_gold, opponent_income = int(input()), int(input())
  862. new_grid_input = [input() for _ in range(12)]
  863. buildings_input = [input() for _ in range(int(input()))]
  864. units_input = [input() for _ in range(int(input()))]
  865. # log(gold, income, opponent_gold, opponent_income)
  866. # log(new_grid_input)
  867. # log(buildings_input)
  868. # log(units_input)
  869. return gold, income, opponent_gold, opponent_income, new_grid_input, buildings_input, units_input
  870. while True:
  871. # <--- get and parse input
  872. gold, income, opponent_gold, opponent_income, new_grid_input, buildings_input, units_input = get_input()
  873. new_grid = [list(row) for row in new_grid_input]
  874. buildings = [Building(*[int(j) for j in item.split()]) for item in buildings_input]
  875. # log(f"* buildings: {buildings}")
  876. units = [Unit(*[int(j) for j in item.split()]) for item in units_input]
  877. # log(f"* units: {units}")
  878. # --->
  879. log("## Start ##")
  880. log("* Update")
  881. # <--- update
  882. player.update(gold, income, units, buildings)
  883. # log(f"player: {player}")
  884. opponent.update(opponent_gold, opponent_income, units, buildings)
  885. # log(f"opponent: {opponent}")
  886. grid.update(new_grid, buildings, units)
  887. # log(f"grid:\n{grid.print_grid()}")
  888. # --->
  889. commands = []
  890. # start
  891. log(f"Threat level: {grid.threat_level}")
  892. if grid.emergency:
  893. log("<!> HQ is threaten!")
  894. todo = []
  895. played, abandonned = [], []
  896. acted = True
  897. defs, max_def = 0, 8
  898. to_spare = 15 * (grid.threat_level // 5)
  899. while player.can_act():
  900. if acted:
  901. # only re-eval if an action occured
  902. q = InterestQueue()
  903. for cell in grid.influence_zone(ME):
  904. if cell.movable and not cell in abandonned and not cell in played:
  905. if cell.owned and cell.threat > 0 and cell.pos != player.hq.pos:
  906. if defs > max_def:
  907. continue
  908. if grid.emergency:
  909. p = Defend(cell)
  910. else:
  911. p = Secure(cell)
  912. elif cell.opponents:
  913. p = Attack(cell)
  914. elif not cell.owned:
  915. p = Colonize(cell)
  916. else:
  917. continue
  918. p.eval()
  919. q.put(p)
  920. if not q:
  921. break
  922. acted = False
  923. objective = q.get()
  924. if type(objective) is Secure:
  925. if (player.current_gold - to_spare) > Building.cost[Building.TOWER] \
  926. and not objective.cell.mine_site \
  927. and not objective.cell.building:
  928. action = BuildTower(objective.cell)
  929. defs += 1
  930. else:
  931. abandonned.append(objective.cell)
  932. continue
  933. elif type(objective) is Defend:
  934. if player.current_gold > Building.cost[Building.TOWER] \
  935. and not objective.cell.mine_site \
  936. and not objective.cell.building:
  937. action = BuildTower(objective.cell)
  938. defs += 1
  939. elif objective.cell.unit and objective.cell.unit.owned:
  940. # already a unit here: stay on place
  941. objective.cell.unit.has_moved = True
  942. continue
  943. elif player.can_afford(1):
  944. action = Train(1, objective.cell)
  945. defs += 1
  946. else:
  947. near_unit = next((grid[n].unit for n in objective.cell.neighbors
  948. if grid[n].unit
  949. and grid[n].unit.owned
  950. and grid[n].unit.level == 1
  951. and not grid[n].unit.has_moved),
  952. None)
  953. if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
  954. action = Move(near_unit, objective.cell)
  955. defs += 1
  956. else:
  957. abandonned.append(objective.cell)
  958. continue
  959. elif type(objective) in (Colonize, Attack):
  960. near_units = [grid[n].unit for n in objective.cell.neighbors if grid[n].unit \
  961. and grid[n].unit.owned \
  962. and not grid[n].unit.has_moved \
  963. and grid[n].unit.level == objective.min_level]
  964. if len(near_units) == 1:
  965. near_unit = near_units[0]
  966. elif len(near_units) > 1:
  967. near_unit = min(near_units, key=lambda u: len([u.pos == o.cell.pos for o in q.items]))
  968. else:
  969. near_unit = None
  970. if near_unit and grid.can_move(objective.cell.pos, near_unit.level):
  971. action = Move(near_unit, objective.cell)
  972. else:
  973. if objective.min_level > player.max_affordable() or player.gold < (to_spare + Unit.cost[objective.min_level]):
  974. abandonned.append(objective.cell)
  975. continue
  976. action = Train(objective.min_level, objective.cell)
  977. log(f"priority: {objective}")
  978. # a unit occupy the cell
  979. already_there = action.cell.unit
  980. if already_there and already_there.owned:
  981. if already_there.has_moved:
  982. log("cell is occupied: abandon")
  983. abandonned.append(objective.cell)
  984. continue
  985. log(f"* needs to evacuate unit {already_there.id_} before")
  986. evacuate = grid.get_next_move(already_there)
  987. if evacuate:
  988. evac_action = Move(already_there, evacuate.cell)
  989. log(f"forced: {evac_action}")
  990. grid.apply(evac_action)
  991. todo.append(evac_action)
  992. else:
  993. log(f"<!> No move available for unit {already_there.id_}")
  994. abandonned.append(objective.cell)
  995. continue
  996. acted = True
  997. log(f"> Apply action {action}")
  998. played.append(action.cell)
  999. grid.apply(action)
  1000. todo.append(action)
  1001. # units which did not move
  1002. for unit in player.units:
  1003. if not unit.has_moved:
  1004. objective = grid.get_next_move(unit)
  1005. if objective:
  1006. action = Move(unit, objective.cell)
  1007. log(f"default: {action}")
  1008. grid.apply(action)
  1009. todo.append(action)
  1010. else:
  1011. log(f"<!> No move available for unit {unit.id_}")
  1012. unit.has_moved = True
  1013. # can build mines?
  1014. if player.current_gold > grid.cost_for_new_mine():
  1015. site = grid.get_building_site(Building.MINE)
  1016. if site:
  1017. action = BuildMine(site.cell)
  1018. if action:
  1019. log(f"default: {action}")
  1020. grid.apply(action)
  1021. todo.append(action)
  1022. log(f"* todo: {todo}")
  1023. commands = [action.command() for action in todo]
  1024. if not commands:
  1025. log("nothing to do: wait")
  1026. commands = ["WAIT"]
  1027. log(f"* commands: {commands}")
  1028. print(";".join(commands))