fall2022.py 28 KB

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