script.py 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292
  1. '''
  2. @author: olivier.massot, 2019
  3. '''
  4. import heapq
  5. import sys
  6. import time
  7. # TODO:
  8. # * find a way to change direction without slowing down if possible
  9. # * Try to integer extra steps into the path algo, instead of adding independents paths
  10. # * Increase the shooting rate in the second part of the game (no barrels left)
  11. # * get_shooting_spots: try to intercept ennemy
  12. debug = True
  13. t0 = time.time()
  14. def log(*msg):
  15. if debug:
  16. print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr)
  17. current_turn = 0
  18. class CollisionAlert(Exception):
  19. pass
  20. class Queue():
  21. def __init__(self):
  22. self.items = []
  23. def __bool__(self):
  24. return bool(self.items)
  25. def __repr__(self):
  26. return str(self.items)
  27. def put(self, item, priority):
  28. heapq.heappush(self.items, (priority, item))
  29. def fput(self, item, priority):
  30. while priority in [p for p, _ in self.items]:
  31. priority += 1
  32. self.put(item, priority)
  33. def get(self):
  34. return heapq.heappop(self.items)[1]
  35. @classmethod
  36. def merge(cls, *args, reverse=False):
  37. q = cls()
  38. q.items = list(heapq.merge(*[a.items for a in args], key=lambda x: x[1], reverse=reverse))
  39. return q
  40. class InterestQueue(Queue):
  41. def __add__(self, other):
  42. self.items += other.items
  43. return self
  44. def put(self, item):
  45. heapq.heappush(self.items, item)
  46. def get(self):
  47. return heapq.heappop(self.items)
  48. @classmethod
  49. def merge(cls, *args, reverse=False):
  50. q = cls()
  51. q.items = list(heapq.merge(*[a.items for a in args], reverse=reverse))
  52. return q
  53. class ObjectivesQueue(InterestQueue):
  54. @classmethod
  55. def re_eval(cls, q, pos=None, d=None):
  56. new_q = cls()
  57. while q:
  58. o = q.get()
  59. o.eval(pos, d)
  60. new_q.put(o)
  61. return new_q
  62. class Base():
  63. def __repr__(self):
  64. return f"<{self.__class__.__name__}: {self.__dict__}>"
  65. class BaseObjective(Base):
  66. def __init__(self, target):
  67. self.target = target
  68. self.interest = 0
  69. def __lt__(self, other):
  70. return self.interest < other.interest
  71. def __repr__(self):
  72. return f"<{self.__class__.__name__}: target={self.target.id};int={self.interest})>"
  73. def _pre_eval(self, pos = None, d = None):
  74. self.distance = Grid.manhattan(pos, self.target.pos) if pos is not None else 0
  75. self.alignment = abs(Grid.diff_directions(Grid.direction_to(*pos, *self.target.pos), d)) if d is not None else 0
  76. def eval(self, pos = None, d = None):
  77. self._pre_eval(pos, d)
  78. self._compute_interest()
  79. def _compute_interest(self):
  80. self.interest = 7 * self.distance + 3 * self.alignment
  81. class GetBarrel(BaseObjective):
  82. def _pre_eval(self, pos = None, d = None):
  83. super()._pre_eval(pos, d)
  84. self.ennemy_near = any(Grid.manhattan(e.next_pos, self.target.pos) < self.distance for e in grid.ennemy_ships)
  85. def _compute_interest(self):
  86. self.interest = 6 * self.distance + 9 * self.alignment + 3 * self.target.dispersal + self.target.mine_threat * 2 + 12 * self.ennemy_near
  87. if self.distance <= 2 and self.alignment > 1:
  88. # dead angle
  89. self.interest += 36
  90. class Attack(BaseObjective):
  91. def _compute_interest(self):
  92. self.interest = 7 * self.distance + 3 * self.alignment + self.target.stock // 4 - 20 * self.target.blocked_since
  93. class PathNode(tuple):
  94. def __new__(self, x, y, parent=None):
  95. n = tuple.__new__(self, (x, y))
  96. n.parent = parent
  97. n.cost = 0
  98. n.orientation = 0
  99. return n
  100. def __repr__(self):
  101. return f"<{self[0]}, {self[1]}, c:{self.cost}, o:{self.orientation}>"
  102. class Grid(Base):
  103. w = 23
  104. h = 21
  105. _neighbors = {}
  106. _next_cell = {}
  107. def __init__(self):
  108. self.load_entities({})
  109. @classmethod
  110. def preload(cls):
  111. cls._neighbors = {}
  112. for x in range(-1, cls.w + 1):
  113. for y in range(-1, cls.h + 1):
  114. cls.cache_neighbors(x, y)
  115. cls._next_cell = {}
  116. for x in range(0, cls.w):
  117. for y in range(0, cls.h):
  118. cls.cache_next_cell(x, y)
  119. for x in range(0, cls.w):
  120. for y in range(0, cls.h):
  121. Ship.cache_area(x, y)
  122. @classmethod
  123. def contains(cls, x, y):
  124. return 0 <= x < cls.w and 0 <= y < cls.h
  125. def __contains__(self, key):
  126. return self.contains(*key)
  127. def __iter__(self):
  128. for item in ((x, y) for x in range(self.w) for y in range(self.h)):
  129. yield item
  130. # data
  131. def load_entities(self, entities):
  132. # special: mines too far from ships are not recorded but still exist
  133. ghost_mines = []
  134. if hasattr(self, "mines"):
  135. for m in self.mines:
  136. if not m.id in entities:
  137. if all((self.manhattan(m.pos, ship.pos) > 5) for ship in self.owned_ships):
  138. m.ghost = True
  139. ghost_mines.append(m)
  140. self.entities = entities
  141. self.index = {}
  142. self.ships = []
  143. self.owned_ships = []
  144. self.ennemy_ships = []
  145. self.ships = []
  146. self.barrels = []
  147. self.mines = []
  148. self.cannonballs = []
  149. self.threat = {}
  150. for e in list(entities.values()) + ghost_mines:
  151. self.index[e.pos] = e
  152. type_ = type(e)
  153. if type_ is Ship:
  154. self.ships.append(e)
  155. if e.owned:
  156. self.owned_ships.append(e)
  157. else:
  158. self.ennemy_ships.append(e)
  159. elif type_ is Barrel:
  160. self.barrels.append(e)
  161. elif type_ is Mine:
  162. self.mines.append(e)
  163. elif type_ is Cannonball:
  164. self.cannonballs.append(e)
  165. if e.pos not in self.threat or self.threat[e.pos] > e.countdown:
  166. self.threat = {e.pos: e.countdown}
  167. for s in self.owned_ships:
  168. s.allies = [other for other in self.owned_ships if other is not s]
  169. for s in self.ennemy_ships:
  170. s.allies = [other for other in self.ennemy_ships if other is not s]
  171. for s in self.ships:
  172. s.next_pos_proba(2)
  173. self.next_to_mine = {}
  174. for m in self.mines:
  175. for n in self.neighbors(*m.pos):
  176. self.next_to_mine[n] = m
  177. self.next_to_barrel = {}
  178. for b in self.barrels:
  179. for n in self.neighbors(*b.pos):
  180. self.next_to_barrel[n] = b
  181. self.update_moving_costs()
  182. grav_center = self.barrels_gravity_center()
  183. for b in self.barrels:
  184. b.dispersal = Grid.manhattan(grav_center, b.pos) if grav_center != None else 0
  185. b.mine_threat = any(type(self.at(*c)) is Mine for c in self.neighbors(*b.pos))
  186. for s in self.owned_ships:
  187. s._can_move = {c: (s.moving_cost(*c) < 1000)
  188. for c in [s.front, s.front_left, s.left, s.front_right,
  189. s.right, s.back_left, s.back_right]}
  190. s.objectives = ObjectivesQueue()
  191. s.ennemies = ObjectivesQueue()
  192. for b in self.barrels:
  193. obj = GetBarrel(b)
  194. obj.eval(s.prow, s.orientation)
  195. s.objectives.put(obj)
  196. for e in self.ennemy_ships:
  197. obj = Attack(e)
  198. obj.eval(s.pos, s.orientation)
  199. s.ennemies.put(obj)
  200. def at(self, x, y):
  201. try:
  202. return self.index[(x, y)]
  203. except KeyError:
  204. return None
  205. @classmethod
  206. def is_border(cls, x, y):
  207. return x == -1 or y == -1 or x == cls.w or y == cls.h
  208. def collision_at(self, x, y):
  209. e = self.at(x, y)
  210. return type(e) in [Mine, Ship, Cannonball] or not (x, y) in self.__iter__()
  211. def barrels_gravity_center(self):
  212. wx, wy, wtotal = 0,0,0
  213. for b in self.barrels:
  214. wx += (b.x * b.amount)
  215. wy += (b.y * b.amount)
  216. wtotal += b.amount
  217. return (wx // wtotal, wy // wtotal) if wtotal else None
  218. def update_moving_costs(self):
  219. base_costs = {}
  220. self.collisions = []
  221. for x in range(-1, self.w + 1):
  222. for y in range(-1, self.h + 1):
  223. base_costs[(x, y)] = 10 # base moving cost
  224. for x, y in base_costs:
  225. if x in (-1, self.w + 1) or y in (-1, self.h):
  226. base_costs[(x, y)] = 1000 # out of the map
  227. elif x in (0, self.w - 1) or y in (0, self.h - 1):
  228. base_costs[(x, y)] = 15 # borders are a little more expensive
  229. for c in self.next_to_mine:
  230. base_costs[c] += 30
  231. for m in self.mines:
  232. base_costs[m.pos] += 1000
  233. if m.pos in self.threat:
  234. if self.threat[m.pos] <= 2:
  235. # avoid the area of a mines going to explode
  236. for n in self.neighbors(*m.pos):
  237. base_costs[n] += 1000
  238. for c in self.cannonballs:
  239. if 0 < c.countdown <= 2:
  240. base_costs[c.pos] += 1000
  241. for ship in self.ships:
  242. ship._moving_costs = {}
  243. ship._moving_costs.update(base_costs)
  244. played_before = True
  245. for other in self.ships:
  246. if other is ship:
  247. played_before = False
  248. continue
  249. dist = self.manhattan(ship.pos, other.pos)
  250. if dist > 6:
  251. continue
  252. for c in self.zone(other.pos, 3):
  253. ship._moving_costs[c] += 25
  254. next_positions = other.next_pos_proba()
  255. for c, proba in next_positions[1 if played_before else 0].items():
  256. if proba >= 20:
  257. ship._moving_costs[c] = ship._moving_costs.get(c, 0) + 13 * proba
  258. def shooting_spot(self, ship, target, current=None):
  259. shooting_spots = Queue()
  260. target_pos = target.next_pos if type(target) is Ship else target.pos
  261. for x, y in self.zone(target_pos, 8):
  262. if ship.moving_cost(x, y) > 100:
  263. continue
  264. if self.manhattan((x, y), target_pos) <= 2:
  265. continue
  266. interest = 0 # the lower the better
  267. if (x, y) == current:
  268. interest -= 20
  269. interest += ship.moving_cost(x, y)
  270. # avoid cells too close from borders
  271. if not 3 < x < (self.w - 3):
  272. interest += 50
  273. if not 3 <= y < (self.h - 3):
  274. interest += 50
  275. # priorize cells in the current direction
  276. diff = abs(Grid.diff_directions(ship.orientation, Grid.direction_to(*ship.prow, x, y)))
  277. interest += 10 * abs(diff)
  278. # priorize spots at distance 6 from active ship
  279. interest += (10 * abs(6 - self.manhattan((x, y), ship.pos)))
  280. # priorize spots at distance 6 from targetted ship
  281. interest += (10 * abs(6 - self.manhattan((x, y), target.pos)))
  282. shooting_spots.put((x, y), interest)
  283. return shooting_spots.get()
  284. # geometrical algorithms
  285. @staticmethod
  286. def from_cubic(xu, yu, zu):
  287. return (zu, int(xu + (zu - (zu & 1)) / 2))
  288. @staticmethod
  289. def to_cubic(x, y):
  290. zu = x
  291. xu = int(y - (x - (x & 1)) / 2)
  292. yu = int(-xu - zu)
  293. return (xu, yu, zu)
  294. @staticmethod
  295. def manhattan(from_, to_):
  296. xa, ya = from_
  297. xb, yb = to_
  298. return abs(xa - xb) + abs(ya - yb)
  299. @classmethod
  300. def zone(cls, center, radius):
  301. return [(x, y) for x in range(0, cls.w) for y in range(0, cls.h) if cls.manhattan(center, (x, y)) <= radius]
  302. @staticmethod
  303. def closest(from_, in_):
  304. return min(in_, key=lambda x: Grid.manhattan(from_, x.pos))
  305. @staticmethod
  306. def directions(y):
  307. if y % 2 == 0:
  308. return [(1, 0), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1)]
  309. else:
  310. return [(1, 0), (1,-1), (0,-1), (-1, 0), (0, 1), (1, 1)]
  311. @staticmethod
  312. def direction_to(x0, y0, x, y):
  313. dx, dy = (x - x0), (y - y0)
  314. if dx > 0:
  315. if dy == 0:
  316. return 0
  317. elif dy > 0:
  318. return 5
  319. else:
  320. return 1
  321. elif dx < 0:
  322. if dy == 0:
  323. return 3
  324. elif dy > 0:
  325. return 4
  326. else:
  327. return 2
  328. else:
  329. if dy > 0:
  330. return 5 if y0 % 2 == 0 else 4
  331. else:
  332. return 1 if y0 % 2 == 0 else 2
  333. @staticmethod
  334. def add_directions(d1, d2):
  335. d = d2 + d1
  336. if d < 0:
  337. d += 6
  338. elif d > 5:
  339. d -= 6
  340. return d
  341. @staticmethod
  342. def diff_directions(d1, d2):
  343. d = d2 - d1
  344. if d <= -3:
  345. d += 6
  346. elif d > 3:
  347. d -= 6
  348. return d
  349. # @staticmethod
  350. # def next_cell(x, y, d, repeat=1):
  351. # for _ in range(repeat):
  352. # dx, dy = Grid.directions(y)[d]
  353. # x, y = x + dx, y + dy
  354. # return x, y
  355. @staticmethod
  356. def symetry(d):
  357. return d + 3 if d < 3 else d - 3
  358. @staticmethod
  359. def abs_neighbors(x, y):
  360. return ((x + dx, y + dy) for dx, dy in Grid.directions(y))
  361. @classmethod
  362. def cache_neighbors(cls, xc, yc):
  363. cls._neighbors[(xc, yc)] = [(x, y) for x, y in Grid.abs_neighbors(xc, yc) if 0 <= x < cls.w and 0 <= y < cls.h]
  364. @classmethod
  365. def neighbors(cls, x, y):
  366. try:
  367. return cls._neighbors[(x, y)]
  368. except KeyError:
  369. cls.cache_neighbors(x, y)
  370. return cls._neighbors[(x, y)]
  371. @classmethod
  372. def abs_next_cell(cls, x, y, d):
  373. dx, dy = Grid.directions(y)[d]
  374. return x + dx, y + dy
  375. @classmethod
  376. def cache_next_cell(cls, x, y):
  377. for d, dv in enumerate(Grid.directions(y)):
  378. dx, dy = dv
  379. cls._next_cell[(x, y, d)] = (x + dx, y + dy)
  380. @classmethod
  381. def next_cell(cls, x, y, d, repeat=1):
  382. for _ in range(repeat):
  383. try:
  384. x, y = cls._next_cell[(x, y, d)]
  385. except KeyError:
  386. x, y = cls.abs_next_cell(x, y, d)
  387. return x, y
  388. def rotate(self, center, coordinates, rotations):
  389. if coordinates == [center] or rotations % 6 == 0:
  390. return coordinates
  391. x0, y0 = center
  392. xu0, yu0, zu0 = self.to_cubic(x0, y0)
  393. result = []
  394. for x, y in coordinates:
  395. xu, yu, zu = self.to_cubic(x, y)
  396. dxu, dyu, dzu = xu - xu0, yu - yu0, zu - zu0
  397. for _ in range(rotations):
  398. dxu, dyu, dzu = -dzu, -dxu, -dyu
  399. xru, yru, zru = dxu + xu0, dyu + yu0, dzu + zu0
  400. xr, yr = self.from_cubic(xru, yru, zru)
  401. result.append((xr, yr))
  402. return result
  403. # pathfinding
  404. def path(self, start, start_d, target, moving_costs={}, inertia=0, incl_start=False, limit=10000):
  405. nodes = Queue()
  406. break_on, iteration = limit, 0
  407. broken = False
  408. effective_start = start
  409. origin = PathNode(*effective_start)
  410. origin.orientation = start_d
  411. for _ in range(inertia):
  412. effective_start = self.next_cell(*effective_start, start_d)
  413. origin = PathNode(*effective_start, origin)
  414. origin.orientation = start_d
  415. nodes.put(origin, 0)
  416. neighbors = []
  417. while nodes:
  418. current = nodes.get()
  419. if broken or current == target:
  420. path = []
  421. previous = current
  422. while previous:
  423. if previous != start or incl_start:
  424. path.insert(0, previous)
  425. previous = previous.parent
  426. return path, iteration
  427. neighbors = self.neighbors(*current)
  428. for x, y in neighbors:
  429. if (x, y) == current.parent:
  430. continue
  431. iteration += 1
  432. if break_on > 0 and iteration >= break_on:
  433. broken = True
  434. moving_cost = moving_costs.get((x, y), 1000)
  435. if moving_cost >= 1000:
  436. continue
  437. d = Grid.direction_to(*current, x, y)
  438. diff = abs(Grid.diff_directions(current.orientation, d))
  439. if diff > 1:
  440. # change direction one degree at a time
  441. continue
  442. area = Ship.get_area(x, y, d)
  443. if any((moving_costs.get(c, 1000) >= 1000 and not Grid.is_border(*c)) for c in area):
  444. continue
  445. cost = current.cost + moving_cost + diff * 20
  446. if (x, y) == start and d == start_d:
  447. # prefer to go right at start (if not speed)
  448. cost -= 10
  449. # if diff and current.parent and current.orientation != current.parent.orientation:
  450. # # avoid consecutives direction changes
  451. # cost += 20
  452. priority = cost + 10 * Grid.manhattan((x, y), target)
  453. node = PathNode(x, y, current)
  454. node.cost = cost
  455. node.orientation = d
  456. nodes.put(node, priority)
  457. return None, iteration
  458. class Entity(Base):
  459. def __init__(self, ent_id):
  460. self.id = int(ent_id)
  461. self.x, self.y = 0, 0
  462. self.args = [0,0,0,0]
  463. def update(self, x, y, *args):
  464. self.x, self.y = int(x), int(y)
  465. @property
  466. def pos(self):
  467. return (self.x, self.y)
  468. def __lt__(self, other):
  469. # default comparison, used to avoid errors when used with queues and priorities are equals
  470. return self.id < other.id
  471. class Position(Base):
  472. def __init__(self, pos, d, speed, weight=10):
  473. self.pos = pos
  474. self.d = d
  475. self.speed = speed
  476. self.weight = weight
  477. self.area = Ship.get_area(*self.pos, self.d)
  478. class Ship(Entity):
  479. MAX_SPEED = 2
  480. SCOPE = 10
  481. SLOW_DOWN = 1
  482. SPEED_UP = 2
  483. TURN_LEFT = 3
  484. TURN_RIGHT = 4
  485. MOVES = [None, SPEED_UP, TURN_LEFT, TURN_RIGHT, SLOW_DOWN]
  486. COMMANDS = {SLOW_DOWN: "SLOWER", SPEED_UP: "FASTER", TURN_LEFT: "PORT", TURN_RIGHT: "STARBOARD", None: "NONE"}
  487. areas = {}
  488. def __init__(self, *args, **kwargs):
  489. super().__init__(*args, **kwargs)
  490. self.x, self.y = 0, 0
  491. self.orientation = 0
  492. self.speed = 0
  493. self.stock = 0
  494. self.owned = 0
  495. self.next_cell = None
  496. self.next_pos = None
  497. self.last_fire = None
  498. self.last_mining = None
  499. self.blocked_since = 0
  500. self.same_traject_since = 0
  501. self.last_action = ""
  502. self.allies = []
  503. self._moving_costs = {}
  504. self.objectives = ObjectivesQueue()
  505. self.ennemies = ObjectivesQueue()
  506. self.objective = None
  507. self.objectives_next = []
  508. self.target_ennemy = None
  509. self.path = []
  510. self.distance = 0
  511. self.alignment = 0
  512. def __repr__(self):
  513. return f"<Ship {self.id}: pos=({self.x}, {self.y}), orientation={self.orientation}, speed={self.speed}, blocked={self.blocked_since}, last_fire={self.last_fire}, next_pos={self.next_pos}, area={self.area}>"
  514. @classmethod
  515. def abs_area(cls, x, y, d):
  516. return [Grid.next_cell(x, y, d), (x, y), Grid.next_cell(x, y, Grid.add_directions(d, 3))]
  517. @classmethod
  518. def cache_area(cls, x, y):
  519. for d in range(3):
  520. area = [Grid.next_cell(x, y, d), (x, y), Grid.next_cell(x, y, d + 3)]
  521. Ship.areas[(x, y, d)] = area
  522. Ship.areas[(x, y, d + 3)] = list(reversed(area))
  523. @classmethod
  524. def get_area(cls, x, y, d):
  525. try:
  526. return list(Ship.areas[(x, y, d)])
  527. except KeyError:
  528. return cls.abs_area(x, y, d)
  529. def update(self, x, y, *args):
  530. previous_state = self.state()
  531. previous_traject = self.traject()
  532. super().update(x, y)
  533. self.orientation, self.speed, self.stock, self.owned = map(int, args)
  534. self.objectives = ObjectivesQueue()
  535. self.ennemies = ObjectivesQueue()
  536. self.objective = None
  537. self.objectives_next = []
  538. self.target_ennemy = None
  539. self.goto = None
  540. self.path = []
  541. self.cached_next_pos_proba = {}
  542. self.area = Ship.get_area(self.x, self.y, self.orientation)
  543. self.prow, _, self.stern = self.area
  544. self.next_cell = self.get_next_cell()
  545. self.next_pos = self.get_next_pos()
  546. self.next_move = None
  547. self.next_area = Ship.get_area(*self.next_pos, self.orientation)
  548. self.front = Grid.next_cell(*self.prow, self.orientation)
  549. self.front_left = Grid.next_cell(*self.prow, Grid.add_directions(self.orientation, 1))
  550. self.left = Grid.next_cell(*self.prow, Grid.add_directions(self.orientation, 2))
  551. self.front_right = Grid.next_cell(*self.prow, Grid.add_directions(self.orientation, -1))
  552. self.right = Grid.next_cell(*self.prow, Grid.add_directions(self.orientation, -2))
  553. self.back_left = Grid.next_cell(*self.stern, Grid.add_directions(self.orientation, 1))
  554. self.back_right = Grid.next_cell(*self.stern, Grid.add_directions(self.orientation, -1))
  555. self._can_move = {}
  556. if self.traject() != previous_traject:
  557. self.same_traject_since += 1
  558. else:
  559. self.same_traject_since = 0
  560. if self.state() == previous_state:
  561. self.blocked_since += 1
  562. else:
  563. self.blocked_since = 0
  564. def traject(self):
  565. return (self.orientation, self.speed)
  566. def state(self):
  567. return (self.x, self.y, self.orientation, self.speed)
  568. @classmethod
  569. def get_pos_in(cls, current, speed, orientation, in_=1):
  570. return Grid.next_cell(*current, orientation, repeat=speed * in_)
  571. def get_next_pos(self, in_=1):
  572. return self.get_pos_in(self.pos, self.speed, self.orientation, in_)
  573. def next_pos_proba(self, in_=1):
  574. if in_ in self.cached_next_pos_proba:
  575. return {k: v for k, v in self.cached_next_pos_proba.items() if k <= in_}
  576. # guess next positions
  577. positions = {0: [Position(self.pos, self.orientation, self.speed)]}
  578. for i in range(in_):
  579. positions[i + 1] = []
  580. for p in positions[i]:
  581. pos, d, speed = p.pos, p.d, p.speed
  582. # next pos with inertia
  583. inertia = Grid.next_cell(*pos, d, repeat=speed)
  584. # wait, fire or mine
  585. p = Position(inertia, d, speed, 30)
  586. if self.moving_cost(*p.pos) >= 1000:
  587. p.weight = 10
  588. # turn left
  589. p = Position(inertia, Grid.add_directions(d, 1), speed)
  590. if not self.moving_cost(*p.pos) >= 1000:
  591. positions[i + 1].append(p)
  592. # turn right
  593. p = Position(inertia, Grid.add_directions(d, -1), speed)
  594. if not self.moving_cost(*p.pos) >= 1000:
  595. positions[i + 1].append(p)
  596. # speed up
  597. if speed < self.MAX_SPEED:
  598. p = Position(Grid.next_cell(*pos, d, repeat=speed + 1), d, speed + 1)
  599. if not self.moving_cost(*p.pos) >= 1000:
  600. positions[i + 1].append(p)
  601. # slow down
  602. if speed > 1:
  603. p = Position(Grid.next_cell(*pos, d, repeat=speed - 1), d, speed - 1)
  604. if not self.moving_cost(*p.pos) >= 1000:
  605. positions[i + 1].append(p)
  606. # we voluntary ignore the case where a ship at speed 1 would slow down,
  607. # as it is not expected to be a standard behaviour for a ship
  608. # agregate
  609. proba = {}
  610. for i, plst in positions.items():
  611. proba[i] = {}
  612. weights = {}
  613. total_weight = sum([p.weight for p in plst])
  614. for p in plst:
  615. for c in p.area:
  616. weights[c] = weights.get(c, 0) + p.weight
  617. for c in weights:
  618. proba[i][c] = 100 * weights[c] // total_weight
  619. self.cached_next_pos_proba = proba
  620. return proba
  621. def guess_next_positions(self, in_=1):
  622. proba = self.next_pos_proba(in_)
  623. best = {}
  624. for i in proba:
  625. best[i] = max(proba[i].items(), key=lambda x: x[1])[0]
  626. return best
  627. def get_next_cell(self, in_=1):
  628. return Grid.next_cell(self.x, self.y, self.orientation, repeat=in_)
  629. def in_current_direction(self, x, y):
  630. return self.orientation == Grid.direction_to(*self.pos, x, y)
  631. def moving_cost(self, x, y):
  632. return self._moving_costs.get((x, y), 1000)
  633. def can_turn_left(self):
  634. return (self._can_move[self.left] or grid.is_border(*self.left)) \
  635. and (self._can_move[self.right] or grid.is_border(*self.right)) \
  636. and (self._can_move[self.back_right] or grid.is_border(*self.back_right))
  637. def can_turn_right(self):
  638. return (self._can_move[self.right] or grid.is_border(*self.right)) \
  639. and (self._can_move[self.left] or grid.is_border(*self.left)) \
  640. and (self._can_move[self.back_left] or grid.is_border(*self.back_left))
  641. def can_move_fwd(self):
  642. return self._can_move[self.front]
  643. def can_move(self):
  644. return self.can_move_fwd() or self.can_turn_left() or self.can_turn_left()
  645. def area_after_moving(self, move):
  646. new_speed = self.speed
  647. new_orientation = self.orientation
  648. if move == Ship.SPEED_UP:
  649. new_speed += 1
  650. elif move == Ship.SLOW_DOWN:
  651. new_speed -= 1
  652. elif move == Ship.TURN_LEFT:
  653. new_orientation = Grid.add_directions(self.orientation, 1)
  654. elif move == Ship.TURN_RIGHT:
  655. new_orientation = Grid.add_directions(self.orientation, -1)
  656. new_pos = self.get_next_cell(new_speed)
  657. return self.get_area(*new_pos, new_orientation)
  658. def plan_next_move(self):
  659. if self.speed and any(self.front in s.area for s in grid.ships if s is not self):
  660. log("Blocked: speed 0")
  661. self.speed = 0
  662. if self.path:
  663. planned = self._follow_path(self.path)
  664. if self.path is None:
  665. if self.can_move():
  666. available = {}
  667. if self.can_move_fwd():
  668. if self.speed:
  669. available[None] = 0
  670. else:
  671. available[Ship.SPEED_UP] = 0
  672. if self.can_turn_left():
  673. available[Ship.TURN_LEFT] = 0
  674. if self.can_turn_right():
  675. available[Ship.TURN_RIGHT] = 0
  676. for m in available:
  677. new_area = self.area_after_moving(m)
  678. available[m] = 2 * self.moving_cost(*new_area[0]) + 2 * self.moving_cost(*new_area[1]) + self.moving_cost(*new_area[2])
  679. planned = min(available.items(), key=lambda x: x[1])[0]
  680. log(f"(!) broken: automove ({Ship.COMMANDS[planned]})")
  681. else:
  682. log(f"(!) broken: can not move")
  683. self.next_area = self.area
  684. return False
  685. elif not self.path:
  686. return False
  687. next_move = None
  688. available_moves = [planned] + [m for m in Ship.MOVES if m != planned]
  689. risk = Queue()
  690. for move in available_moves:
  691. new_area = self.area_after_moving(move)
  692. r = 0
  693. for i, c in enumerate(new_area):
  694. mc = self.moving_cost(*c)
  695. if mc >= 1000 and c in grid: # special: extra-grid cells are not consider as collisions since a part of the ship can go there
  696. if grid.threat.get(c, 0) > 1 and mc < 2000:
  697. r += 10
  698. else:
  699. if i == 1:
  700. # the center of the ship is threaten
  701. r += 50
  702. else:
  703. r += 25
  704. risk.fput(move, r)
  705. if r:
  706. log(f"/!\ Danger: planned move <{Ship.COMMANDS[move]}> could lead to collision (risk={r}, area={new_area})")
  707. else:
  708. log(f"Safe move: {move}")
  709. next_move = move
  710. break
  711. else:
  712. try:
  713. next_move = risk.get()
  714. log(f"* No collision-free move was found, go to the less risky: {next_move}")
  715. except IndexError:
  716. next_move = planned
  717. log("* No collision-free move was found, go to the initial one: {next_move}")
  718. self.next_move = next_move
  719. self.next_area = new_area
  720. return True
  721. def _follow_path(self, path):
  722. # flags represent direction changes or end of the path
  723. last_flag = len(path)
  724. next_flag = next((i for i, n in enumerate(path) if n.orientation != self.orientation), last_flag)
  725. afternext_flag = next((i for i, n in enumerate(path[next_flag:]) if n.orientation != path[next_flag].orientation), last_flag)
  726. if not self.speed:
  727. diff = Grid.diff_directions(self.orientation, path[0].orientation)
  728. if diff > 0 and self.last_action == "STARBOARD" or diff < 0 and self.last_action == "PORT":
  729. # special: avoid the starting hesitation
  730. return Ship.SPEED_UP
  731. if diff and next_flag == 0:
  732. # start, with a direction change
  733. if diff > 0:
  734. return Ship.TURN_LEFT
  735. elif diff < 0:
  736. return Ship.TURN_RIGHT
  737. # start straight
  738. else:
  739. return Ship.SPEED_UP
  740. elif self.speed == self.MAX_SPEED:
  741. if next_flag <= self.speed:
  742. if afternext_flag >= (next_flag + 2): # there is at least one straight cell after this drift
  743. # drift
  744. diff = Grid.diff_directions(self.orientation, path[next_flag].orientation)
  745. if diff > 0:
  746. return Ship.TURN_LEFT
  747. elif diff < 0:
  748. return Ship.TURN_RIGHT
  749. else:
  750. return Ship.SLOW_DOWN
  751. if next_flag <= self.speed + 1:
  752. # next direction change or target will be passed at current speed
  753. return Ship.SLOW_DOWN
  754. elif self.speed == 1:
  755. if next_flag <= 1:
  756. diff = Grid.diff_directions(self.orientation, path[next_flag].orientation)
  757. if diff > 0:
  758. return Ship.TURN_LEFT
  759. elif diff < 0:
  760. return Ship.TURN_RIGHT
  761. elif next_flag >= 4:
  762. return Ship.SPEED_UP
  763. return None
  764. def move(self):
  765. if self.next_move == Ship.SPEED_UP:
  766. self.speed_up()
  767. elif self.next_move == Ship.SLOW_DOWN:
  768. self.slow_down()
  769. elif self.next_move == Ship.TURN_LEFT:
  770. self.turn_left()
  771. elif self.next_move == Ship.TURN_RIGHT:
  772. self.turn_right()
  773. else:
  774. return False
  775. return True
  776. def fire_at_will(self, *args, **kwargs):
  777. return self._fire_at_will(*args, **kwargs)
  778. def _fire_at_will(self, target):
  779. if not self.can_fire():
  780. return False
  781. avoid = []
  782. for ship in grid.owned_ships:
  783. avoid += ship.next_area
  784. next_positions = target.next_pos_proba(4)
  785. barrels = [b.pos for b in grid.barrels]
  786. for bpos in barrels:
  787. if any(Grid.manhattan(ship.pos, bpos) <= Grid.manhattan(target.pos, bpos) for ship in grid.owned_ships):
  788. avoid.append(bpos)
  789. for t, probas in next_positions.items():
  790. # include mines and barrels
  791. mines_next = {}
  792. for c, proba in probas.items():
  793. if c in grid.next_to_mine:
  794. mpos = grid.next_to_mine[c].pos
  795. dist = Grid.manhattan(self.next_pos, mpos)
  796. if dist <= 2:
  797. continue
  798. alignment = abs(Grid.diff_directions(self.orientation, Grid.direction_to(*self.pos, *mpos)))
  799. if alignment <= 1:
  800. continue
  801. mines_next[mpos] = mines_next.get(mpos, 1) + proba
  802. probas.update(mines_next)
  803. for c in probas:
  804. if c in barrels:
  805. probas[c] += 20
  806. shots = sorted(probas.items(), key=lambda x: x[1], reverse=True)
  807. for c, proba in shots:
  808. if c in avoid:
  809. continue
  810. if proba < 20:
  811. continue
  812. dist = Grid.manhattan(self.prow, c)
  813. if dist > self.SCOPE:
  814. continue
  815. # time for the cannonball to reach this pos (including fire turn)
  816. delay = 1 + (1 + round(dist / 3))
  817. if delay != t:
  818. continue
  819. log(f"[x] precise shoot: pos={c}")
  820. ship.fire(*c)
  821. return True
  822. return False
  823. def can_mine(self):
  824. return self.last_mining is None or (current_turn - self.last_mining) >= 4
  825. def can_fire(self):
  826. return self.last_fire is None or (current_turn - self.last_fire) >= 1
  827. def mine_maybe(self):
  828. if self.can_mine():
  829. if not any(Grid.manhattan(self.prow, ally.next_pos) <= 5 for ally in self.allies):
  830. self.mine()
  831. return True
  832. return False
  833. # --- Basic commands
  834. def _act(self, cmd, *args):
  835. self.last_action = cmd
  836. output = " ".join([cmd] + [str(a) for a in args])
  837. log(f"ship {self.id}: {output}")
  838. print(output)
  839. def auto_move(self, x, y):
  840. self._act("MOVE", x, y)
  841. def speed_up(self):
  842. self._act("FASTER")
  843. def slow_down(self):
  844. self._act("SLOWER")
  845. def turn_right(self):
  846. self._act("STARBOARD")
  847. def turn_left(self):
  848. self._act("PORT")
  849. def wait(self):
  850. self._act("WAIT")
  851. def mine(self):
  852. self.last_mining = current_turn
  853. self._act("MINE")
  854. def fire(self, x, y):
  855. self.last_fire = current_turn
  856. self._act("FIRE", x, y)
  857. class Barrel(Entity):
  858. def __init__(self, *args, **kwargs):
  859. super().__init__(*args, **kwargs)
  860. self.amount = 0
  861. self.dispersal = 0
  862. self.mine_threat = False
  863. self.ennemy_near = False
  864. def __repr__(self):
  865. return f"<Barrel {self.id}: pos=({self.x}, {self.y}), amount={self.amount}>"
  866. def update(self, x, y, *args):
  867. super().update(x, y)
  868. self.amount = int(args[0])
  869. class Mine(Entity):
  870. def __init__(self, *args, **kwargs):
  871. super().__init__(*args, **kwargs)
  872. self.ghost = False
  873. def __repr__(self):
  874. return f"<Mine {self.id}: pos=({self.x}, {self.y}), ghost={self.ghost}>"
  875. class Cannonball(Entity):
  876. def update(self, x, y, *args):
  877. super().update(x, y)
  878. self.sender, self.countdown = int(args[0]), int(args[1])
  879. entities = {}
  880. map_entity = {"SHIP": Ship,
  881. "BARREL": Barrel,
  882. "MINE": Mine,
  883. "CANNONBALL": Cannonball}
  884. Grid.preload()
  885. grid = Grid()
  886. ### *** Main Loop ***
  887. while True:
  888. seen = []
  889. current_turn += 1
  890. # <--- get input
  891. my_ship_count, entity_count = int(input()), int(input())
  892. ent_input = [input().split() for _ in range(entity_count)]
  893. # --->
  894. log(f"### TURN {current_turn}")
  895. log(">> Load input")
  896. # <--- load input
  897. previous_ent, entities = grid.entities, {}
  898. for e in ent_input:
  899. ent_id, ent_type, *data = e
  900. ent_id = int(ent_id)
  901. entities[ent_id] = grid.entities.get(ent_id, map_entity[ent_type](ent_id))
  902. entities[ent_id].update(*data)
  903. grid.load_entities(entities)
  904. # --->
  905. # log(f"Owned Ships: {grid.owned_ships}")
  906. log(f"Ennemy Ships: {grid.ennemy_ships}")
  907. log(f"Barrels: {grid.barrels}")
  908. # log(f"Mines: {grid.mines}")
  909. log(f"Cannonballs: {grid.cannonballs}")
  910. max_it = 9000 // len(grid.owned_ships)
  911. ### Acquire
  912. log("# Acquiring")
  913. # main objective
  914. while not all(s.objective for s in grid.owned_ships):
  915. try:
  916. acquired = sorted([(s, s.objectives.get()) for s in grid.owned_ships if not s.objective], key= lambda x: x[1].interest)
  917. for s, o in acquired:
  918. if not s.objective and not any(al.objective.target is o.target for al in s.allies if al.objective):
  919. s.objective = o
  920. except IndexError:
  921. break
  922. # targetted ennemy
  923. for s in grid.owned_ships:
  924. s.target_ennemy = s.ennemies.get()
  925. ### Plan
  926. log("# Planning")
  927. for ship in grid.owned_ships:
  928. log(f"---- ship {ship.id} ---")
  929. log(f"ship: {ship}")
  930. log(f"target: {ship.target_ennemy}")
  931. it_consumed = 0
  932. if ship.objective:
  933. ship.goto = ship.objective.target.pos
  934. elif ship.target_ennemy:
  935. ship.goto = grid.shooting_spot(ship, ship.target_ennemy.target, current=ship.goto)
  936. else:
  937. log("ERROR: No target")
  938. continue
  939. log(f"goto: {ship.goto}")
  940. ship.path, its = grid.path(ship.pos,
  941. ship.orientation,
  942. ship.goto,
  943. moving_costs=ship._moving_costs,
  944. inertia=ship.speed,
  945. limit=(max_it - it_consumed))
  946. it_consumed += its
  947. if ship.objective and ship.path and ship.path[-1] == ship.goto:
  948. while ship.objectives and len(ship.path) < 15:
  949. pos, d = ship.path[-1], ship.path[-1].orientation
  950. ship.objectives = ObjectivesQueue.re_eval(ship.objectives, pos, d)
  951. current_obj = ship.objectives.get()
  952. ship.objectives_next.append(current_obj)
  953. new_path, its = grid.path(pos, d,
  954. current_obj.target.pos,
  955. moving_costs=ship._moving_costs,
  956. limit=(max_it - it_consumed))
  957. it_consumed += its
  958. if new_path and new_path[-1] == current_obj.target.pos:
  959. ship.path += new_path
  960. else:
  961. break
  962. ship.plan_next_move()
  963. log(f"obj: {ship.objective}; next: {ship.objectives_next}")
  964. log(f"path: {ship.path}")
  965. log(f"next_move: {Ship.COMMANDS[ship.next_move]}")
  966. ### Process
  967. log("# Processing")
  968. for ship in grid.owned_ships:
  969. log(f"---- ship {ship.id} ---")
  970. if not ship.objective and not ship.target_ennemy:
  971. log("No target: wait")
  972. ship.wait()
  973. if ship.move():
  974. continue
  975. # no movement was required, can fire
  976. if ship.fire_at_will(ship.target_ennemy.target):
  977. continue
  978. # or mine
  979. if ship.mine_maybe():
  980. continue
  981. log("ERROR: Did not act, wait")
  982. ship.wait()
  983. # Changelog:
  984. # 2019-04-17
  985. # * if no available move, takes the less risky
  986. # * add the ennemy_near evaluation to the pre_eval method of GetBarrel and update it
  987. # * ignore probas < 20 in the moving cost calc (about ennemy next positions)
  988. # * get_shooting_spots: discurage direction changes
  989. # * increase shooting_spots distance from 5 to 7
  990. # * fire on mines, do not shoot mines that are in front of the ship
  991. # * increase the weight of a dead angle in GetBarrel.eval() from 18 to 36 <=> the weight of a distance of 6 needed to turn
  992. # * mines: increase moving_cost of neighbors if a cannon ball is going to hit the mine
  993. # * Improve the _follow_path algo
  994. # * anticipate the speed at 0 when blocked
  995. # * coeff 13 instead of 10 for the presence probability, making it impassable from env. 77% instead of 100%
  996. # * moving cost takes now in account the order of play of the ships
  997. # * increase the max length of the path from 10 to 15 when seeking for next objectives
  998. # * minor improvement to automove
  999. # * Avoid shooting barrels unless ennemy is nearest
  1000. # * include distance to ennemy in the eval of the shooting spot
  1001. # * (disactivated because of below) avoid consecutives direction changes
  1002. # * path: direction change moving cost changed from 10 to 20 because of the slowing effect