AdventOfCode/Python/2018/24.py

128 lines
3.3 KiB
Python

from lib import *
input = read_input(2018, 24)
@dataclass
class Group:
army: int
units: int
hp: int
ap: int
at: str
init: int
weak: set[str]
immune: set[str]
def __hash__(self):
return id(self)
@staticmethod
def parse(army, line, boost=0):
units, hp, _, extra, ap, at, init = re.match(
r"^(\d+) units each with (\d+) hit points( \((.*)\))? with an attack that does (\d+) (\w+) damage at initiative (\d+)$",
line,
).groups()
weak = set()
immune = set()
for part in extra.split("; ") if extra else []:
t, _, *xs = part.split()
{"weak": weak, "immune": immune}[t].update(x.strip(",") for x in xs)
return Group(army, int(units), int(hp), int(ap) + boost, at, int(init), weak, immune)
@property
def ep(self):
return self.units * self.ap
@property
def dead(self):
return self.units <= 0
def calc_damage(self, target):
if self.at in target.immune:
return 0
mul = 2 if self.at in target.weak else 1
return self.ep * mul
def attack(self, target):
damage = self.calc_damage(target) // target.hp
target.units -= damage
return damage
immune, infect = [
[Group.parse(i, group) for group in army.splitlines()[1:]] for i, army in enumerate(input.split("\n\n"))
]
while immune and infect:
targets = {}
imm_att = set(immune)
inf_att = set(infect)
for group in sorted(immune + infect, key=lambda g: (-g.ep, -g.init)):
attackable = [inf_att, imm_att][group.army]
if not attackable:
continue
target = max(attackable, key=lambda g: (group.calc_damage(g), g.ep, g.init))
if not group.calc_damage(target):
continue
attackable.remove(target)
targets[group] = target
for group, target in sorted(targets.items(), key=lambda a: -a[0].init):
if group.dead:
continue
group.attack(target)
immune, infect = [[g for g in x if not g.dead] for x in [immune, infect]]
print(sum(g.units for g in immune + infect))
def test(boost):
immune, infect = [
[Group.parse(i, group, boost if i == 0 else 0) for group in army.splitlines()[1:]]
for i, army in enumerate(input.split("\n\n"))
]
while immune and infect:
targets = {}
imm_att = set(immune)
inf_att = set(infect)
for group in sorted(immune + infect, key=lambda g: (-g.ep, -g.init)):
attackable = [inf_att, imm_att][group.army]
if not attackable:
continue
target = max(attackable, key=lambda g: (group.calc_damage(g), g.ep, g.init))
if not group.calc_damage(target):
continue
attackable.remove(target)
targets[group] = target
ok = False
for group, target in sorted(targets.items(), key=lambda a: -a[0].init):
if group.dead:
continue
if group.attack(target):
ok = True
if not ok:
break
immune, infect = [[g for g in x if not g.dead] for x in [immune, infect]]
if infect:
return None
return sum(g.units for g in immune)
boost = 0
while not (out := test(boost)):
boost += 1
print(out)