zerosleeps

Since 2010

Advent of Code 2022 day 10

My Python solution for Advent of Code 2022 day 10. Just the right amount of frustrating to keep it fun and interesting. I spent ages debugging part two because I kept confusing the “sprite” with the CRT’s current position - I was adding padding to the CRT’s position not the sprite’s position, so I kept getting answers that were always slightly off by weird amounts.

Anyway, here we go. (I’ve chopped out the “large example” input and the right-hand side of the part two equality test from this code block.)

import math
from unittest import TestCase
from utils import get_raw_input


class Process:
    def __init__(self, x_register=1):
        self.x_register = x_register
        self.current_instruction = None
        self.current_step = 0
        self.inputs = None

    def tick(self):
        if self.current_instruction:
            exec(f"self.{self.current_instruction}()")

    def new_instruction(self, instruction, inputs=None):
        if self.current_instruction != None:
            raise Exception("Already got an instruction")
        else:
            self.current_instruction = instruction
            self.current_step = 0
            self.inputs = inputs

    def noop(self):
        self.current_instruction = None

    def addx(self):
        if self.current_step < 1:
            self.current_step += 1
        else:
            self.x_register += int(self.inputs)
            self.current_instruction = None

    @property
    def ready(self):
        return not bool(self.current_instruction)


def run_program(process, instructions):
    cycles = [process.x_register]
    for i, instruction in enumerate(instructions):
        try:
            process.new_instruction(instruction.split()[0], instruction.split()[1])
        except IndexError:
            process.new_instruction(instruction)

        while not process.ready:
            process.tick()
            cycles.append(process.x_register)
    return cycles


def extract_signal_strength(cycles):
    return [(i + 1, value) for i, value in enumerate(cycles) if ((i % 40) - 19) == 0]


def calculate_signal_strength(signal_strengths):
    return sum([math.prod(strength) for strength in signal_strengths])


def build_screen(cycles):
    crt = []
    for y in range(6):
        row = []
        for x in range(40):
            sprite_centre = cycles[x + (y * 40)]
            if x in range(sprite_centre - 1, sprite_centre + 2):
                row.append("#")
            else:
                row.append(".")
        crt.append(row)
    return "\n".join(["".join(row) for row in crt])


def part_one(input):
    process = Process()
    cycles = run_program(process, input)
    return calculate_signal_strength(extract_signal_strength(cycles))


def part_two(input):
    process = Process()
    cycles = run_program(process, input)
    return build_screen(cycles)


class TestPartOne(TestCase):
    def setUp(self):
        self.small_example = [
            "noop",
            "addx 3",
            "addx -5",
        ]
        self.large_example = [
            REMOVED
        ]
        self.process = Process()

    def test_first_example_x_register(self):
        run_program(self.process, self.small_example)
        self.assertEqual(self.process.x_register, -1)

    def test_second_example_registers(self):
        cycles = run_program(self.process, self.large_example)
        self.assertEqual(cycles[19], 21)
        self.assertEqual(cycles[59], 19)
        self.assertEqual(cycles[99], 18)
        self.assertEqual(cycles[139], 21)
        self.assertEqual(cycles[179], 16)
        self.assertEqual(cycles[219], 18)

    def test_part_one_final_example(self):
        self.assertEqual(part_one(self.large_example), 13140)

    def test_part_two_example(self):
        self.assertEqual(
            part_two(self.large_example),
            REMOVED,
        )


if __name__ == "__main__":
    input = get_raw_input("day_10_input.txt")
    print(f"Part one: {part_one(input)}")
    print(f"Part two: \n{part_two(input)}")

Advent of Code 2022 day 9

Took me ages to work out a bug with part two. My original solution for part one worked fine and my code passed for the part 2 examples. As is almost always the case, the bug seems obvious now I know what it was!

from unittest import TestCase
from utils import get_raw_input


class Rope:
    def __init__(self, number_of_knots=1):
        self.head = [0, 0]
        self.knots = [[0, 0] for _ in range(number_of_knots)]
        self.tail_visits = set()

    def move_head(self, direction):
        if direction == "U":
            self.head[1] += 1
        elif direction == "R":
            self.head[0] += 1
        elif direction == "D":
            self.head[1] -= 1
        elif direction == "L":
            self.head[0] -= 1
        self.move_knots()

    def move_knots(self):
        for i, knot in enumerate(self.knots):
            if i == 0:
                knot_to_follow = self.head
            else:
                knot_to_follow = self.knots[i - 1]

            x_difference = knot_to_follow[0] - knot[0]
            y_difference = knot_to_follow[1] - knot[1]

            if x_difference > 1 or (x_difference == 1 and abs(y_difference) == 2):
                knot[0] += 1
            if x_difference < -1 or (x_difference == -1 and abs(y_difference) == 2):
                knot[0] -= 1
            if y_difference > 1 or (y_difference == 1 and abs(x_difference) == 2):
                knot[1] += 1
            if y_difference < -1 or (y_difference == -1 and abs(x_difference) == 2):
                knot[1] -= 1

            if i + 1 == len(self.knots):
                self.tail_visits.add(tuple(knot))

    @property
    def number_of_tail_visits(self):
        return len(self.tail_visits)


