zerosleeps

Since 2010

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()))}")