2018-12-24 - Immune System Simulator 20XX
(original .ipynb)puzzle_input_lines = open("puzzle_input/day24.txt").readlines() test_input_lines = """Immune System: 17 units each with 5390 hit points (weak to radiation, bludgeoning) with an attack that does 4507 fire damage at initiative 2 989 units each with 1274 hit points (immune to fire; weak to bludgeoning, slashing) with an attack that does 25 slashing damage at initiative 3 Infection: 801 units each with 4706 hit points (weak to radiation) with an attack that does 116 bludgeoning damage at initiative 1 4485 units each with 2961 hit points (immune to radiation; weak to fire, cold) with an attack that does 12 slashing damage at initiative 4""".split("\n")
from collections import defaultdict import re # I don't like using classes to solve these problems, but this was the simplest way class UnitGroup: def __init__(self, identifier, army, unit_group, boost=0): units, hit_points, strengths, weaknesses, damage, damage_type, initiative = unit_group self.identifier = f"{army}.{identifier}" self.army = army self.units = units self.hit_points = hit_points self.strengths = strengths self.weaknesses = weaknesses self.damage = damage + boost self.damage_type = damage_type self.initiative = initiative def effective_power(self): return self.units * self.damage def select_target(self, unit_groups, targets): highest_damage = 0 selected_target = None for other in sorted(unit_groups, key=effective_power_and_initiative, reverse=True): if self.army == other.army or other.identifier in targets.values() or other.units < 1: continue damage = self.calculate_damage(other) if damage > highest_damage: selected_target = other.identifier highest_damage = damage return selected_target def calculate_damage(self, enemy): total_damage = self.units * self.damage if enemy.strengths and self.damage_type in enemy.strengths: total_damage *= 0 if enemy.weaknesses and self.damage_type in enemy.weaknesses: total_damage *= 2 return total_damage def attack(self, enemy): if self.units < 1: return total_damage = self.calculate_damage(enemy) killed_units = min(enemy.units, total_damage // enemy.hit_points) enemy.units -= killed_units return killed_units def parse_strengths_weaknesses(weakness_str): strengths, weaknesses = None, None if weakness_str is not None: weakness_str = weakness_str.lstrip("(") weakness_str = weakness_str.rstrip(")") parts = weakness_str.split("; ") weak_str = "weak to" immune_str = "immune to" for part in parts: if part.startswith(weak_str): weaknesses = part.lstrip(weak_str).split(", ") elif part.startswith(immune_str): strengths = part.lstrip(immune_str).split(", ") return strengths, weaknesses def parse_input_line(input_line): if army_match := re.match("^(.+)\:$", input_line): return army_match.group(1), None elif unit_group_match := re.match("^(\d+) units each with (\d+) hit points (\(.+\))?\s?with an attack that does (\d+) (\w+) damage at initiative (\d+)$", input_line): units = int(unit_group_match.group(1)) hit_points = int(unit_group_match.group(2)) strengths, weaknesses = parse_strengths_weaknesses(unit_group_match.group(3)) damage = int(unit_group_match.group(4)) damage_type = unit_group_match.group(5) initiative = int(unit_group_match.group(6)) return None, (units, hit_points, strengths, weaknesses, damage, damage_type, initiative) else: return None, None def parse_input(input_lines, boost=0): current_army = None unit_groups = [] for i, line in enumerate(input_lines): army, unit_group = parse_input_line(line) if army: current_army = army elif unit_group: if current_army == "Immune System": unit_groups.append(UnitGroup(i, current_army, unit_group, boost)) else: unit_groups.append(UnitGroup(i, current_army, unit_group)) else: pass return unit_groups def effective_power(unit_group): return unit_group.effective_power() def effective_power_and_initiative(unit_group): return (unit_group.effective_power(), unit_group.initiative) def initiative(unit_group): return unit_group.initiative def get_unit_group(unit_groups, unit_group_id): for unit_group in unit_groups: if unit_group.identifier == unit_group_id: return unit_group return None def armies_remaining(unit_groups): armies = set() for unit_group in unit_groups: if unit_group.units > 0: armies.add(unit_group.army) return len(armies) def fight(unit_groups): killed_units = 1 while killed_units > 0: killed_units = 0 # target selection targets = {} for unit_group in sorted(unit_groups, key=effective_power_and_initiative, reverse=True): target = unit_group.select_target(unit_groups, targets) if target is not None: targets[unit_group.identifier] = target # attacking for unit_group in sorted(unit_groups, key=initiative, reverse=True): if unit_group.units < 1 or unit_group.identifier not in targets: continue other = get_unit_group(unit_groups, targets[unit_group.identifier]) killed_units += unit_group.attack(other) def part_one(input_lines): unit_groups = parse_input(input_lines) fight(unit_groups) return sum(ug.units for ug in unit_groups) assert 5216 == part_one(test_input_lines) print(part_one(puzzle_input_lines))
14854
def remaining_units(unit_groups, army): total = 0 for unit_group in unit_groups: if unit_group.army == army: total += unit_group.units return total def part_two(input_lines): unit_groups = parse_input(input_lines) boost = 1 fight(unit_groups) immune_units = remaining_units(unit_groups, "Immune System") infection_units = remaining_units(unit_groups, "Infection") while immune_units == 0 or infection_units > 0: # I had a much-faster binary search, but this is cleaner to read :) boost += 1 unit_groups = parse_input(input_lines, boost) fight(unit_groups) immune_units = remaining_units(unit_groups, "Immune System") infection_units = remaining_units(unit_groups, "Infection") return sum(ug.units for ug in unit_groups) assert 51 == part_two(test_input_lines) print(part_two(puzzle_input_lines))
3467