Advent of Code Solutions

I have set myself a personal challenge to try and get up to date with advent of code, a set of programming challenges released each December.

Each day has 2 related problems, for a total of 50 problems per year. Each button below will show my code solving that problem. Green buttons where I have solved both problems of the day, orange where I have only solved the first one and red where I haven't solved either.

2015 2016 2017 2018 2019 2020 2021 2022 2023 2024
Not started
Part 1 completed
Both parts completed
from util.input import get_input
from dataclasses import dataclass
from itertools import combinations

@dataclass
class Item:

    name: str
    cost: int
    damage: int
    armour: int

    def __repr__(self):
        return self.name

@dataclass
class Actor:
    health: int
    damage: int
    armour: int
    


WEAPONS = [
    Item('Dagger', 8, 4, 0),
    Item('Shortsword', 10,5,0),
    Item('Warhammer', 25, 6, 0),
    Item('Longsword', 40, 7, 0),
    Item('Greataxe', 74, 8, 0)
]

ARMOUR = [
    Item('Leather', 13, 0, 1),
    Item('Chainmail', 31, 0, 2),
    Item('Splintmail', 53, 0, 3),
    Item('Bandedmail', 75, 0, 4),
    Item('Platemail', 102, 0, 5)
]

RINGS = [
    Item('dam1', 25, 1, 0),
    Item('dam2', 50, 2, 0),
    Item('dam3', 100, 3, 0),
    Item('def1', 20, 0, 1),
    Item('def2', 40, 0, 2),
    Item('def3', 80, 0, 3),
]

def get_possible_loadouts():
    possible_loadouts = []
    for weapon in WEAPONS:
        possible_loadouts.append([weapon])
        for armour in ARMOUR:
            possible_loadouts.append([weapon, armour])
            for n_rings in [1,2]:
                for ring_combo in combinations(RINGS, n_rings):
                    possible_loadouts.append([weapon] + list(ring_combo))
                    possible_loadouts.append([weapon, armour] + list(ring_combo))
    return possible_loadouts


def part_1():
    
    puzzle_input = get_input(2015, 21)

    boss_specs = []
    for line in puzzle_input.splitlines():
        boss_specs.append(int(line.split(':')[-1].strip()))

    possible_loadouts = get_possible_loadouts()

    loadouts = sorted(possible_loadouts, key=lambda items: sum(item.cost for item in items))


    for loadout in loadouts:
        player = Actor(
            100,
            sum(item.damage for item in loadout),
            sum(item.armour for item in loadout)
        )
        boss = Actor(*boss_specs)

        player_turn = True
        while player.health > 0 and boss.health > 0:

            if player_turn:
                boss.health -= max(1, player.damage - boss.armour)
                player_turn = False
            else:
                player.health -= max(1, boss.damage - player.armour)
                player_turn = True            

        if not player_turn:
            return sum(item.cost for item in loadout)
    

def part_2():
    puzzle_input = get_input(2015, 21)

    boss_specs = []
    for line in puzzle_input.splitlines():
        boss_specs.append(int(line.split(':')[-1].strip()))

    possible_loadouts = get_possible_loadouts()

    loadouts = sorted(possible_loadouts, key=lambda items: sum(item.cost for item in items), reverse=True)

    for loadout in loadouts:
        player = Actor(
            100,
            sum(item.damage for item in loadout),
            sum(item.armour for item in loadout)
        )
        boss = Actor(*boss_specs)

        player_turn = True
        while player.health > 0 and boss.health > 0:

            if player_turn:
                boss.health -= max(1, player.damage - boss.armour)
                player_turn = False
            else:
                player.health -= max(1, boss.damage - player.armour)
                player_turn = True            

        if player_turn:
            return sum(item.cost for item in loadout)