15_goblins.py 5.7 KB

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