zerosleeps

Since 2010

Advent of Code 2020 day 3

My Python solution for Advent of Code 2020 day 3. Nested lists and row/column coordinates always make my brain hurt.

I took a gamble for part one and decided not to build the repeating pattern when needed, instead using the mod/wraparound approach. Got lucky that part two didn’t build on this component of the puzzle!

Grid could do with a little love, e.g. why does it have an ‘x’ getter but not a ‘y’ getter?

Ooh one other change I made is to the way I parse the puzzle input. In past years (and in this years previous challenges) I’ve usually had a get_input function that reads the input file and parses it. That has meant that my unit tests - with their hardcoded “example” inputs - have had to use pre-parsed inputs (or I’ve had to parse the example inputs manually before sticking it in the unit test).

I changed that today so that the Grid objects themselves parse raw input, and I can therefore feed them either text files or sample strings, which in turn means the parsing itself is now getting tested. Much better - will continue doing that.

from math import prod
from pathlib import Path
import unittest

def get_input():
    return (Path(__file__).parent / 'day_03_input.txt').read_text()

class Grid():
    def __init__(self, input):
        self.grid = self.parse_input(input)
        self.position = {'x': 0, 'y': 0}

    @classmethod
    def parse_input(self, input):
        return [ [ char for char in line.strip() ] for line in input.splitlines() ]

    @property
    def grid_height(self):
        return len(self.grid)

    @property
    def grid_width(self):
        """Return width of slope

        Assumes all rows have the same width
        """
        return len(self.grid[0])

    @property
    def x(self):
        """Return effective x position

        Accounts for arboreal genetics and biome stability
        """
        return self.position['x'] % self.grid_width

    def past_bottom(self):
        if self.position['y'] > ( self.grid_height - 1 ):
            return True
        else:
            return False

    def slope(self, delta_x, delta_y):
        obstacles = []
        while not self.past_bottom():
            obstacles.append(self.grid[self.position['y']][self.x])
            self.position['y'] += delta_y
            self.position['x'] += delta_x
        return obstacles

def part_one(input):
    return Grid(input).slope(3,1).count('#')

def part_two(input):
    slope_results = []
    slopes_to_check = [[1,1],[3,1],[5,1],[7,1],[1,2]]
    for slope in slopes_to_check:
        grid = Grid(input)
        slope_results.append(grid.slope(slope[0], slope[1]).count('#'))
    return prod(slope_results)

class TestExamples(unittest.TestCase):
    def setUp(self):
        self.example_grid = """..##.......
            #...#...#..
            .#....#..#.
            ..#.#...#.#
            .#...##..#.
            ..#.##.....
            .#.#.#....#
            .#........#
            #.##...#...
            #...##....#
            .#..#...#.#"""

    def test_part_one(self):
        self.assertEqual(part_one(self.example_grid), 7)

    def test_part_two(self):
        self.assertEqual(part_two(self.example_grid), 336)

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