zerosleeps

Since 2010

Reading log for 2020

According to my own records I completed 39 books last year, and abandoned an additional two. It’s been decades since I’ve read as much as I did in 2020.

I used to devour books when I was a pre-teen: The Hardy Boys, The Secret Seven, stuff like that. When I started high-school, my first English teacher decided those books were beneath my ability and somewhat forcibly tried to get me to read other stuff. For whatever reason her suggestions never stuck, and the implied ridicule put me off reading for a very long time.

She must have had good intentions, but her execution needed some work. Never liked her much. Ms. Awlson was her name.

Advent of Code 2020 day 25

Advent of Code 2020 day 25. A bit overcooked considering (spoiler) there’s no part 2 🙁

Very happy to have gotten all 50 stars this year. With the exception of day 10 part 2 the code is all my own. Yes I’ve used hints and pointers, and I’m already embarrassed by some of my solutions, but that all comes hand-in-hand with being a developer, right?

Great fun. Was nice to have a little puzzle to look forward to at the end of each day.

class Handshake:
    def __init__(self, public_key):
        self.public_key = public_key
        self.value = 1
        self.loop_size = self.get_loop_size()

    def transform(self):
        self.value = (self.value * 7) % 20201227

    def private_key(self, public_key):
        value = 1
        for _ in range(self.loop_size):
            value = (value * public_key) % 20201227
        return value

    def get_loop_size(self):
        loop_size = 0
        while self.value != self.public_key:
            self.transform()
            loop_size += 1
        return loop_size

def part_one(public_keys):
    card = Handshake(public_keys[0])
    return card.private_key(public_keys[1])

if __name__ == '__main__':
    print(f"Part one: {part_one([5764801, 17807724])}")

Advent of Code 2020 day 24

Advent of Code 2020 day 24. Two fun days in a row! Kinda wish I went a bit more object-orientated with this one - the result looks a bit messy and “scripty”.

I vaguely remembered a previous Advent of Code puzzle that used a hexagonal grid, and after a little poke around I used the cube coordinates system described by Red Blob Games.

Spent a while wondering why I wasn’t getting the correct answers for the part two examples, before I realised that I’d need to somehow cater for tiles beyond those I’d already looked at, as the puzzle clearly stated that they exist and default to white. pad_floor was therefore born.

from pathlib import Path
import re
import unittest

DIRECTIONS = {
    'e': [1, -1, 0],
    'se': [0, -1, 1],
    'sw': [-1, 0, 1],
    'w': [-1, 1, 0],
    'nw': [0, 1, -1],
    'ne': [1, 0, -1]
}

def get_raw_input():
    return (Path(__file__).parent / 'day_24_input.txt').read_text()

def parse_raw_input(raw_input):
    regexp = re.compile(r'e|se|sw|w|nw|ne')
    return [
        [direction for direction in re.findall(regexp, line.strip())]
        for line in raw_input.strip().splitlines()
    ]

def build_floor(parsed_input):

    tiles = {}
    # 0/None: white, 1: black

    for line in parsed_input:
        x, y, z = 0, 0, 0
        for direction in line:
            x += DIRECTIONS[direction][0]
            y += DIRECTIONS[direction][1]
            z += DIRECTIONS[direction][2]

        if tiles.get((x, y, z), 0) == 0:
            tiles[(x, y, z)] = 1
        else:
            tiles[(x, y, z)] = 0

    return tiles

def pad_floor(floor):
    new_tiles = {}
    for tile in floor:
        for adjacent_direction in DIRECTIONS.values():
            k = (
                tile[0] + adjacent_direction[0],
                tile[1] + adjacent_direction[1],
                tile[2] + adjacent_direction[2]
            )
            if k not in floor:
                new_tiles[k] = 0

    floor.update(new_tiles)
    return floor

def part_one(parsed_input):
    return len([tile for tile in build_floor(parsed_input).values() if tile == 1])

def adjacent_tile_colours(floor, tile):
    x, y, z = tile
    return [
        floor.get((x + 1, y - 1, z), 0),
        floor.get((x, y - 1, z + 1), 0),
        floor.get((x - 1, y, z + 1), 0),
        floor.get((x - 1, y + 1, z), 0),
        floor.get((x, y + 1, z - 1), 0),
        floor.get((x + 1, y, z - 1), 0)
    ]

