|
|
@@ -0,0 +1,118 @@
|
|
|
+#!/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)
|