2  Advent Of Code

3 Advent of Code: Python으로 퍼즐 풀기

이 리포지토리는 Real Python의 Advent of Code: Python으로 퍼즐 풀기 튜토리얼 코드를 담고 있습니다.

3.1 의존성

테스트에는 Pytest가 사용됩니다. 먼저 가상 환경을 만들어야 합니다:

$ python -m venv venv
$ source venv/bin/activate

그런 다음 pippytest를 설치할 수 있습니다:

(venv) $ python -m pip install pytest

aoc_grid.py 예제는 ColoramaNumPy를 사용합니다. 해당 예제를 실행하려면 해당 패키지도 환경에 설치해야 합니다:

(venv) $ python -m pip install colorama numpy

퍼즐 솔루션은 Python의 표준 라이브러리만 사용합니다. 2021년 5일차 솔루션은 Python 3.10 이상에서만 사용할 수 있는 구조적 패턴 매칭을 사용합니다.

3.2 저자

3.3 라이선스

MIT 라이선스에 따라 배포됩니다. 자세한 내용은 LICENSE를 참조하세요.

3.4 파일: aoc_grid.py

import numpy as np
from colorama import Cursor

grid = np.array(
    [
        [1, 1, 1, 1, 1],
        [1, 0, 0, 0, 1],
        [1, 1, 1, 0, 1],
        [1, 0, 0, 2, 1],
        [1, 1, 1, 1, 1],
    ]
)

num_rows, num_cols = grid.shape
for row in range(num_rows):
    for col in range(num_cols):
        symbol = " *o"[grid[row, col]]
        print(f"{Cursor.POS(col + 1, row + 2)}{symbol}")

3.5 파일: aoc_state_machine.py

from dataclasses import dataclass


@dataclass
class StateMachine:
    memory: dict[str, int]
    program: list[str]

    def run(self):
        """Run the program"""
        current_line = 0
        while current_line < len(self.program):
            instruction = self.program[current_line]

            # Set a register to a value
            if instruction.startswith("set "):
                register, value = instruction[4], int(instruction[6:])
                self.memory[register] = value

            # Increase the value in a register by 1
            elif instruction.startswith("inc "):
                register = instruction[4]
                self.memory[register] += 1

            # Move the line pointer
            current_line += 1

3.6 파일: solutions/2019/01_the_tyranny_of_the_rocket_equation/aoc201901.py

AoC 1, 2019: 로켓 방정식의 폭정

"""AoC 1, 2019: The Tyranny of the Rocket Equation"""

# Standard library imports
import pathlib
import sys


def parse(puzzle_input):
    """Parse input"""
    return [int(line) for line in puzzle_input.split("\n")]


