128 lines
3.3 KiB
Python
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)
|