def run(rope, moves):
    for move in moves:
        direction, distance = move.split()
        for i in range(int(distance)):
            rope.move_head(direction)
    return rope


def part_one(input):
    rope = Rope()
    run(rope, input)
    return rope.number_of_tail_visits


def part_two(input):
    rope = Rope(9)
    run(rope, input)
    return rope.number_of_tail_visits


if __name__ == "__main__":
    input = get_raw_input("day_09_input.txt")
    print(f"Part one: {part_one(input)}")
    print(f"Part two: {part_two(input)}")


class TextExamples(TestCase):
    def setUp(self):
        self.first_example = [
            "R 4",
            "U 4",
            "L 3",
            "D 1",
            "R 4",
            "D 1",
            "L 5",
            "R 2",
        ]
        self.second_example = [
            "R 5",
            "U 8",
            "L 8",
            "D 3",
            "R 17",
            "D 10",
            "L 25",
            "U 20",
        ]

    def test_part_one_example(self):
        self.assertEqual(part_one(self.first_example), 13)

    def test_part_one_example_one(self):
        self.assertEqual(part_two(self.first_example), 1)

    def test_part_one_example_two(self):
        self.assertEqual(
            part_two(self.second_example),
            36,
        )

Advent of Code 2022 day 8

Not very pleased with the code I’m about to post for Advent of Code 2022 day 8. It has loads of replication and dumb use of variables. No unit tests either. But… it works and that’s got to be better than beautiful code that doesn’t work.

import math
from utils import get_raw_input


def parse_raw_input(raw_input):
    return [[int(c) for c in line] for line in raw_input]


def shorter_than_current_node(other_node, current_node):
    return other_node < current_node


def viewing_distance(other_nodes, current_node):
    can_see = 0
    for node in other_nodes:
        if node < current_node:
            can_see += 1
        else:
            can_see += 1
            break
    return can_see


def part_one(input):
    can_be_seen = 0
    for y in range(len(input)):
        for x in range(len(input[y])):
            north = [row[x] for row in input][:y]
            east = input[y][x + 1 :]
            south = [row[x] for row in input][y + 1 :]
            west = input[y][:x]
            if (
                all([shorter_than_current_node(node, input[y][x]) for node in north])
                or all([shorter_than_current_node(node, input[y][x]) for node in east])
                or all([shorter_than_current_node(node, input[y][x]) for node in south])
                or all([shorter_than_current_node(node, input[y][x]) for node in west])
            ):
                can_be_seen += 1
    return can_be_seen


def part_two(input):
    best_score = 0
    for y in range(len(input)):
        for x in range(len(input[y])):
            north = list(reversed([row[x] for row in input][:y]))
            east = input[y][x + 1 :]
            south = [row[x] for row in input][y + 1 :]
            west = list(reversed(input[y][:x]))
            current_node_distances = [
                viewing_distance(north, input[y][x]),
                viewing_distance(east, input[y][x]),
                viewing_distance(south, input[y][x]),
                viewing_distance(west, input[y][x]),
            ]
            scenic_score = math.prod(current_node_distances)
            if scenic_score > best_score:
                best_score = scenic_score
    return best_score


if __name__ == "__main__":
    parsed_input = parse_raw_input(get_raw_input("day_08_input.txt"))
    print(part_one(parsed_input))
    print(part_two(parsed_input))

Advent of Code 2022 day 7

A little bit of OOP, a little bit of recursion. Had fun with this one.

import unittest
from utils import get_raw_input


class Directory:
    def __init__(self, parent=None):
        self.parent = parent
        self.directories = {}
        self.files = {}

    def subdirectory(self, name):
        return self.directories.setdefault(name, Directory(parent=self))

    def add_file(self, size, name):
        return self.files.setdefault(name, int(size))

    @property
    def size(self):
        return sum(self.files.values()) + sum(
            [dir.size for dir in self.directories.values()]
        )

    @property
    def subdirectories(self):
        return list(self.directories.values()) + [
            sd
            for d in [
                directory.subdirectories for directory in self.directories.values()
            ]
            for sd in d
        ]


def build_file_system(raw_input):
    root_directory = Directory()
    current_directory = root_directory

    for line in raw_input:
        if line == "$ cd /":
            current_directory = root_directory
        elif line == "$ cd ..":
            current_directory = current_directory.parent
        elif line.startswith("$ cd"):
            current_directory = current_directory.subdirectory(line.split()[-1])
        elif line.startswith("dir"):
            current_directory.subdirectory(line.split()[-1])
        elif line[0].isdigit():
            current_directory.add_file(*line.split())

    return root_directory


