zerosleeps

Since 2010

Advent of Code 2022 day 14

Completed day 14 today as well. It’s hideously inefficient but the code fits my brain, so adapting it for part 2 was nice and quick and worked first time 🥳

from itertools import pairwise
from unittest import TestCase
from utils import get_raw_input


class Cave:
    def __init__(self, raw_input):
        self.build(raw_input)
        self.sand = set()
        self.overflowing = False

    def plot_rocks(self, x1, y1, x2, y2):
        if y1 == y2:
            return [(i, y1) for i in range(min([x1, x2]), max([x1, x2]) + 1)]
        else:
            return [(x1, i) for i in range(min([y1, y2]), max([y1, y2]) + 1)]

    def build(self, raw_input):
        self.rocks = set()
        for line in raw_input:
            for pair in pairwise(line.split(" -> ")):
                self.rocks.update(
                    self.plot_rocks(
                        int(pair[0].split(",")[0]),
                        int(pair[0].split(",")[1]),
                        int(pair[1].split(",")[0]),
                        int(pair[1].split(",")[1]),
                    )
                )

    @property
    def boundaries(self):
        # y_min is highest rock, y_max is lowest rock
        return {
            "x_min": min([rock[0] for rock in self.rocks]),
            "x_max": max([rock[0] for rock in self.rocks]),
            "y_min": min([rock[1] for rock in self.rocks]),
            "y_max": max([rock[1] for rock in self.rocks]),
        }

    def drop_sand(self, floor=False):
        sand_position = (500, 0)
        while True:
            if (
                not floor
                and (
                    sand_position[1] > self.boundaries["y_max"]
                    or sand_position[0] < self.boundaries["x_min"]
                    or sand_position[0] > self.boundaries["x_max"]
                )
            ) or (floor and (500, 0) in self.sand):
                # Out of bounds
                self.overflowing = True
                return
            elif (
                (sand_position[0], sand_position[1] + 1) not in self.rocks
                and (sand_position[0], sand_position[1] + 1) not in self.sand
                and (
                    not floor
                    or (floor and sand_position[1] + 1 < self.boundaries["y_max"] + 2)
                )
            ):
                # Can fall straight down
                sand_position = (sand_position[0], sand_position[1] + 1)
            elif (
                (sand_position[0] - 1, sand_position[1] + 1) not in self.rocks
                and (sand_position[0] - 1, sand_position[1] + 1) not in self.sand
                and (
                    (not floor)
                    or (floor and sand_position[1] + 1 < self.boundaries["y_max"] + 2)
                )
            ):
                # Can fall down-and-left
                sand_position = (sand_position[0] - 1, sand_position[1] + 1)
            elif (
                (sand_position[0] + 1, sand_position[1] + 1) not in self.rocks
                and (sand_position[0] + 1, sand_position[1] + 1) not in self.sand
                and (
                    (not floor)
                    or (floor and sand_position[1] + 1 < self.boundaries["y_max"] + 2)
                )
            ):
                # Can fall down-and-right
                sand_position = (sand_position[0] + 1, sand_position[1] + 1)
            else:
                # Can't move
                self.sand.add(sand_position)
                return


class TestExamples(TestCase):
    def setUp(self):
        self.example_input = [
            "498,4 -> 498,6 -> 496,6",
            "503,4 -> 502,4 -> 502,9 -> 494,9",
        ]
        self.cave = Cave(self.example_input)

    def test_part_one(self):
        self.assertEqual(run(self.cave), 24)

    def test_part_two(self):
        self.assertEqual(run(self.cave, True), 93)


def run(cave, floor=False):
    while not cave.overflowing:
        cave.drop_sand(floor)
    return len(cave.sand)


if __name__ == "__main__":
    print(f"Part one: {run(Cave(get_raw_input('day_14_input.txt')))}")
    print(f"Part two: {run(Cave(get_raw_input('day_14_input.txt')), True)}")

Advent of Code 2022 day 13 is done

I figured out my problem: I wasn’t correctly handing the scenario where a list had been exhausted with no result. I was incorrectly assuming that when either side of the list had been exhausted, a result must have been obtained.

