fall2022.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. import heapq
  2. import sys
  3. import time
  4. debug = True
  5. t0 = time.time()
  6. ## TODOS:
  7. # Indexer les cases à portée de recycleurs alliés uniquement, et ne pas tenir compte des recycleurs ennemis dans les
  8. # priorités de construction de nouveaux recycleurs, au contraire : c'est mieux d'aller piquer le minerai chez l'ennemi!
  9. # Faire des zones à l'intérieur des contigs, pour attirer le mouvement vers les zones à coloniser
  10. # Limiter les constructions agressives en fonction de ces zones
  11. # Identifier le moment où la situation est "verrouillée" (plus d'accès entre alliés et ennemis)
  12. # et cesser les construction de recycleurs
  13. # Identifier les "noeuds" de passages entre les blocs
  14. # si la tension autour d'une case est trop forte autour d'un noeud, ne pas bouger et défendre ; ajouter une action
  15. # "defend"
  16. # Identifier les blocs ennemis au sein du territoire alliée et les détruire prioritairement
  17. ## Pistes:
  18. # s'étendre verticalement dès que la situation se bloque horizontalement
  19. # penser les recycleurs comme des obstacles!
  20. def log(*msg):
  21. if debug:
  22. print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr, flush=True)
  23. class BaseClass:
  24. def __repr__(self):
  25. return f"<{self.__class__.__name__}: {self.__dict__}>"
  26. class Queue(BaseClass):
  27. def __init__(self):
  28. self.items = []
  29. def __bool__(self):
  30. return bool(self.items)
  31. def __repr__(self):
  32. return str(self.items)
  33. def values(self):
  34. return (v for _, v in self.items)
  35. def put(self, priority, item):
  36. while priority in [p for p, _ in self.items]:
  37. priority += 1
  38. heapq.heappush(self.items, (priority, item))
  39. def get(self):
  40. return heapq.heappop(self.items)[1]
  41. def get_items(self):
  42. return heapq.heappop(self.items)
  43. class PathNode(tuple):
  44. def __new__(cls, x, y, parent=None):
  45. n = tuple.__new__(cls, (x, y))
  46. n.parent = parent
  47. n.cost = 0
  48. return n
  49. def __repr__(self):
  50. return f"<{self[0]}, {self[1]}, c:{self.cost}>"
  51. class Player(BaseClass):
  52. ME = 1
  53. OPPONENT = 0
  54. NONE = -1
  55. SIDE_WEST = -1
  56. SIDE_EAST = 1
  57. def __init__(self, id_):
  58. self.id = id_
  59. self.matter = 0
  60. self.territory = 0
  61. self.side = None
  62. class Cell(BaseClass):
  63. def __init__(self, x, y):
  64. self.x = x
  65. self.y = y
  66. self.amount = 0
  67. self.owner = Player.NONE
  68. self.units = 0
  69. self.recycler = False
  70. self.can_build = False
  71. self.can_spawn = False
  72. self.in_range_of_recycler = False
  73. @property
  74. def pos(self):
  75. return self.x, self.y
  76. @property
  77. def owned(self):
  78. return self.owner == Player.ME
  79. @property
  80. def is_opponents(self):
  81. return self.owner == Player.OPPONENT
  82. @property
  83. def is_neutral(self):
  84. return self.owner == Player.NONE
  85. @property
  86. def is_grass(self):
  87. return self.amount == 0
  88. @property
  89. def is_movable(self):
  90. return not self.is_grass and not self.recycler
  91. @property
  92. def lifetime(self):
  93. return 10000000 if not self.in_range_of_recycler else self.amount
  94. @property
  95. def unmovable_next_round(self):
  96. return self.amount == 1 and self.in_range_of_recycler
  97. def update(self, scrap_amount, owner, units, recycler, can_build, can_spawn, in_range_of_recycler):
  98. self.amount = scrap_amount
  99. self.owner = owner
  100. self.units = units
  101. self.recycler = recycler
  102. self.can_build = can_build
  103. self.can_spawn = can_spawn
  104. self.in_range_of_recycler = in_range_of_recycler
  105. class RobotGroup(BaseClass):
  106. COST = 10
  107. def __init__(self, x, y, owner, amount, initial_amount=None):
  108. self.x = x
  109. self.y = y
  110. self.owner = owner
  111. self.amount = amount
  112. self.initial_amount = initial_amount if initial_amount is not None else amount
  113. self.amount_played = 0
  114. @property
  115. def pos(self):
  116. return self.x, self.y
  117. @pos.setter
  118. def pos(self, pos):
  119. self.x, self.y = pos
  120. @property
  121. def owned(self):
  122. return self.owner == Player.ME
  123. @property
  124. def has_played(self):
  125. return self.amount_played >= self.initial_amount
  126. class Recycler(BaseClass):
  127. COST = 10
  128. def __init__(self, x, y, owner, area):
  129. self.x = x
  130. self.y = y
  131. self.owner = owner
  132. self.area = area
  133. @property
  134. def total_available_amount(self):
  135. return sum(cell.amount for cell in self.area)
  136. @property
  137. def immediately_available_amount(self):
  138. return sum((cell.amount > 0) for cell in self.area)
  139. def lifetime(self):
  140. return min(cell.amount for cell in self.area)
  141. def __repr__(self):
  142. return f"<{self.__class__.__name__}: ({self.x}, {self.y}), {self.owner}, " \
  143. f"{self.immediately_available_amount}, {self.total_available_amount}, {self.lifetime()}>"
  144. class Block(BaseClass):
  145. def __init__(self, id_, start):
  146. self.id = id_
  147. self.start = start
  148. self.area = [start]
  149. self.owner = Player.NONE
  150. class Contig(BaseClass):
  151. UNKNOWN = 'U'
  152. OWNED = 'O'
  153. PARTIALLY_OWNED = 'P'
  154. CONFLICTUAL = 'C'
  155. NOT_OWNED = 'N'
  156. def __init__(self, id_, start):
  157. self.id = id_
  158. self.start = start
  159. self.area = [start]
  160. self.status = Contig.UNKNOWN
  161. self.has_robots = False
  162. self.blocks = []
  163. def __repr__(self):
  164. return f"<Contig(start: {self.start}, size: {len(self.area)}, status: {self.status}, robots: {self.has_robots})>"
  165. class MoveOrder(BaseClass):
  166. def __init__(self, pos, amount, priority):
  167. self.pos = pos
  168. self.amount = amount
  169. self.priority = priority
  170. self.affected = 0
  171. @property
  172. def fulfilled(self):
  173. return self.affected >= self.amount
  174. class MoveOrderCandidate(BaseClass):
  175. def __init__(self, order, unit, destination, next_pos=None):
  176. self.order = order
  177. self.unit = unit
  178. self.destination = destination
  179. self.next_pos = next_pos
  180. class Action(BaseClass):
  181. pass
  182. class Wait(Action):
  183. @staticmethod
  184. def do():
  185. return "WAIT"
  186. class Move(Action):
  187. def __init__(self, from_, to, amount, priority):
  188. self.from_ = from_
  189. self.to = to
  190. self.amount = amount
  191. self.priority = priority
  192. def do(self):
  193. x0, y0 = self.from_
  194. x1, y1 = self.to
  195. return f'MOVE {self.amount} {x0} {y0} {x1} {y1}'
  196. class Build(Action):
  197. COST = 10
  198. SUPPORT = 'S'
  199. AGGRESSIVE = 'A'
  200. def __init__(self, pos, destination, priority):
  201. self.pos = pos
  202. self.priority = priority
  203. self.destination = destination
  204. def do(self):
  205. x, y = self.pos
  206. return f'BUILD {x} {y}'
  207. class Spawn(Action):
  208. COST = 10
  209. def __init__(self, pos, amount, priority):
  210. self.pos = pos
  211. self.amount = amount
  212. self.priority = priority
  213. def do(self):
  214. x, y = self.pos
  215. return f'SPAWN {self.amount} {x} {y}'
  216. class Grid(BaseClass):
  217. def __init__(self, width, height, me, opponent):
  218. self.width = width
  219. self.height = height
  220. self.cells = {(x, y): Cell(x, y) for x in range(width) for y in range(height)}
  221. self.round = 0
  222. self.me = me
  223. self.opponent = opponent
  224. self.units = {}
  225. self.recyclers = {}
  226. self.contigs = []
  227. self._neighbors_cache = {}
  228. self._distance_cache = {}
  229. self.index_contigs = {}
  230. self.index_blocks = {}
  231. self.index_tensions = {}
  232. self.index_threats = {}
  233. self.index_nearest_enemy = {}
  234. self.path_cache = {}
  235. @property
  236. def grid(self):
  237. return [[self.cells[(x, y)] for x in range(self.width)] for y in range(self.height)]
  238. def print(self, key=None):
  239. if key is None:
  240. key = lambda x: x
  241. log("\n" + "\n".join(["".join([f"{key(c)}|" for c in row]) for row in self.grid]))
  242. def manhattan(self, from_, to_):
  243. if (from_, to_) in self._distance_cache:
  244. return self._distance_cache[(from_, to_)]
  245. xa, ya = from_
  246. xb, yb = to_
  247. dist = abs(xa - xb) + abs(ya - yb)
  248. self._distance_cache[(from_, to_)] = dist
  249. self._distance_cache[(to_, from_)] = dist
  250. return dist
  251. def neighbors(self, x, y, diags=False):
  252. if (x, y, diags) in self._neighbors_cache:
  253. return self._neighbors_cache[(x, y, diags)]
  254. n = [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
  255. if diags:
  256. n += [(x - 1, y - 1), (x + 1, y - 1), (x - 1, y + 1), (x + 1, y + 1)]
  257. neighbors = [
  258. (x, y)
  259. for x, y in n
  260. if 0 <= x < self.width and 0 <= y < self.height
  261. ]
  262. # self._neighbors_cache[(x, y, diags)] = neighbors
  263. return neighbors
  264. @staticmethod
  265. def create():
  266. me = Player(Player.ME)
  267. opponent = Player(Player.OPPONENT)
  268. w, h = [int(i) for i in input().split()]
  269. return Grid(w, h, me, opponent)
  270. def update(self):
  271. self.round += 1
  272. my_matter, opp_matter = [int(i) for i in input().split()]
  273. self.me.matter = my_matter
  274. self.opponent.matter = opp_matter
  275. self.path_cache = {}
  276. for y in range(self.height):
  277. for x in range(self.width):
  278. scrap_amount, owner, units, recycler, can_build, can_spawn, in_range_of_recycler = [int(k) for k in
  279. input().split()]
  280. self.cells[(x, y)].update(scrap_amount, owner, units, recycler, can_build, can_spawn,
  281. in_range_of_recycler)
  282. if owner == Player.ME and self.me.side is None:
  283. self.me.side = Player.SIDE_WEST if x <= (self.width // 2) else Player.SIDE_EAST
  284. if owner == Player.OPPONENT and self.opponent.side is None:
  285. self.opponent.side = Player.SIDE_WEST if x <= (self.width // 2) else Player.SIDE_EAST
  286. log('update')
  287. # update robots
  288. self.units = {}
  289. for cell in self.cells.values():
  290. if cell.units:
  291. self.units[cell.pos] = RobotGroup(cell.x, cell.y, cell.owner, cell.units)
  292. # update recyclers
  293. self.recyclers = {}
  294. seen = set()
  295. for cell in self.cells.values():
  296. if cell.recycler:
  297. area = [self.cells[pos] for pos in self.neighbors(*cell.pos) if cell.pos not in seen]
  298. seen |= set(self.neighbors(*cell.pos))
  299. self.recyclers[cell.pos] = Recycler(cell.x, cell.y, cell.owner, area)
  300. self.update_possessions()
  301. self.update_contigs()
  302. self.update_blocks()
  303. self.update_tension_map()
  304. self.update_threat_map()
  305. self.update_nearest_enemy()
  306. def owned_units(self):
  307. return [r for r in self.units.values() if r.owner == Player.ME]
  308. def opponent_units(self):
  309. return [r for r in self.units.values() if r.owner == Player.OPPONENT]
  310. def owned_recyclers(self):
  311. return [r for r in self.recyclers.values() if r.owner == Player.ME]
  312. def opponent_recyclers(self):
  313. return [r for r in self.recyclers.values() if r.owner == Player.OPPONENT]
  314. def tension(self, cell):
  315. return self.index_tensions.get(cell.pos, 0)
  316. def threat(self, cell):
  317. return self.index_threats.get(cell.pos, 0)
  318. def current_winner(self):
  319. if self.me.territory > self.opponent.territory:
  320. return Player.ME
  321. elif self.me.territory < self.opponent.territory:
  322. return Player.OPPONENT
  323. else:
  324. return Player.NONE
  325. def update_possessions(self):
  326. self.me.territory = 0
  327. self.opponent.territory = 0
  328. for c in self.cells.values():
  329. if c.owned:
  330. self.me.territory += 1
  331. elif c.is_opponents:
  332. self.opponent.territory += 1
  333. def update_contigs(self):
  334. """ contigs are isolated blocks of cells """
  335. self.contigs = []
  336. self.index_contigs = {}
  337. seen = []
  338. id_ = 0
  339. # build contigs
  340. for c in self.cells.values():
  341. if c.pos in seen or c.is_grass:
  342. continue
  343. id_ += 1
  344. contig = Contig(id_, c.pos)
  345. candidates = self.neighbors(*c.pos)
  346. while candidates:
  347. candidate = candidates.pop()
  348. seen.append(candidate)
  349. if self.cells[candidate].is_grass or self.cells[candidate].recycler or candidate in contig.area:
  350. continue
  351. for n in self.neighbors(*candidate):
  352. if n not in contig.area:
  353. candidates.append(n)
  354. contig.area.append(candidate)
  355. self.contigs.append(contig)
  356. self.index_contigs = {p: None for p in self.cells}
  357. for contig in self.contigs:
  358. owners = set()
  359. # update index
  360. for p in contig.area:
  361. self.index_contigs[p] = contig
  362. owners.add(self.cells[p].owner)
  363. if self.cells[p].owned and self.cells[p].units:
  364. contig.has_robots = True
  365. # status
  366. if Player.ME in owners:
  367. if Player.OPPONENT in owners:
  368. contig.status = Contig.CONFLICTUAL
  369. elif Player.NONE in owners:
  370. contig.status = Contig.PARTIALLY_OWNED
  371. else:
  372. contig.status = Contig.OWNED
  373. else:
  374. contig.status = Contig.NOT_OWNED
  375. def update_blocks(self):
  376. id_ = 0
  377. for contig in self.contigs:
  378. seen = []
  379. for pos in contig.area:
  380. if pos in seen:
  381. continue
  382. buffer = {pos}
  383. id_ += 1
  384. block = Block(id_, pos)
  385. block.owner = self.cells[pos].owner
  386. seen.append(pos)
  387. while buffer:
  388. p = buffer.pop()
  389. for candidate in self.neighbors(*p):
  390. if not candidate in block.area and self.cells[candidate].owner == block.owner:
  391. buffer.add(candidate)
  392. block.area.append(candidate)
  393. seen.append(candidate)
  394. contig.blocks.append(block)
  395. for p in block.area:
  396. self.index_blocks[p] = block
  397. def update_tension_map(self):
  398. self.index_tensions = {}
  399. for c in self.cells.values():
  400. if not c.units:
  401. continue
  402. k = 1 if c.is_opponents else -1
  403. tension = k * c.units
  404. self.index_tensions[c.pos] = self.index_tensions.get(c.pos, 0) + tension
  405. def update_threat_map(self):
  406. self.index_threats = {}
  407. for c in self.cells.values():
  408. if not c.is_opponents or not c.units:
  409. continue
  410. threat = c.units
  411. for n in self.neighbors(*c.pos):
  412. self.index_threats[n] = self.index_threats.get(n, 0) + threat
  413. def update_nearest_enemy(self):
  414. self.index_nearest_enemy = {pos: 0 for pos in self.cells}
  415. for contig in self.contigs:
  416. for pos in contig.area:
  417. c = self.cells[pos]
  418. if not c.owned or not any(self.cells[n].owned for n in self.neighbors(*pos)):
  419. continue
  420. nearest = None
  421. for other_pos in contig.area:
  422. other = self.cells[other_pos]
  423. if not other.is_opponents:
  424. continue
  425. dist = self.manhattan(c.pos, other.pos)
  426. if not nearest or dist < nearest[1]:
  427. nearest = (other.pos, dist)
  428. if nearest:
  429. self.index_nearest_enemy[c.pos] = nearest[1]
  430. def path(self, start, target):
  431. if (start, target) in self.path_cache:
  432. return self.path_cache[(start, target)]
  433. nodes = Queue()
  434. its, break_on = 0, 150
  435. origin = PathNode(*start)
  436. nodes.put(0, origin)
  437. while nodes:
  438. current = nodes.get()
  439. if current == target:
  440. path = []
  441. previous = current
  442. while previous:
  443. if previous != start:
  444. path.insert(0, previous)
  445. previous = previous.parent
  446. return path
  447. neighbors = self.neighbors(*current)
  448. for x, y in neighbors:
  449. its += 1
  450. if its > break_on:
  451. log(f"<!> pathfinding broken from {start} to {target}")
  452. return None
  453. if (x, y) == current.parent:
  454. continue
  455. cell = self.cells[(x, y)]
  456. if not cell.is_movable:
  457. continue
  458. if cell.lifetime <= (current.cost + 1):
  459. continue
  460. cost = current.cost + 1
  461. priority = cost + self.manhattan((x, y), target)
  462. node = PathNode(x, y, current)
  463. node.cost = cost
  464. nodes.put(priority, node)
  465. return None
  466. def get_next_build_action(self):
  467. build_actions = Queue()
  468. # List available build actions
  469. k0 = 80000
  470. kmax_build = 70000
  471. k_available_amount = -1000
  472. k_already_exploited = 30000
  473. k_area_unit = 500
  474. k_area_owned = 1000
  475. k_area_neutral = 0
  476. k_area_opponents = -4000
  477. k_area_opponent_unit = -1000
  478. k_recyclers_owned = 5000
  479. k_space_around = -1000
  480. k_aggressive_build = 8000
  481. k_aggressive_build_for_defense = -6000
  482. k_aggressive_build_tension = -1000
  483. k_first_rounds = 8000 # on décourage le build lors des premiers rounds, il bloque l'avancée
  484. k0_recyclers_owned = k_recyclers_owned * len(self.owned_recyclers())
  485. for contig in self.contigs:
  486. if contig.status in (Contig.OWNED, Contig.PARTIALLY_OWNED, Contig.NOT_OWNED):
  487. # nothing to do
  488. continue
  489. for p in contig.area:
  490. place = self.cells[p]
  491. if not place.owned or place.units or place.recycler:
  492. continue
  493. k = k0
  494. if self.round <= 3:
  495. k += (4 - self.round) * k_first_rounds
  496. area = [place.pos] + self.neighbors(*place.pos)
  497. destination = Build.SUPPORT if not any(self.cells[p].is_opponents for p in area) else Build.AGGRESSIVE
  498. k0_already_exploited = 0
  499. for pos in area:
  500. k += k_available_amount * self.cells[pos].amount
  501. k0_already_exploited += k_already_exploited * self.cells[pos].in_range_of_recycler
  502. if self.cells[pos].owned:
  503. k += k_area_owned
  504. k += k_area_unit * self.cells[pos].units
  505. elif self.cells[pos].is_opponents:
  506. k += k_area_opponents
  507. k += k_area_opponent_unit * self.cells[pos].units
  508. else:
  509. k += k_area_neutral
  510. if destination == Build.SUPPORT:
  511. neighborhood = {n for p in area for n in self.neighbors(*p) if n not in area}
  512. for n in neighborhood:
  513. if self.cells[n].amount > 1:
  514. k += k_space_around
  515. k += k0_recyclers_owned
  516. k += k0_already_exploited
  517. else:
  518. k += k_aggressive_build
  519. if self.current_winner() == Player.ME:
  520. k += k_aggressive_build_for_defense
  521. for n in self.neighbors(p[0], p[1], True):
  522. k += k_aggressive_build_tension * self.index_tensions.get(n, 0)
  523. if k > kmax_build:
  524. continue
  525. action = Build(place.pos, destination, k)
  526. build_actions.put(k, action)
  527. # List available spawn actions
  528. k0 = 54000
  529. kmax_spawn = 70000
  530. k_opportunities = -1000
  531. k_conquest = -3000
  532. k_tension = -1000
  533. k_tension_bridgehead = -1000
  534. k_tension_around = -300
  535. k_tension_around_bridgehead = -300
  536. k_dist_to_enemy = 400
  537. k_bridgehead = -6000
  538. k_reinforcement = 500
  539. ki = 0 # little hack to avoid the while in queue.put
  540. for contig in self.contigs:
  541. if contig.status == Contig.OWNED:
  542. # nothing to do
  543. continue
  544. if contig.status == Contig.PARTIALLY_OWNED and contig.has_robots:
  545. # nothing to do
  546. continue
  547. for p in contig.area:
  548. place = self.cells[p]
  549. if not place.owned or not place.is_movable or place.unmovable_next_round:
  550. continue
  551. k = k0 + ki
  552. ki += 1
  553. is_bridgehead = sum(self.cells[n].is_opponents for n in self.neighbors(p[0], p[1], True)) > 5
  554. tension = self.index_tensions.get(place.pos, 0)
  555. for pos in self.neighbors(place.pos[0], place.pos[1], True):
  556. tension += self.index_tensions.get(pos, 0)
  557. k += k_tension * tension + k_tension_bridgehead * is_bridgehead
  558. for pos in self.neighbors(*place.pos):
  559. n = self.cells[pos]
  560. if not n.is_movable:
  561. continue
  562. if n.is_neutral:
  563. k += k_opportunities
  564. elif n.is_opponents:
  565. k += k_conquest
  566. k += k_tension_around * self.tension(n) + k_tension_around_bridgehead * is_bridgehead
  567. k += k_dist_to_enemy * self.index_nearest_enemy[p]
  568. amount = max(sum([self.index_tensions.get(n, 0) for n in self.neighbors(*p)]), 1)
  569. if k > kmax_spawn:
  570. continue
  571. for _ in range(amount):
  572. action = Spawn(place.pos, 1, k)
  573. build_actions.put(k, action)
  574. k += k_reinforcement
  575. # log('---')
  576. # for action in sorted([a for a in build_actions.values() if type(a) is Spawn], key=lambda x: x.priority)[:10]:
  577. # log(action)
  578. if not build_actions:
  579. return None
  580. action = build_actions.get()
  581. # update cells to take this order into account
  582. place = self.cells[action.pos]
  583. if type(action) is Build:
  584. place.recycler = True
  585. area = [action.pos] + self.neighbors(*action.pos)
  586. self.recyclers[action.pos] = Recycler(action.pos[0], action.pos[1], Player.ME, area)
  587. for pos in area:
  588. self.cells[pos].in_range_of_recycler = True
  589. self.update_contigs()
  590. if type(action) is Spawn:
  591. place.units += action.amount
  592. if action.pos in self.units:
  593. self.units[action.pos].amount += action.amount
  594. else:
  595. self.units[action.pos] = RobotGroup(action.pos[0], action.pos[1], Player.ME, action.amount, 0)
  596. if action.pos in self.index_tensions:
  597. self.index_tensions[action.pos] -= action.amount
  598. else:
  599. self.index_tensions[action.pos] = -1 * action.amount
  600. return action
  601. def build_move_actions(self):
  602. # List possible destinations per contig
  603. move_actions = []
  604. k0_position = 60000
  605. k_position_distance = 3000
  606. k_position_opponents = -5000
  607. k_position_destroy = -3000
  608. k_dist_to_enemy = 2000
  609. k_colonize = -100 * self.width
  610. k_consolidate = -50 * self.width
  611. k_expand = -600
  612. k_threat = -1500
  613. ki = 0 # little hack to avoid the while in queue.put
  614. for contig in self.contigs:
  615. orders = []
  616. units = []
  617. if contig.status in (Contig.NOT_OWNED, Contig.OWNED):
  618. # nothing to do
  619. continue
  620. # on ne conserve que les cases voisines ou limitrophes du territoire
  621. move_area = []
  622. for pos in contig.area:
  623. c = self.cells[pos]
  624. if (c.owned and any(not self.cells[n].owned for n in self.neighbors(*pos))) or \
  625. (not c.owned and any(self.cells[n].owned for n in self.neighbors(*pos))):
  626. move_area.append(pos)
  627. # list destinations
  628. for pos in move_area:
  629. c = self.cells[pos]
  630. k = k0_position + ki
  631. ki += 1
  632. if c.owned or not c.is_movable:
  633. continue
  634. amount_needed = 1
  635. if c.is_opponents:
  636. k += k_position_opponents
  637. if c.units:
  638. k += k_position_destroy
  639. amount_needed = c.amount
  640. k += k_dist_to_enemy * self.index_nearest_enemy[pos]
  641. k += k_threat * self.threat(c)
  642. for n in self.neighbors(pos[0], pos[1], True):
  643. if not self.cells[n].is_grass and not self.cells[n].owned:
  644. k += k_expand
  645. order = MoveOrder(pos, amount_needed, k)
  646. orders.append(order)
  647. # Prioritize units per distance
  648. for pos in contig.area:
  649. if pos in self.units:
  650. unit = self.units[pos]
  651. if not unit.owned or not unit.initial_amount:
  652. continue
  653. units.append(unit)
  654. # limit pathfinding to the priority orders
  655. pathfinding_limit = 4
  656. pathfinding_dist_limit = 5
  657. q = Queue()
  658. for unit in units:
  659. if not unit.initial_amount:
  660. continue
  661. pathfinding_it = 0
  662. for order in orders:
  663. destination = order.pos
  664. next_pos = None
  665. dist = self.manhattan(unit.pos, destination)
  666. if dist == 1:
  667. next_pos = order.pos
  668. elif pathfinding_it < pathfinding_limit and dist <= pathfinding_dist_limit:
  669. pathfinding_it += 1
  670. path = self.path(unit.pos, order.pos)
  671. if path is not None:
  672. dist = len(path)
  673. next_pos = path[0]
  674. priority = order.priority
  675. priority += k_position_distance * dist
  676. if contig.status == Contig.CONFLICTUAL:
  677. # advance
  678. if self.me.side == Player.SIDE_WEST and order.pos[0] > unit.pos[0] \
  679. or self.me.side == Player.SIDE_EAST and order.pos[0] < unit.pos[0]:
  680. priority += k_colonize
  681. # consolidate the frontline
  682. if self.me.side == Player.SIDE_WEST and order.pos[0] == unit.pos[0] \
  683. or self.me.side == Player.SIDE_EAST and order.pos[0] == unit.pos[0]:
  684. priority += k_consolidate
  685. candidate = MoveOrderCandidate(order, unit, destination, next_pos)
  686. q.put(priority, candidate)
  687. # for priority, candidate in sorted(q.items, key=lambda x: x[0])[:18]:
  688. # log(f"{candidate.unit.pos} -> {candidate.order.pos}, p:{priority}, a:{candidate.order.amount}")
  689. # affect orders
  690. while q:
  691. k, candidate = q.get_items()
  692. if candidate.unit.has_played:
  693. continue
  694. if candidate.order.fulfilled:
  695. continue
  696. # attention, on prend le montant initial car les unités spawned ce tour ne peuvent pas bouger
  697. amount = min(candidate.order.amount, candidate.unit.initial_amount)
  698. from_, to_ = candidate.unit.pos, candidate.destination
  699. if from_ == to_:
  700. # already there
  701. continue
  702. action = Move(from_, to_, amount, k)
  703. candidate.unit.amount_played += amount
  704. candidate.order.affected += amount
  705. # update situation
  706. # next_pos = None
  707. # if to_ in self.neighbors(*from_):
  708. # next_pos = to_
  709. # # else:
  710. # # path = self.path(from_, to_)
  711. # # if path:
  712. # # next_pos = path[0]
  713. #
  714. # self.cells[from_].units -= amount
  715. # self.index_tensions[from_] += amount
  716. # if next_pos:
  717. # self.cells[next_pos].units -= amount
  718. # candidate.unit.pos = next_pos
  719. # self.index_tensions[next_pos] = self.index_tensions.get(next_pos, 0) - amount
  720. move_actions.append(action)
  721. return move_actions
  722. def act(self):
  723. # Resolve
  724. actions = []
  725. expanse = 0
  726. log('compute build actions')
  727. while expanse < self.me.matter:
  728. action = self.get_next_build_action()
  729. if action is None:
  730. break
  731. if (expanse + action.COST) > self.me.matter:
  732. break
  733. actions.append(action)
  734. expanse += action.COST
  735. log('computes move actions')
  736. actions += self.build_move_actions()
  737. if not actions:
  738. actions.append(Wait())
  739. log('resolve')
  740. for action in actions:
  741. log(action)
  742. print(";".join([a.do() for a in actions]))
  743. @staticmethod
  744. def debug_print_block(cell):
  745. if cell.pos in grid.index_contigs and grid.index_contigs[cell.pos]:
  746. contig_id = f"{grid.index_contigs[cell.pos].id:02d}"
  747. block_id = f"{grid.index_blocks[cell.pos].id:02d}" if contig_id != 'xx' else 'xx'
  748. return f"{contig_id}.{block_id}"
  749. else:
  750. return 'xx.xx'
  751. grid = Grid.create()
  752. while True:
  753. grid.update()
  754. log(f"round {grid.round}")
  755. # for contig in grid.contigs:
  756. # log(contig)
  757. # grid.print(grid.debug_print_block)
  758. grid.act()