def part_two(parsed_input):
    floor = build_floor(parsed_input)

    for day in range(100):
        floor = pad_floor(floor)
        changes = {}
        for position, colour in floor.items():
            adjacent_black_tiles = len([
                adjacent_colour
                for adjacent_colour
                in adjacent_tile_colours(floor, position)
                if adjacent_colour == 1
            ])
            if colour == 1 and (adjacent_black_tiles == 0 or adjacent_black_tiles > 2):
                changes[position] = 0
            elif colour == 0 and adjacent_black_tiles == 2:
                changes[position] = 1
        floor.update(changes)
    return len([tile for tile in floor.values() if tile == 1])

class TestExamples(unittest.TestCase):
    def setUp(self):
        self.example_input = """sesenwnenenewseeswwswswwnenewsewsw
                                neeenesenwnwwswnenewnwwsewnenwseswesw
                                seswneswswsenwwnwse
                                nwnwneseeswswnenewneswwnewseswneseene
                                swweswneswnenwsewnwneneseenw
                                eesenwseswswnenwswnwnwsewwnwsene
                                sewnenenenesenwsewnenwwwse
                                wenwwweseeeweswwwnwwe
                                wsweesenenewnwwnwsenewsenwwsesesenwne
                                neeswseenwwswnwswswnw
                                nenwswwsewswnenenewsenwsenwnesesenew
                                enewnwewneswsewnwswenweswnenwsenwsw
                                sweneswneswneneenwnewenewwneswswnese
                                swwesenesewenwneswnwwneseswwne
                                enesenwswwswneneswsenwnewswseenwsese
                                wnwnesenesenenwwnenwsewesewsesesew
                                nenewswnwewswnenesenwnesewesw
                                eneswnwswnwsenenwnwnwwseeswneewsenese
                                neswnwewnwnwseenwseesewsenwsweewe
                                wseweeenwnesenwwwswnew"""

    def test_part_one_example(self):
        self.assertEqual(part_one(parse_raw_input(self.example_input)), 10)

    def test_part_two_example(self):
        self.assertEqual(part_two(parse_raw_input(self.example_input)), 2208)

class TestPuzzleInput(unittest.TestCase):
    def test_part_one(self):
        self.assertEqual(part_one(parse_raw_input(get_raw_input())), 465)

if __name__ == '__main__':
    print(f"Part one: {part_one(parse_raw_input(get_raw_input()))}")
    print(f"Part two: {part_two(parse_raw_input(get_raw_input()))}")

Advent of Code 2020 day 23

Advent of Code 2020 day 23. Ooh I liked this one because it taught me something new: linked lists!

I took the “obvious” approach for part one, but almost nothing of that version is shown below as I first re-factored it, then stuck part_two_answer on top of it.

import unittest

class Game():
    def __init__(self, starting_circle, part=1):
        starting_circle = [int(char) for char in starting_circle]
        self.max_cup = max(starting_circle)
        self.min_cup = min(starting_circle)

        if part == 2:
            starting_circle += [ i for i in range(self.max_cup + 1, 1_000_000 + 1)]
            self.max_cup = 1_000_000

        self.circle = {
            cup: starting_circle[(i + 1) % len(starting_circle)]
            for i, cup in enumerate(starting_circle)
        }

        self.length = len(self.circle)
        self.current_cup = starting_circle[0]

    def move(self):
        crab = [
            self.circle[self.current_cup],
            self.circle[self.circle[self.current_cup]],
            self.circle[self.circle[self.circle[self.current_cup]]]
        ]

        destination = self.current_cup - 1

        if destination < self.min_cup:
            destination = self.max_cup

        while destination in crab:
            destination -= 1
            if destination < self.min_cup:
                destination = self.max_cup

        destination_original_link = self.circle[destination]
        chain_end_original_link = self.circle[crab[-1]]

        self.circle[destination] = crab[0]
        self.circle[crab[-1]] = destination_original_link
        self.circle[self.current_cup] = chain_end_original_link
        self.current_cup = chain_end_original_link

    def part_one_answer(self):
        cup = 1
        result = ""
        for i in range(self.length - 1):
            result += str(self.circle[cup])
            cup = self.circle[cup]
        return result

    def part_two_answer(self):
        return self.circle[1] * self.circle[self.circle[1]]