Final code:

from functools import cmp_to_key
from utils import get_raw_input_as_str

def parse_raw_input(raw_input):
    return [[eval(packet) for packet in pair.splitlines()] for pair in raw_input.split("\n\n")]

def compare(left, right):
    if type(left) == type(right) == list:
        result = None
        i = 0
        while result is None:
            if len(left) != len(right) and len(left) <= i:
                return 1
            elif len(left) != len(right) and len(right) <= i:
                return -1
            elif len(left) == i and len(right) == i:
                return None
            else:
                result = compare(left[i], right[i])
                i += 1
        return result
    elif type(left) == type(right) == int:
        if left < right:
            return 1
        elif left > right:
            return -1
        else:
            return None
    elif type(left) == int and type(right) != int:
        return compare([left], right)
    elif type(right) == int and type(left) != int:
        return compare(left, [right])
    else:
        raise Exception

def part_one(input):
    return sum([i + 1 for i, pair in enumerate(input) if compare(pair[0], pair[1]) == 1])

def part_two(input):
    flattened_input = [packet for pair in input for packet in pair] + [[[2]]] + [[[6]]]
    sorted_packets = sorted(flattened_input, key=cmp_to_key(compare), reverse=True)
    return (sorted_packets.index([[2]]) + 1) * (sorted_packets.index([[6]]) + 1)

if __name__ == '__main__':
    print(f"Part one: {part_one(parse_raw_input(get_raw_input_as_str('day_13_input.txt')))}")
    print(f"Part one: {part_two(parse_raw_input(get_raw_input_as_str('day_13_input.txt')))}")

Advent of Code 2022 day 13

Not-quite-working solution for Advent of Code 2022 day 13. Works for both parts of the example input, doesn’t work for my actual input. I’ll come back to this.

from functools import cmp_to_key
from utils import get_raw_input_as_str

def parse_raw_input(raw_input):
    return [[eval(packet) for packet in pair.splitlines()] for pair in raw_input.split("\n\n")]

def compare(left, right):
    if type(left) == type(right) == int:
        if left < right:
            return 1
        elif left > right:
            return -1
        else:
            return None
    elif type(left) == type(right) == list:
        result = None
        i = 0
        while result is None:
            if len(left) <= i and len(right) >= i:
                return 1
            elif len(right) <= i and len(left) >= i:
                return -1
            else:
                result = compare(left[i], right[i])
                i += 1
        return result
    elif type(left) == int and type(right) != int:
        return compare([left], right)
    elif type(right) == int and type(left) != int:
        return compare(left, [right])
    else:
        return None

def part_one(input):
    return sum([i + 1 for i, pair in enumerate(input) if compare(pair[0], pair[1]) == 1])

def part_two(input):
    flattened_input = [packet for pair in input for packet in pair] + [[[2]]] + [[[6]]]
    sorted_packets = sorted(flattened_input, key=cmp_to_key(compare), reverse=True)
    return (sorted_packets.index([[2]]) + 1) * (sorted_packets.index([[6]]) + 1)

if __name__ == '__main__':
    print(part_one(parse_raw_input(get_raw_input_as_str("day_13_input.txt"))))
    print(part_two(parse_raw_input(get_raw_input_as_str("day_13_input.txt"))))

Advent of Code 2022 day 12

Well this is an embarrassing mess. I swear on my eyes, the tricky stuff is an adaption of the pseudocode on the Wikipedia page for A* search algorithm 🤦‍♂️

Just part one for now. I’m falling behind. Fingers crossed the next puzzle is more my jam.

import heapq
from string import ascii_lowercase
from utils import get_raw_input