def part_one(file_system):
    return sum(
        [
            directory.size
            for directory in file_system.subdirectories
            if directory.size <= 100000
        ]
    )


def part_two(file_system):
    min_space_to_find = 30000000 - (70000000 - file_system.size)
    candidate_directory_sizes = [
        directory.size
        for directory in file_system.subdirectories
        if directory.size >= min_space_to_find
    ]
    return min(candidate_directory_sizes)


if __name__ == "__main__":
    raw_input = get_raw_input("day_07_input.txt")
    file_system = build_file_system(raw_input)
    print(f"Part one: {part_one(file_system)}")
    print(f"Part two: {part_two(file_system)}")


class TestExamples(unittest.TestCase):
    def setUp(self):
        self.input = [
            "$ cd /",
            "$ ls",
            "dir a",
            "14848514 b.txt",
            "8504156 c.dat",
            "dir d",
            "$ cd a",
            "$ ls",
            "dir e",
            "29116 f",
            "2557 g",
            "62596 h.lst",
            "$ cd e",
            "$ ls",
            "584 i",
            "$ cd ..",
            "$ cd ..",
            "$ cd d",
            "$ ls",
            "4060174 j",
            "8033020 d.log",
            "5626152 d.ext",
            "7214296 k",
        ]
        self.file_system = build_file_system(self.input)

    def test_size_of_e(self):
        self.assertEqual(self.file_system.directories["a"].directories["e"].size, 584)

    def test_size_of_a(self):
        self.assertEqual(self.file_system.directories["a"].size, 94853)

    def test_size_of_d(self):
        self.assertEqual(self.file_system.directories["d"].size, 24933642)

    def test_size_of_root_directory(self):
        self.assertEqual(self.file_system.size, 48381165)

    def test_part_one_example(self):
        self.assertEqual(part_one(self.file_system), 95437)

    def test_part_two_example(self):
        self.assertEqual(part_two(self.file_system), 24933642)

Advent of Code 2022 day 5

I’m jumping around a little bit, but here’s my Python solution for Advent of Code 2022 day 5. I had quite a few false starts trying to parse the input for this one - I knew I wanted to transpose the stacks, but trying to extract the stack IDs from their delimiters while managing potential white-space caused some head scratching. Some list popping and pushing from there.

import re
import unittest
import utils


def extract_stacks_from_raw_input(raw_input):
    return [line.rstrip() for line in raw_input if "[" in line]


def extract_steps_from_raw_input(raw_input):
    return [line.rstrip() for line in raw_input if "move" in line]


def parse_raw_stacks(raw_stacks: list[str]):
    # Some of this is simply to work around the fact that I've set my text editor
    # to strip trailing whitespace from all lines in a file on save
    stacks_width = max([len(line) for line in raw_stacks])
    transposed_raw_stacks = [
        [row.ljust(stacks_width)[i] for row in raw_stacks] for i in range(stacks_width)
    ]
    dirty_stacks = [
        stack
        for stack in transposed_raw_stacks
        if any([char.isupper() for char in stack])
    ]
    return [list(reversed([c for c in stack if c.isupper()])) for stack in dirty_stacks]


def extract_data_from_step(step):
    return [
        int(g)
        for g in re.match(r"^move (\d+) from (\d+) to (\d+)$", step).group(1, 2, 3)
    ]


def parse_raw_steps(raw_steps):
    return [extract_data_from_step(step) for step in raw_steps]


def part_one(raw_input):
    stacks = parse_raw_stacks(extract_stacks_from_raw_input(raw_input))
    steps = parse_raw_steps(extract_steps_from_raw_input(raw_input))
    for step in steps:
        for _ in range(step[0]):
            stacks[step[2] - 1] += stacks[step[1] - 1].pop()
    return "".join([stack[-1] for stack in stacks])


def part_two(raw_input):
    stacks = parse_raw_stacks(extract_stacks_from_raw_input(raw_input))
    steps = parse_raw_steps(extract_steps_from_raw_input(raw_input))
    for step in steps:
        stacks[step[2] - 1] += stacks[step[1] - 1][-step[0] :]
        del stacks[step[1] - 1][-step[0] :]
    return "".join([stack[-1] for stack in stacks])


class TestExamples(unittest.TestCase):
    def setUp(self):
        self.raw_input = [
            "    [D]    ",
            "[N] [C]    ",
            "[Z] [M] [P]",
            " 1   2   3 ",
            "",
            "move 1 from 2 to 1",
            "move 3 from 1 to 3",
            "move 2 from 2 to 1",
            "move 1 from 1 to 2",
        ]

    def test_part_one(self):
        self.assertEqual(part_one(self.raw_input), "CMZ")

    def test_part_two(self):
        self.assertEqual(part_two(self.raw_input), "MCD")


if __name__ == "__main__":
    raw_input = utils.get_raw_input("day_05_input.txt", strip=False)
    print(f"Part one: {part_one(raw_input)}")
    print(f"Part two: {part_two(raw_input)}")