def part1(module_masses):
    """Solve part 1"""
    return sum(mass // 3 - 2 for mass in module_masses)


def part2(module_masses):
    """Solve part 2"""
    return sum(all_fuel(mass) for mass in module_masses)


def all_fuel(mass):
    """Calculate fuel while taking mass of the fuel into account.

    ## Example:

    >>> all_fuel(1969)
    966
    """
    fuel = mass // 3 - 2
    if fuel <= 0:
        return 0
    else:
        return fuel + all_fuel(mass=fuel)


def solve(puzzle_input):
    """Solve the puzzle for the given input"""
    data = parse(puzzle_input)
    solution1 = part1(data)
    solution2 = part2(data)

    return solution1, solution2


if __name__ == "__main__":
    for path in sys.argv[1:]:
        print(f"\n{path}:")
        solutions = solve(puzzle_input=pathlib.Path(path).read_text().strip())
        print("\n".join(str(solution) for solution in solutions))

3.7 파일: solutions/2019/01_the_tyranny_of_the_rocket_equation/test_aoc201901.py

AoC 1, 2019 테스트: 로켓 방정식의 폭정

"""Tests for AoC 1, 2019: The Tyranny of the Rocket Equation"""

# Standard library imports
import pathlib

# Third party imports
import aoc201901 as aoc
import pytest

PUZZLE_DIR = pathlib.Path(__file__).parent


@pytest.fixture
def example1():
    puzzle_input = (PUZZLE_DIR / "example1.txt").read_text().strip()
    return aoc.parse(puzzle_input)


def test_parse_example1(example1):
    """Test that input is parsed properly"""
    assert example1 == [12, 14, 1969, 100756]


def test_part1_example1(example1):
    """Test part 1 on example input"""
    assert aoc.part1(example1) == 2 + 2 + 654 + 33583


def test_part2_example1(example1):
    """Test part 2 on example input"""
    assert aoc.part2(example1) == 2 + 2 + 966 + 50346

3.8 파일: solutions/2020/01_report_repair/aoc202001.py

AoC 1, 2020: 보고서 수정

"""AoC 1, 2020: Report Repair"""

# Standard library imports
import pathlib
import sys


def parse(puzzle_input):
    """Parse input"""
    return [int(line) for line in puzzle_input.split()]


def part1(numbers):
    """Solve part 1"""
    for num1 in numbers:
        for num2 in numbers:
            if num1 < num2 and num1 + num2 == 2020:
                return num1 * num2


def part2(numbers):
    """Solve part 2"""


def solve(puzzle_input):
    """Solve the puzzle for the given input"""
    data = parse(puzzle_input)
    solution1 = part1(data)
    solution2 = part2(data)

    return solution1, solution2


if __name__ == "__main__":
    for path in sys.argv[1:]:
        print(f"\n{path}:")
        solutions = solve(puzzle_input=pathlib.Path(path).read_text().strip())
        print("\n".join(str(solution) for solution in solutions))

3.9 파일: solutions/2020/01_report_repair/test_aoc202001.py

AoC 1, 2020 테스트: 보고서 수정

"""Tests for AoC 1, 2020: Report Repair"""

# Standard library imports
import pathlib

# Third party imports
import aoc202001 as aoc
import pytest

PUZZLE_DIR = pathlib.Path(__file__).parent


@pytest.fixture
def example1():
    puzzle_input = (PUZZLE_DIR / "example1.txt").read_text().strip()
    return aoc.parse(puzzle_input)


def test_parse_example1(example1):
    """Test that input is parsed properly"""
    assert example1 == [1721, 979, 366, 299, 675, 1456]


def test_part1_example1(example1):
    """Test part 1 on example input"""
    assert aoc.part1(example1) == 514579


@pytest.mark.skip(reason="Not implemented")
def test_part2_example1(example1):
    """Test part 2 on example input"""
    assert aoc.part2(example1) == ...

3.10 파일: solutions/2020/05_binary_boarding/aoc202005.py

AoC 5, 2020: 바이너리 탑승

"""AoC 5, 2020: Binary Boarding"""

# Standard library imports
import pathlib
import sys

BP2BINARY = str.maketrans({"F": "0", "B": "1", "L": "0", "R": "1"})


def parse(puzzle_input):
    """Parse input"""
    return [
        int(bp.translate(BP2BINARY), base=2) for bp in puzzle_input.split("\n")
    ]


def part1(seat_ids):
    """Solve part 1"""
    return max(seat_ids)


def part2(seat_ids):
    """Solve part 2"""
    all_ids = set(range(min(seat_ids), max(seat_ids) + 1))
    return (all_ids - set(seat_ids)).pop()


def solve(puzzle_input):
    """Solve the puzzle for the given input"""
    data = parse(puzzle_input)
    solution1 = part1(data)
    solution2 = part2(data)

    return solution1, solution2


if __name__ == "__main__":
    for path in sys.argv[1:]:
        print(f"\n{path}:")
        solutions = solve(puzzle_input=pathlib.Path(path).read_text().strip())
        print("\n".join(str(solution) for solution in solutions))

3.11 파일: solutions/2020/05_binary_boarding/test_aoc202005.py

AoC 5, 2020 테스트: 바이너리 탑승

"""Tests for AoC 5, 2020: Binary Boarding"""

# Standard library imports
import pathlib

# Third party imports
import aoc202005 as aoc
import pytest

PUZZLE_DIR = pathlib.Path(__file__).parent


@pytest.fixture
def example1():
    puzzle_input = (PUZZLE_DIR / "example1.txt").read_text().strip()
    return aoc.parse(puzzle_input)


def test_parse_example1(example1):
    """Test that input is parsed properly"""
    assert example1 == [357, 567, 119, 820]


def test_part1_example1(example1):
    """Test part 1 on example input"""
    assert aoc.part1(example1) == 820


def test_part2():
    """Test part 2 on example input"""
    seat_ids = [3, 9, 4, 8, 5, 10, 7, 11]
    assert aoc.part2(seat_ids) == 6

3.12 파일: solutions/2021/05_hydrothermal_venture/aoc202105.py

AoC 5, 2021: 열수 벤처

"""AoC 5, 2021: Hydrothermal Venture"""

# Standard library imports
import collections
import pathlib
import sys


def parse(puzzle_input):
    """Parse input"""
    lines = []
    for line in puzzle_input.split("\n"):
        point1, point2 = line.split(" -> ")
        x1, y1 = point1.split(",")
        x2, y2 = point2.split(",")
        lines.append((int(x1), int(y1), int(x2), int(y2)))
    return lines


def part1(lines):
    """Solve part 1"""
    vertical = [(x1, y1, x2, y2) for x1, y1, x2, y2 in lines if x1 == x2]
    horizontal = [(x1, y1, x2, y2) for x1, y1, x2, y2 in lines if y1 == y2]
    return count_overlaps(vertical + horizontal)


def part2(lines):
    """Solve part 2"""
    return count_overlaps(lines)


def count_overlaps(lines):
    """Count overlapping points between a list of lines

    ## Example:

    >>> count_overlaps(
    ...     [(3, 3, 3, 5), (3, 3, 6, 3), (6, 6, 6, 3), (4, 5, 6, 5)]
    ... )
    3
    """
    overlaps = collections.Counter(
        point for line in lines for point in points(line)
    )
    return sum(num_points >= 2 for num_points in overlaps.values())


def points(line):
    """List all points making up a line

    ## Examples:

    >>> points((0, 3, 3, 3))  # Horizontal line
    [(0, 3), (1, 3), (2, 3), (3, 3)]
    >>> points((3, 3, 3, 0))  # Vertical line
    [(3, 3), (3, 2), (3, 1), (3, 0)]
    >>> points((1, 2, 3, 4))  # Diagonal line
    [(1, 2), (2, 3), (3, 4)]
    """
    match line:
        case (x1, y1, x2, y2) if x1 == x2:
            return [(x1, y) for y in coords(y1, y2)]
        case (x1, y1, x2, y2) if y1 == y2:
            return [(x, y1) for x in coords(x1, x2)]
        case (x1, y1, x2, y2):
            return [
                (x, y)
                for x, y in zip(coords(x1, x2), coords(y1, y2), strict=False)
            ]


def coords(start, stop):
    """List coordinates between start and stop, inclusive.

    ## Examples:

    >>> list(coords(0, 3))
    [0, 1, 2, 3]
    >>> list(coords(2, -2))
    [2, 1, 0, -1, -2]
    """
    step = 1 if start <= stop else -1
    return range(start, stop + step, step)


def solve(puzzle_input):
    """Solve the puzzle for the given input"""
    data = parse(puzzle_input)
    solution1 = part1(data)
    solution2 = part2(data)

    return solution1, solution2


if __name__ == "__main__":
    for path in sys.argv[1:]:
        print(f"\n{path}:")
        solutions = solve(puzzle_input=pathlib.Path(path).read_text().strip())
        print("\n".join(str(solution) for solution in solutions))

3.13 파일: solutions/2021/05_hydrothermal_venture/test_aoc202105.py

AoC 5, 2021 테스트: 열수 벤처

"""Tests for AoC 5, 2021: Hydrothermal Venture"""

# Standard library imports
import pathlib

# Third party imports
import aoc202105 as aoc
import pytest

PUZZLE_DIR = pathlib.Path(__file__).parent


@pytest.fixture
def example1():
    puzzle_input = (PUZZLE_DIR / "example1.txt").read_text().strip()
    return aoc.parse(puzzle_input)


@pytest.fixture
def example2():
    puzzle_input = (PUZZLE_DIR / "example2.txt").read_text().strip()
    return aoc.parse(puzzle_input)


def test_parse_example1(example1):
    """Test that input is parsed properly"""
    assert example1 == [(2, 0, 0, 2), (0, 2, 2, 2), (0, 0, 0, 2), (0, 0, 2, 2)]


def test_part1_example1(example1):
    """Test part 1 on example input"""
    assert aoc.part1(example1) == 1


def test_part1_example2(example2):
    """Test part 1 on example input"""
    assert aoc.part1(example2) == 5


def test_part2_example1(example1):
    """Test part 2 on example input"""
    assert aoc.part2(example1) == 4


def test_part2_example2(example2):
    """Test part 2 on example input"""
    assert aoc.part2(example2) == 12

3.14 파일: templates/aoc_template.py

import pathlib
import sys


def parse(puzzle_input):
    """Parse input"""


def part1(data):
    """Solve part 1"""


def part2(data):
    """Solve part 2"""


def solve(puzzle_input):
    """Solve the puzzle for the given input"""
    data = parse(puzzle_input)
    solution1 = part1(data)
    solution2 = part2(data)

    return solution1, solution2


if __name__ == "__main__":
    for path in sys.argv[1:]:
        print(f"{path}:")
        puzzle_input = pathlib.Path(path).read_text().strip()
        solutions = solve(puzzle_input)
        print("\n".join(str(solution) for solution in solutions))

3.15 파일: templates/test_aoc_template.py

import pathlib

import aoc_template as aoc
import pytest

PUZZLE_DIR = pathlib.Path(__file__).parent


@pytest.fixture
def example1():
    puzzle_input = (PUZZLE_DIR / "example1.txt").read_text().strip()
    return aoc.parse(puzzle_input)


@pytest.fixture
def example2():
    puzzle_input = (PUZZLE_DIR / "example2.txt").read_text().strip()
    return aoc.parse(puzzle_input)


@pytest.mark.skip(reason="Not implemented")
def test_parse_example1(example1):
    """Test that input is parsed properly"""
    assert example1 == ...


@pytest.mark.skip(reason="Not implemented")
def test_part1_example1(example1):
    """Test part 1 on example input"""
    assert aoc.part1(example1) == ...


@pytest.mark.skip(reason="Not implemented")
def test_part2_example1(example1):
    """Test part 2 on example input"""
    assert aoc.part2(example1) == ...


@pytest.mark.skip(reason="Not implemented")
def test_part2_example2(example2):
    """Test part 2 on example input"""
    assert aoc.part2(example2) == ...