def part_one(input):
    game = Game(input)
    for _ in range(100):
        game.move()
    return game.part_one_answer()

def part_two(input):
    game = Game(input, 2)
    for _ in range(10_000_000):
        game.move()
    return game.part_two_answer()

class TestPartOne(unittest.TestCase):
    def test_part_one_example(self):
        self.assertEqual(part_one("389125467"), '67384529')

class TestPartTwo(unittest.TestCase):
    def test_part_two_example(self):
        self.assertEqual(part_two("389125467"), 149245887792)

if __name__ == '__main__':

    print(f"Part one: {part_one(INPUT)}")
    print(f"Part two: {part_two(INPUT)}")

Advent of Code 2020 day 21

Advent of Code 2020 day 21. I’m out-of-sequence as I skipped day 21 to prioritise day 22 yesterday. As is often the case with Advent of Code, the actual coding was fine once I understood the trick the puzzle was trying to get at. In this instance, it took me ages to understand that an ingredient associated with an allergen must be in all the recipes that contain that allergen.

from pathlib import Path
import re
import unittest

def get_raw_input():
    return (Path(__file__).parent/'day_21_input.txt').read_text()

class Food():
    def __init__(self, raw_line):
        self.raw_line = raw_line

    def parse_raw_line(self):
        regexp = re.compile(r'^(?P<ingredients>.*) \(contains (?P<contains>.*)\)$')
        return re.match(regexp, self.raw_line)

    @property
    def ingredients(self):
        return self.parse_raw_line()['ingredients'].split()

    @property
    def contains(self):
        return self.parse_raw_line()['contains'].split(', ')

def build_allergens(foods):
    allergens = {}
    for food in foods:
        for contains in food.contains:
            if contains in allergens:
                allergens[contains] &= set(food.ingredients)
            else:
                allergens[contains] = set(food.ingredients)

    while any([len(ingredients) > 1 for ingredients in allergens.values()]):
        # Find allergents with only one possible ingredient, and remove that
        # ingredient from all other allergens
        for allergen, ingredients in allergens.items():
            if len(ingredients) == 1:
                for update_allergen in allergens:
                    if update_allergen != allergen:
                        allergens[update_allergen] -= ingredients

    return allergens

def build_no_allergens(foods, allergens):
    return [
        ingredient
        for food in foods
        for ingredient in food.ingredients
        if ingredient not in [
            allergen_ingredient
            for allergen in allergens.values()
            for allergen_ingredient in allergen
        ]
    ]

def part_one(raw_input):
    foods = [Food(raw_line) for raw_line in raw_input.strip().splitlines()]
    allergens = build_allergens(foods)
    return len(build_no_allergens(foods,allergens))

def part_two(raw_input):
    foods = [Food(raw_line) for raw_line in raw_input.strip().splitlines()]
    allergens = build_allergens(foods)
    return ','.join([allergens[allergen].pop() for allergen in sorted(allergens)])

class TestPartOne(unittest.TestCase):
    def test_part_one_example(self):
        self.assertEqual(part_one("mxmxvkd kfcds sqjhc nhms (contains dairy, fish)\ntrh fvjkl sbzzf mxmxvkd (contains dairy)\nsqjhc fvjkl (contains soy)\nsqjhc mxmxvkd sbzzf (contains fish)"), 5)

class TestPartTwo(unittest.TestCase):
    def test_part_two_example(self):
        self.assertEqual(part_two("mxmxvkd kfcds sqjhc nhms (contains dairy, fish)\ntrh fvjkl sbzzf mxmxvkd (contains dairy)\nsqjhc fvjkl (contains soy)\nsqjhc mxmxvkd sbzzf (contains fish)"), 'mxmxvkd,sqjhc,fvjkl')

if __name__ == '__main__':
    print(f'Part one: {part_one(get_raw_input())}')
    print(f'Part two: {part_two(get_raw_input())}')