15_goblins.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. #!/usr/bin/env python3
  2. import sys
  3. from operator import attrgetter
  4. from heapq import heappush, heappop
  5. from time import sleep
  6. SPACE, WALL = 0, 1
  7. GOBLINS, ELVES = 0, 1
  8. class Unit:
  9. counts = [0, 0]
  10. def __init__(self, pos, team, ap):
  11. self.pos = pos
  12. self.team = team
  13. self.hp = 200
  14. self.ap = ap
  15. self.counts[team] += 1
  16. def neighbouring_enemies(self):
  17. for nb in (self.pos - w, self.pos - 1, self.pos + 1, self.pos + w):
  18. u = grid[nb]
  19. if u not in (SPACE, WALL) and u.team != self.team and u.hp > 0:
  20. yield u
  21. def hit(self, ap):
  22. self.hp -= ap
  23. if self.hp <= 0:
  24. grid[self.pos] = SPACE
  25. self.counts[self.team] -= 1
  26. if self.counts[self.team] == 0:
  27. raise BattleLost(self.team)
  28. def fight(self):
  29. enemies = list(self.neighbouring_enemies())
  30. if enemies:
  31. min(enemies, key=attrgetter('hp', 'pos')).hit(self.ap)
  32. return len(enemies) > 0
  33. def move(self):
  34. dest = self.step_to_nearest_enemy()
  35. if dest is None:
  36. return False
  37. grid[self.pos] = SPACE
  38. grid[dest] = self
  39. self.pos = dest
  40. return True
  41. def step_to_nearest_enemy(self):
  42. source = self.pos
  43. dist = {source: 0}
  44. prev = {}
  45. Q = [(0, source)]
  46. visited = {source}
  47. while Q:
  48. udist, u = heappop(Q)
  49. for v in (u - w, u - 1, u + 1, u + w):
  50. if grid[v] == SPACE and v not in visited:
  51. visited.add(v)
  52. alt = udist + 1
  53. known = dist.get(v, None)
  54. if known is None or alt < known:
  55. dist[v] = alt
  56. prev[v] = u
  57. heappush(Q, (alt, v))
  58. def adjacent_enemies(v):
  59. if v in dist and grid[v] == SPACE:
  60. for nb in (v - w, v - 1, v + 1, v + w):
  61. u = grid[nb]
  62. if u not in (WALL, SPACE) and u.team != self.team and u.hp > 0:
  63. yield u
  64. shortest = {}
  65. for v, d in dist.items():
  66. for u in adjacent_enemies(v):
  67. step = v
  68. pathlen = 1
  69. while prev[step] != source:
  70. step = prev[step]
  71. pathlen += 1
  72. entry = pathlen, v, step
  73. if u not in shortest or entry < shortest[u]:
  74. shortest[u] = entry
  75. if shortest:
  76. return min(shortest.values())[-1]
  77. class BattleLost(Exception):
  78. def __init__(self, team):
  79. self.team = team
  80. def parse(f):
  81. grid = []
  82. w = 0
  83. for y, line in enumerate(f):
  84. for x, c in enumerate(line.rstrip()):
  85. if c == '#':
  86. grid.append(WALL)
  87. elif c in '.':
  88. grid.append(SPACE)
  89. else:
  90. grid.append(Unit(y * w + x, ELVES if c == 'E' else GOBLINS, 3))
  91. w = x + 1
  92. return grid, w
  93. def show():
  94. def red(text):
  95. return '\x1b[31m' + text + '\x1b[0m'
  96. def green(text):
  97. return '\x1b[32m' + text + '\x1b[0m'
  98. h = len(grid) // w
  99. for y in range(h):
  100. line = ''
  101. hps = []
  102. for x in range(w):
  103. cell = grid[y * w + x]
  104. if cell not in (WALL, SPACE):
  105. t = green('E') if cell.team == ELVES else red('G')
  106. hps.append('%s(%d)' % (t, cell.hp))
  107. line += t
  108. else:
  109. line += '#' if cell == WALL else ' '
  110. if hps:
  111. line += ' ' + ', '.join(hps)
  112. print(line)
  113. def simulate(ap, delay):
  114. global units
  115. rounds = 0
  116. clear = '\033c'
  117. try:
  118. if delay:
  119. print(clear + 'after 0 rounds with', ap, 'ap:')
  120. show()
  121. while True:
  122. for unit in units:
  123. if unit.hp > 0 and not unit.fight() and unit.move():
  124. unit.fight()
  125. units = sorted((u for u in units if u.hp > 0), key=attrgetter('pos'))
  126. rounds += 1
  127. if delay:
  128. print(clear + 'after', rounds, 'rounds with', ap, 'ap:')
  129. show()
  130. sleep(delay)
  131. except BattleLost as lost:
  132. if delay:
  133. print(clear + 'after', rounds, 'rounds with', ap, 'ap:')
  134. show()
  135. return lost.team, rounds, sum(u.hp for u in units if u.hp > 0)
  136. def reset(elf_ap):
  137. Unit.counts = [0, 0]
  138. grid = []
  139. units = []
  140. for i, cell in enumerate(startgrid):
  141. if cell in (SPACE, WALL):
  142. grid.append(cell)
  143. else:
  144. unit = Unit(i, cell.team, elf_ap if cell.team == ELVES else 3)
  145. grid.append(unit)
  146. units.append(unit)
  147. return grid, units
  148. def report(loser, rounds, hp, ap):
  149. print('Combat ends after', rounds, 'full rounds with', ap, 'attack power:')
  150. winner = 'Goblins' if loser == ELVES else 'Elves'
  151. print(winner, 'win with', hp, 'hit points left')
  152. print('Outcome:', rounds, '*', hp, '=', rounds * hp)
  153. # part 1
  154. elf_ap = 3
  155. delay = int(sys.argv[1]) / 1000 if len(sys.argv) == 2 else None
  156. startgrid, w = parse(sys.stdin)
  157. grid, units = reset(elf_ap)
  158. startelves = sum(u.team == ELVES for u in units)
  159. outcome = outcome3 = simulate(elf_ap, delay)
  160. # part 2
  161. numelves = 0
  162. while numelves != startelves:
  163. elf_ap += 1
  164. grid, units = reset(elf_ap)
  165. outcome = simulate(elf_ap, delay)
  166. numelves = sum(u.team == ELVES for u in units)
  167. report(*outcome3, 3)
  168. print()
  169. print('Elves need', elf_ap, 'attack power not to let any elf die:')
  170. report(*outcome, elf_ap)