last_crusade.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. '''
  2. @author: olivier.massot, 2019
  3. '''
  4. import heapq
  5. import sys
  6. import time
  7. t0 = time.time()
  8. def log(*msg):
  9. print("{} - ".format(str(time.time() - t0)[:5]), *msg, file=sys.stderr)
  10. TOP = 0
  11. LEFT = 1
  12. RIGHT = 2
  13. BOTTOM = 3
  14. DIRS = {TOP: (0, -1), BOTTOM: (0, 1), RIGHT: (1, 0), LEFT: (-1, 0)}
  15. PIVOT_UNCHANGED = 0
  16. PIVOT_LEFT = 1
  17. PIVOT_RIGHT = 2
  18. PIVOT_BACK = 3
  19. PIVOTS = {PIVOT_UNCHANGED: 0, PIVOT_LEFT: 1, PIVOT_RIGHT: 1, PIVOT_BACK: 2}
  20. COMMAND = {PIVOT_LEFT: "LEFT", PIVOT_RIGHT: "RIGHT"}
  21. SYM = {TOP: BOTTOM, BOTTOM: TOP, LEFT: RIGHT, RIGHT: LEFT}
  22. class Queue():
  23. def __init__(self):
  24. self.items = []
  25. def __bool__(self):
  26. return bool(self.items)
  27. def __repr__(self):
  28. return str(self.items)
  29. def values(self):
  30. return (v for _, v in self.items)
  31. def put(self, item, priority):
  32. heapq.heappush(self.items, (priority, item))
  33. def fput(self, item, priority):
  34. while priority in [p for p, _ in self.items]:
  35. priority += 1
  36. self.put(item, priority)
  37. def get(self):
  38. return heapq.heappop(self.items)[1]
  39. def fget(self):
  40. return heapq.heappop(self.items)
  41. class Position(tuple):
  42. def __new__(self, x, y, face):
  43. n = tuple.__new__(self, (x, y,face))
  44. return n
  45. @property
  46. def x(self):
  47. return self[0]
  48. @property
  49. def y(self):
  50. return self[1]
  51. @property
  52. def coords(self):
  53. return self[:2]
  54. @property
  55. def face(self):
  56. return self[2]
  57. class Item():
  58. def __init__(self, x, y, side):
  59. self.pos = Position(x, y, side)
  60. @classmethod
  61. def from_input(cls, x, y, strface):
  62. return cls(int(x), int(y), ["TOP", "LEFT", "RIGHT", "BOTTOM"].index(strface))
  63. class Indi(Item):
  64. graph = "I"
  65. class Rock(Item):
  66. graph = "R"
  67. class RockThreat(Item):
  68. graph = "T"
  69. class Piece():
  70. type = 0
  71. pipes = {}
  72. pivoted = {PIVOT_LEFT: 0, PIVOT_RIGHT: 0, PIVOT_BACK: 0}
  73. graph = ["...",
  74. "...",
  75. "..."]
  76. def __init__(self, x, y, locked=False):
  77. self.x = int(x)
  78. self.y = int(y)
  79. self.locked = locked
  80. def __repr__(self):
  81. return "<{}{}:{}>".format(self.type, "X"*self.locked, (self.x, self.y))
  82. def result(self, from_):
  83. return self.pipes[from_]
  84. def open_on(self, face_in):
  85. return face_in in self.pipes
  86. def apply(self, face_in):
  87. return self.pipes.get(face_in, None)
  88. def connects(self, face_in, face_out):
  89. return self.apply(face_in) == face_out
  90. class Piece0(Piece):
  91. pass
  92. class Piece1(Piece0):
  93. type = 1
  94. pipes = {TOP: BOTTOM, RIGHT: BOTTOM, LEFT: BOTTOM}
  95. pivoted = {PIVOT_LEFT: 1, PIVOT_RIGHT: 1, PIVOT_BACK: 1}
  96. graph = ["x x",
  97. " ",
  98. "x x"]
  99. class Piece2(Piece0):
  100. type = 2
  101. pipes = {LEFT: RIGHT, RIGHT: LEFT}
  102. pivoted = {PIVOT_LEFT: 3, PIVOT_RIGHT: 3, PIVOT_BACK: 2}
  103. graph = ["xxx",
  104. " ",
  105. "xxx"]
  106. class Piece3(Piece0):
  107. type = 3
  108. pipes = {TOP: BOTTOM}
  109. pivoted = {PIVOT_LEFT: 2, PIVOT_RIGHT: 2, PIVOT_BACK: 3}
  110. graph = ["x x",
  111. "x x",
  112. "x x"]
  113. class Piece4(Piece0):
  114. type = 4
  115. pipes = {TOP: LEFT, RIGHT: BOTTOM}
  116. pivoted = {PIVOT_LEFT: 5, PIVOT_RIGHT: 5, PIVOT_BACK: 4}
  117. graph = ["x x",
  118. " / ",
  119. "x x"]
  120. class Piece5(Piece0):
  121. type = 5
  122. pipes = {LEFT: BOTTOM, TOP: RIGHT}
  123. pivoted = {PIVOT_LEFT: 4, PIVOT_RIGHT: 4, PIVOT_BACK: 5}
  124. graph = ["x x",
  125. " \ ",
  126. "x x"]
  127. class Piece6(Piece0):
  128. type = 6
  129. pipes = {LEFT: RIGHT, RIGHT: LEFT}
  130. pivoted = {PIVOT_LEFT: 9, PIVOT_RIGHT: 7, PIVOT_BACK: 8}
  131. graph = ["x x",
  132. " ",
  133. "xxx"]
  134. class Piece7(Piece0):
  135. type = 7
  136. pipes = {TOP: BOTTOM, RIGHT: BOTTOM}
  137. pivoted = {PIVOT_LEFT: 6, PIVOT_RIGHT: 8, PIVOT_BACK: 9}
  138. graph = ["x x",
  139. "x ",
  140. "x x"]
  141. class Piece8(Piece0):
  142. type = 8
  143. pipes = {RIGHT: BOTTOM, LEFT: BOTTOM}
  144. pivoted = {PIVOT_LEFT: 7, PIVOT_RIGHT: 9, PIVOT_BACK: 6}
  145. graph = ["xxx",
  146. " ",
  147. "x x"]
  148. class Piece9(Piece0):
  149. type = 9
  150. pipes = {TOP: BOTTOM, LEFT: BOTTOM}
  151. pivoted = {PIVOT_LEFT: 8, PIVOT_RIGHT: 6, PIVOT_BACK: 7}
  152. graph = ["x x",
  153. " x",
  154. "x x"]
  155. class Piece10(Piece0):
  156. type = 10
  157. pipes = {TOP: LEFT}
  158. pivoted = {PIVOT_LEFT: 13, PIVOT_RIGHT: 11, PIVOT_BACK: 12}
  159. graph = ["x x",
  160. " x",
  161. "xxx"]
  162. class Piece11(Piece0):
  163. type = 11
  164. pipes = {TOP: RIGHT}
  165. pivoted = {PIVOT_LEFT: 10, PIVOT_RIGHT: 12, PIVOT_BACK: 13}
  166. graph = ["x x",
  167. "x ",
  168. "xxx"]
  169. class Piece12(Piece0):
  170. type = 12
  171. pipes = {RIGHT: BOTTOM}
  172. pivoted = {PIVOT_LEFT: 11, PIVOT_RIGHT: 13, PIVOT_BACK: 10}
  173. graph = ["xxx",
  174. "x ",
  175. "x x"]
  176. class Piece13(Piece0):
  177. type = 13
  178. pipes = {LEFT: BOTTOM}
  179. pivoted = {PIVOT_LEFT: 12, PIVOT_RIGHT: 10, PIVOT_BACK: 11}
  180. graph = ["xxx",
  181. " x",
  182. "x x"]
  183. classes = [Piece0, Piece1, Piece2, Piece3, Piece4,
  184. Piece5, Piece6, Piece7, Piece8, Piece9,
  185. Piece10, Piece11, Piece12, Piece13]
  186. def new_piece(x, y, v):
  187. x, y, v = int(x), int(y), int(v)
  188. type_, locked = abs(v), v < 0
  189. piece = classes[type_](x, y, locked)
  190. return piece
  191. def turn(piece, pivot):
  192. if pivot == PIVOT_UNCHANGED:
  193. return piece
  194. if piece.locked:
  195. raise Exception("can not pivot a locked piece")
  196. pivoted_type = piece.pivoted[pivot]
  197. if pivoted_type == piece.type:
  198. return piece
  199. return classes[pivoted_type](piece.x, piece.y)
  200. class PathNode():
  201. def __init__(self, position, parent=None, pivot=PIVOT_UNCHANGED):
  202. self.pos = position
  203. self.pivot = pivot
  204. self.parent = parent
  205. self.cost = 0
  206. self.round = 0
  207. self.require = []
  208. def __lt__(self, other):
  209. return self.pos < other.pos
  210. def __repr__(self):
  211. return f"<{self.pos}, pivot:{self.pivot}, cost:{self.cost}>"
  212. class Grid():
  213. def __init__(self, w, h, rows, x_exit):
  214. self.w = w
  215. self.h = h
  216. self.cells = {(x, y): new_piece(x, y, v) for y, row in enumerate(rows) for x, v in enumerate(row)}
  217. # add exit
  218. self.exit = (x_exit, h)
  219. self.cells[(x_exit, h)] = Piece1(x_exit, h, locked=True)
  220. self.indi = None
  221. self.rocks = []
  222. def get_cell(self, coords):
  223. return self.cells.get(coords, Piece0(*coords))
  224. def update(self, indi, rocks):
  225. self.indi = indi
  226. self.rocks = rocks
  227. self.include_threats()
  228. self.update_rocks_moves()
  229. @property
  230. def items(self):
  231. return [self.indi] + self.rocks
  232. def graph(self):
  233. res = "\n "
  234. res += "".join([f"{x:02d} " for x in range(self.w)])
  235. res += "\n"
  236. items = []
  237. if self.indi:
  238. items.append(self.indi)
  239. items += self.rocks
  240. for y in range(self.h):
  241. for i in range(3):
  242. if i ==0:
  243. res += "{:02d} ".format(y)
  244. else:
  245. res += " "
  246. for x in range(self.w):
  247. piece = grid.cells[(x, y)]
  248. line = list(piece.graph[i])
  249. for item in items:
  250. if item.pos.coords == (x, y):
  251. if item.pos.face == TOP and i == 0:
  252. line[1] = item.graph
  253. elif item.pos.face == RIGHT and i == 1:
  254. line[2] = item.graph
  255. elif item.pos.face == LEFT and i == 1:
  256. line[0] = item.graph
  257. elif item.pos.face == BOTTOM and i == 2:
  258. line[1] = item.graph
  259. if item.pos.x == x_exit and item.pos.y == h - 1 and i == 2:
  260. line[1] = "E"
  261. line = "".join(line)
  262. if piece.locked:
  263. line = line.replace("x", "#")
  264. res += line
  265. # res += "|"
  266. res += "\n"
  267. return res
  268. def item_at(self, x, y, side=None):
  269. return next((i.pos.coords == (x, y) and (side is None or i.pos.face == side) for i in self.items), None)
  270. @staticmethod
  271. def dist(p1, p2):
  272. xa, ya = p1
  273. xb, yb = p2
  274. return abs(xa - xb) + abs(ya - yb)
  275. def include_threats(self):
  276. threats = []
  277. for p, cell in self.cells.items():
  278. x, y = p
  279. if x == 0:
  280. if LEFT in cell.pipes and not self.item_at(x, y):
  281. threats.append(RockThreat(x, y, LEFT))
  282. elif x == (self.w - 1) and not self.item_at(x, y):
  283. if RIGHT in cell.pipes:
  284. threats.append(RockThreat(x, y, RIGHT))
  285. if y == 0:
  286. if TOP in cell.pipes and not self.item_at(x, y):
  287. threats.append(RockThreat(x, y, TOP))
  288. self.rocks += threats
  289. def update_rocks_moves(self):
  290. self.trajectories = []
  291. for rock in self.rocks:
  292. trajectory = self.trajectory(rock)
  293. is_threat = isinstance(rock, RockThreat)
  294. if is_threat:
  295. # to simulate the 1 turn delay before arriving
  296. trajectory.insert(0, rock.pos)
  297. stops = []
  298. for i, pos in enumerate(trajectory):
  299. piece = self.cells[pos.coords]
  300. if piece.locked or i == 0:
  301. continue
  302. for pivot in [PIVOT_LEFT, PIVOT_RIGHT, PIVOT_BACK]:
  303. pivoted = turn(piece, pivot)
  304. if not pivoted.open_on(pos.face):
  305. stop = (i, pivot)
  306. stops.append(stop)
  307. break
  308. self.trajectories.append((trajectory, stops, is_threat))
  309. log(self.trajectories)
  310. def after(self, position, pivot=PIVOT_UNCHANGED):
  311. x, y, face = position
  312. piece = self.cells[(x, y)]
  313. piece = turn(piece, pivot)
  314. face_out = piece.apply(face)
  315. if face_out is None:
  316. return None
  317. dx, dy = DIRS[face_out]
  318. position = Position(x+dx, y+dy, SYM[face_out])
  319. return position
  320. def trajectory(self, item, with_start=True):
  321. path = []
  322. if with_start:
  323. path.append(item.pos)
  324. current = self.after(item.pos)
  325. while current is not None:
  326. path.append(current)
  327. next_pos = self.after(current)
  328. if not next_pos or next_pos.coords == self.exit or not self.get_cell(next_pos.coords).open_on(next_pos.face):
  329. break
  330. current = self.after(current)
  331. return path
  332. def path(self):
  333. subject = self.indi
  334. nodes = Queue()
  335. origin = PathNode(subject.pos)
  336. nodes.put(origin, 0)
  337. its, break_on = 0, 6000
  338. broken = False
  339. while nodes:
  340. current = nodes.get()
  341. its += 1
  342. if its > break_on:
  343. log("pathfinding broken")
  344. broken = True
  345. if broken or current.pos.coords == self.exit:
  346. path = []
  347. previous = current.parent
  348. while previous:
  349. if previous != origin:
  350. path.insert(0, previous)
  351. previous = previous.parent
  352. return path
  353. next_pos = self.after(current.pos, current.pivot)
  354. if next_pos is None:
  355. log(f"! Next piece doesn't exist: ", current.pos, current.pivot)
  356. continue
  357. next_cell = self.get_cell(next_pos.coords)
  358. requirements = []
  359. deadend = False
  360. for trajectory, stops, is_threat in self.trajectories:
  361. if len(trajectory) > (current.round + 1) and trajectory[(current.round + 1)].coords == next_pos.coords:
  362. alternatives = [Action(trajectory[i], pivot) for i, pivot in stops if 0 < i <= (current.round + 1)]
  363. require = next(iter(alternatives), None)
  364. if require is None:
  365. if not is_threat: #unstoppable rock
  366. deadend = True
  367. log("node is deadend: ", next_pos.coords)
  368. break
  369. else:
  370. continue
  371. if len(alternatives) == 1:
  372. require.last_chance = True
  373. requirements.append(require)
  374. if deadend:
  375. continue
  376. for pivot, rotations in PIVOTS.items():
  377. if rotations > 0:
  378. if next_cell.locked:
  379. continue
  380. elif next_cell.pivoted[pivot] == next_cell.type:
  381. # useless rotation
  382. continue
  383. if current is origin and rotations > 1:
  384. continue
  385. pivoted = turn(next_cell, pivot)
  386. if pivoted.open_on(next_pos.face):
  387. node = PathNode(next_pos, current, pivot)
  388. node.cost = current.cost + rotations
  389. node.round = current.round + 1
  390. node.require = requirements
  391. priority = node.cost
  392. nodes.put(node, priority)
  393. return []
  394. def apply(self, action):
  395. self.cells[action.coords] = turn(self.cells[action.coords], action.pivot)
  396. class Action():
  397. def __init__(self, coords, pivot, last_chance=False):
  398. self.coords = coords[:2]
  399. self.pivot = pivot
  400. self.last_chance = last_chance
  401. def command(self):
  402. return "{} {} {}".format(*self.coords, COMMAND[self.pivot])
  403. def __repr__(self):
  404. return f"<{self.command()}, last_chance: {self.last_chance}>"
  405. def __lt__(self, other):
  406. return self.last_chance and not other.last_chance or self.coords < other.coords
  407. # Get input
  408. w, h = [int(i) for i in input().split()]
  409. rows = [input().split() for _ in range(h)]
  410. x_exit = int(input())
  411. # instanciate grid
  412. grid = Grid(w, h, rows, x_exit)
  413. while True:
  414. # get input
  415. indi = Indi.from_input(*input().split())
  416. rocks = [Rock.from_input(*input().split()) for _ in range(int(input()))]
  417. # update grid
  418. grid.update(indi, rocks)
  419. log(grid.graph())
  420. path = grid.path()
  421. log(path)
  422. if path:
  423. plan = []
  424. requirements = []
  425. for i, node in enumerate(path):
  426. if node.pivot == PIVOT_BACK:
  427. action = Action(node.pos, PIVOT_RIGHT, i <= 1)
  428. log("Node requirement: ", action)
  429. if i <= 1:
  430. requirements.insert(0, action)
  431. else:
  432. requirements.append(action)
  433. for action in node.require:
  434. log("Node requirement: ", action)
  435. if action.last_chance:
  436. requirements.insert(0, action)
  437. else:
  438. requirements.append(action)
  439. for i, node in enumerate(path):
  440. if node.pivot in [PIVOT_LEFT, PIVOT_RIGHT]:
  441. plan.append(Action(node.pos, node.pivot))
  442. elif node.pivot == PIVOT_BACK:
  443. plan.append(Action(node.pos, PIVOT_RIGHT))
  444. else:
  445. if requirements:
  446. action = requirements.pop(0)
  447. plan.append(action)
  448. log(plan)
  449. else:
  450. log("no path, use previous plan")
  451. try:
  452. action = plan.pop(0)
  453. grid.apply(action)
  454. print(action.command())
  455. except IndexError:
  456. print("WAIT")