| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- #!/usr/bin/env python3
- import sys
- import re
- from operator import attrgetter
- class Units:
- def __init__(self, team, n, hp, dmg, dmgty, initiative, immune=[], weak=[]):
- self.team = team
- self.n = n
- self.hp = hp
- self.dmg = dmg
- self.dmgty = dmgty
- self.initiative = initiative
- self.immune = immune
- self.weak = weak
- def copy(self):
- return self.__class__(self.team, self.n, self.hp, self.dmg,
- self.dmgty, self.initiative, [i for i in self.immune],
- [w for w in self.weak])
- @classmethod
- def parse(cls, line, team):
- pat = r'(\d+) units each with (\d+) hit points (?:\(([^)]*)\) )?' \
- r'with an attack that does (\d+) (\w+) damage at initiative (\d+)'
- n, hp, special, dmg, dmgty, init = re.match(pat, line).groups()
- group = cls(team, int(n), int(hp), int(dmg), dmgty, int(init))
- if special:
- for spec in special.split('; '):
- ty, rest = spec.split(' to ')
- setattr(group, ty, rest.split(', '))
- return group
- def effective_power(self):
- return self.n * self.dmg
- def alive(self):
- return self.n > 0
- def pick_order(self):
- return self.effective_power(), self.initiative
- def damage_to(self, other):
- if self.dmgty in other.immune:
- return 0
- if self.dmgty in other.weak:
- return self.effective_power() * 2
- return self.effective_power()
- def attack(self, defender):
- nkilled = min(defender.n, self.damage_to(defender) // defender.hp)
- defender.n -= nkilled
- return nkilled
- def parse(f):
- team = re.match(r'(.+):', f.readline()).group(1)
- for line in sys.stdin:
- line = line.rstrip()
- if line.endswith(':'):
- team = line[:-1]
- elif line:
- yield Units.parse(line, team)
- def fight(groups):
- # target selection phase
- groups.sort(key=Units.pick_order, reverse=True)
- chosen = [False] * len(groups)
- picks = {}
- for attacker in groups:
- opts = []
- for i, defender in enumerate(groups):
- if defender.team != attacker.team and not chosen[i]:
- damage = attacker.damage_to(defender)
- if damage:
- opts.append((damage, defender.effective_power(), defender.initiative, i))
- if opts:
- target = max(opts)[-1]
- if not chosen[target]:
- chosen[target] = True
- picks[attacker] = groups[target]
- # attack phase
- groups.sort(key=attrgetter('initiative'), reverse=True)
- progress = False
- for attacker in groups:
- if attacker.alive() and attacker in picks:
- progress |= attacker.attack(picks[attacker]) > 0
- return progress, [g for g in groups if g.alive()]
- def simulate(initial, boost):
- groups = [g.copy() for g in initial]
- for g in groups:
- if g.team == 'Immune System':
- g.dmg += boost
- while len(set(u.team for u in groups)) > 1:
- progress, groups = fight(groups)
- if not progress:
- return 'Infection', 0
- return groups[0].team, sum(g.n for g in groups)
- initial = list(parse(sys.stdin))
- # part 1
- winner, units = simulate(initial, 0)
- assert winner == 'Infection'
- print(units)
- # part 2
- boost = 0
- while winner != 'Immune System':
- boost += 1
- winner, units = simulate(initial, boost)
- print(units)
|