class Heightmap:
    def __init__(self, raw_input):
        self.input_to_heightmap(raw_input)

    def input_to_heightmap(self, raw_input):
        self.heightmap = []
        for y, line in enumerate(raw_input):
            row = []
            for x, char in enumerate(line):
                if char == "S":
                    self.start = (x, y)
                    row.append(0)
                elif char == "E":
                    self.goal = (x, y)
                    row.append(25)
                else:
                    row.append(ascii_lowercase.index(char))
            self.heightmap.append(row)

    @property
    def node_keys(self):
        return [
            (x, y)
            for y in range(len(self.heightmap))
            for x in range(len(self.heightmap[y]))
        ]

    def node_value(self, node_key):
        if node_key in self.node_keys:
            return self.heightmap[node_key[1]][node_key[0]]
        return None

    def get_neighboring_nodes(self, node):
        candidates = [
            (node[0], node[1] - 1),
            (node[0] + 1, node[1]),
            (node[0], node[1] + 1),
            (node[0] - 1, node[1]),
        ]
        return [
            candidate
            for candidate in candidates
            if candidate in self.node_keys
            and self.node_value(candidate) - self.node_value(node) <= 1
        ]

    def manhattan_distance(self, current, neighbor):
        return abs(current[0] - neighbor[0]) + abs(current[1] - neighbor[1])

    def reconstruct_path(self, came_from, current):
        final_path = [current]
        while current in came_from.keys():
            current = came_from[current]
            final_path.insert(0, current)
        return final_path

    def a_star(self):
        open_set = [(0, self.start)]
        came_from = {}
        g_score = {self.start: 0}
        while open_set:
            current = heapq.heappop(open_set)[1]
            if current == self.goal:
                return self.reconstruct_path(came_from, current)
            for neighbor in self.get_neighboring_nodes(current):
                tentative_g_score = g_score[current] + 1
                if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g_score
                    heapq.heappush(
                        open_set,
                        (
                            tentative_g_score
                            + self.manhattan_distance(current, neighbor),
                            neighbor,
                        ),
                    )


if __name__ == "__main__":
    heightmap = Heightmap(get_raw_input("day_12_input.txt"))
    print(f"{len(heightmap.a_star())=}")

Advent of Code 2022 day 11

Part one solution for Advent of Code 2022 day 11. The problem description made a bit of a fuss about the exact order things happen in, so I’ve over-engineered this a bit. Couldn’t make up my mind whether to create a class for items or not either.

But part two? Nah forget it. Those are the puzzles that turn me off. Some sort of mathematics is required, and I don’t have enough knowledge to even know what knowledge I don’t have. I don’t have the inclination to find out either - maths has never interested me.

import math
import re
import utils


class Monkey:
    def __init__(self, starting_items, operation, test, true, false):
        self.items = self.parse_starting_items(starting_items)
        self.operation = operation
        self.test = int(test)
        self.true = int(true)
        self.false = int(false)
        self.items_inspected = 0

    def parse_starting_items(self, starting_items):
        return [int(n) for n in starting_items.split(", ")]

    def inspect_items(self):
        self.items = [
            eval(f"{self.operation.replace('old','item')}") for item in self.items
        ]
        self.items = [int(item / 3) for item in self.items]
        self.items_inspected += len(self.items)

    def throw_items(self):
        items = [
            {
                "worry_level": item,
                "throw_to": self.true if bool(item % self.test == 0) else self.false,
            }
            for item in self.items
        ]
        self.items = []
        return items


if __name__ == "__main__":
    raw_input = utils.get_raw_input_as_str("day_11_input.txt")
    monkeys = {
        int(m["monkey"]): Monkey(
            *m.group("starting_items", "operation", "test", "true", "false")
        )
        for m in re.finditer(
            r"^.*(?P<monkey>\d+):\n.*: (?P<starting_items>.*)\n.*= (?P<operation>.*)\n.* (?P<test>\d+)\n.*(?P<true>\d+)\n.*(?P<false>\d+)",
            raw_input,
            re.MULTILINE,
        )
    }

    for _ in range(20):
        for k, monkey in monkeys.items():
            monkey.inspect_items()
            for item in monkey.throw_items():
                monkeys[item["throw_to"]].items.append(item["worry_level"])

    for k, monkey in monkeys.items():
        print(f"{k=} {monkey.items=} {monkey.items_inspected=}")

    print(
        math.prod(sorted([monkey.items_inspected for monkey in monkeys.values()])[-2:])
    )