24_immunesys.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. #!/usr/bin/env python3
  2. import sys
  3. import re
  4. from operator import attrgetter
  5. class Units:
  6. def __init__(self, team, n, hp, dmg, dmgty, initiative, immune=[], weak=[]):
  7. self.team = team
  8. self.n = n
  9. self.hp = hp
  10. self.dmg = dmg
  11. self.dmgty = dmgty
  12. self.initiative = initiative
  13. self.immune = immune
  14. self.weak = weak
  15. def copy(self):
  16. return self.__class__(self.team, self.n, self.hp, self.dmg,
  17. self.dmgty, self.initiative, [i for i in self.immune],
  18. [w for w in self.weak])
  19. @classmethod
  20. def parse(cls, line, team):
  21. pat = r'(\d+) units each with (\d+) hit points (?:\(([^)]*)\) )?' \
  22. r'with an attack that does (\d+) (\w+) damage at initiative (\d+)'
  23. n, hp, special, dmg, dmgty, init = re.match(pat, line).groups()
  24. group = cls(team, int(n), int(hp), int(dmg), dmgty, int(init))
  25. if special:
  26. for spec in special.split('; '):
  27. ty, rest = spec.split(' to ')
  28. setattr(group, ty, rest.split(', '))
  29. return group
  30. def effective_power(self):
  31. return self.n * self.dmg
  32. def alive(self):
  33. return self.n > 0
  34. def pick_order(self):
  35. return self.effective_power(), self.initiative
  36. def damage_to(self, other):
  37. if self.dmgty in other.immune:
  38. return 0
  39. if self.dmgty in other.weak:
  40. return self.effective_power() * 2
  41. return self.effective_power()
  42. def attack(self, defender):
  43. nkilled = min(defender.n, self.damage_to(defender) // defender.hp)
  44. defender.n -= nkilled
  45. return nkilled
  46. def parse(f):
  47. team = re.match(r'(.+):', f.readline()).group(1)
  48. for line in sys.stdin:
  49. line = line.rstrip()
  50. if line.endswith(':'):
  51. team = line[:-1]
  52. elif line:
  53. yield Units.parse(line, team)
  54. def fight(groups):
  55. # target selection phase
  56. groups.sort(key=Units.pick_order, reverse=True)
  57. chosen = [False] * len(groups)
  58. picks = {}
  59. for attacker in groups:
  60. opts = []
  61. for i, defender in enumerate(groups):
  62. if defender.team != attacker.team and not chosen[i]:
  63. damage = attacker.damage_to(defender)
  64. if damage:
  65. opts.append((damage, defender.effective_power(), defender.initiative, i))
  66. if opts:
  67. target = max(opts)[-1]
  68. if not chosen[target]:
  69. chosen[target] = True
  70. picks[attacker] = groups[target]
  71. # attack phase
  72. groups.sort(key=attrgetter('initiative'), reverse=True)
  73. progress = False
  74. for attacker in groups:
  75. if attacker.alive() and attacker in picks:
  76. progress |= attacker.attack(picks[attacker]) > 0
  77. return progress, [g for g in groups if g.alive()]
  78. def simulate(initial, boost):
  79. groups = [g.copy() for g in initial]
  80. for g in groups:
  81. if g.team == 'Immune System':
  82. g.dmg += boost
  83. while len(set(u.team for u in groups)) > 1:
  84. progress, groups = fight(groups)
  85. if not progress:
  86. return 'Infection', 0
  87. return groups[0].team, sum(g.n for g in groups)
  88. initial = list(parse(sys.stdin))
  89. # part 1
  90. winner, units = simulate(initial, 0)
  91. assert winner == 'Infection'
  92. print(units)
  93. # part 2
  94. boost = 0
  95. while winner != 'Immune System':
  96. boost += 1
  97. winner, units = simulate(initial, boost)
  98. print(units)