zerosleeps

Since 2010

Advent of Code 2020 day 24

Thursday 24 December 2020

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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()))}")