15_goblins.py 5.4 KB

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