Skip to content

Latest commit

 

History

History
240 lines (158 loc) · 9.41 KB

README.md

File metadata and controls

240 lines (158 loc) · 9.41 KB

@xavdid's Python Advent of Code Project Template

This is my tried-and-true Python utility package for the phenomenal Advent of Code puzzles. It handles creating stub solutions, input parsing, and printing your answer, letting you focus on the actual solve. I've been using it since 2017.

Additionally, Over in the main repo, I include a step-by-step explanation of each puzzle if you're in the learning mood!

Quickstart

To use this base class for your own solutions:

  1. Ensure you have Python 3.12 or higher. You can use pyenv or asdf to manage your Python versions. It may work on older versions, but 3.12-specific features will be added without further breaking changes
  2. Create a new repo using this template (docs) and clone it locally
  3. Start a new solution using ./start
  4. Edit the newly created file at solutions/YEAR/day_01/solution.py
  5. Run your code answers using ./advent
  6. Repeat and enjoy!

Commands

This repo has two main commands: start and advent.

./start

Usage

./start [-h] [--year YEAR] [day]

Scaffold files to start a new Advent of Code solution

positional arguments:

  • day (optional): Which puzzle day to start, between [1,25]. Defaults to the next day without a folder (matching day_NN) in the specified year.

optional arguments:

  • -h, --help (optional): show this help message and exit
  • --year YEAR (optional): Puzzle year. Defaults to current year if December has begun, otherwise previous year.

Examples

  • ./start
  • ./start 2
  • ./start 3 --year 2019

./advent

Usage

./advent [--year year] [--test-data] [--debug] [--profile] [--slow] [--time] [day]

Run a specific day of Advent of Code

informational flags

  • -h, --help: Show this help message and exit
  • --version: Print version info and exit

positional arguments:

  • day (optional): Which puzzle day to start, between [1,25]. Defaults to the latest day with a folder (matching day_NN) in the specified year.

optional flags:

  • --year YEAR: puzzle year. Defaults to current year if December has begun, otherwise previous year
  • -t, --test-data: read puzzle input from input.test.txt instead of input.txt. Ignores @answer decorators (see saving answers)
  • --debug: prints normally-hidden debugging statements (written with self.debug(...)). See debugging
  • --profile: run solution through a performance profiler
  • --slow: specify that long-running solutions (or those requiring manual input) should be run. They're skipped otherwise
  • --time: print information about how long solutions took to run. More useful than timing at a shell level, since this only starts the timer once all imports have happened and any advent-related code is done.

Examples

  • ./advent
  • ./advent 2
  • ./advent 5 --year 2019
  • ./advent 7 --test-data
  • ./advent 9 -t --debug

File Structure

solutions/
├── ...
└── 2020/
    ├── day_01/
    │   ├── solution.py
    │   ├── input.txt
    │   ├── input.test.txt
    │   └── README.md
    ├── day_02/
    │   ├── solution.py
    │   ├── ...
    └── ...
  • each year has a folder (YYYY)
  • each day in that year (will eventually) have a folder (day_NN)

Each day_NN folder has the following files:

  • solution.py, which has a class Solution. ./advent expects both that filename and that class name exactly, so you shouldn't change them. See Writing Solutions for how the file is structured
  • input.txt holds your individualized input from the AoC site. Make sure not to share it publicly!
  • input.test.txt holds the example input from the prompt. It's read when the --test-input flag is used (see below). It also won't throw errors if the result doesn't match the answer. You can also do all your work in input.txt, but it's marginally less convenient
  • README.md is a convenient place to take notes or explain your solution

Writing Solutions

The Solution Class

A helpful base class on which to build your AoC solutions. It's got 2 required properties (which should be pre-filled if you use ./start): _year and _day, corresponding to the puzzle you're solving.

Your puzzle input, the parsed contents of the day's input.txt, will be available at self.input. Learn more in Reading Input.

There's also a convenience method for print-based debugging: self.debug(). You can pass it any number of items and they'll get pretty-printed. It only prints if you use the --debug flag with ./advent.

Reading Input

AoC input takes a number of forms, so there are a number of simple modes for input parsing. Your generated Solution class should inherit from one of the following classes, which will parse self.input for you:

Inherited Class description sample for this mode
TextSolution one solid block of text; the default abcde
IntSolution one number 12345
StrSplitSolution str[], split by a specified separator (default newline) a
b
c
d
e
IntSplitSolution int[], split by a specified separator (default newline) 1
2
3
4
5
# input file is "12345"

from ...base import (
    IntSolution,
    IntSplitSolution,
    StrSplitSolution,
    TextSolution,
)

for BaseClass in [TextSolution, IntSolution, StrSplitSolution, IntSplitSolution]:

    class Solution(BaseClass):
        def show_input(self):
            print(f"\n{self.input} (type: {type(self.input)})\n")

    Solution().show_input()

# 12345 (type: <class 'str'>)
# 12345 (type: <class 'int'>)
# ['12345'] (type: <class 'list'>)
# [12345] (type: <class 'list'>)

You can also change the separator to change how the SplitSolutions work:

# input file is "1,2,3,4,5"

from ...base import IntSplitSolution, StrSplitSolution

for BaseClass in [StrSplitSolution, IntSplitSolution]:

    class Solution(BaseClass):
        separator = ","

        def show_input(self):
            print(f"\n{self.input} (type: {type(self.input)})\n")

    Solution().show_input()

# ['1', '2', '3', '4', '5'] (type: <class 'list'>)
# [1, 2, 3, 4, 5] (type: <class 'list'>)

Solution Functions

Each AoC puzzle has two parts, so there are two functions you need to write: part_1 and part_2. Each should return an int, since that's typically the answer that AoC expects.

Sometimes, it's easier to calculate both parts in a single function (such as if the answer is asking about two parts of a single computation). In that case, there's also a solve() method, which should return a 2-tuple with your answers (like (5, 7)). solve takes precedence if present. Feel free to delete any unused functions when you're done.

class Solution(TextSolution):
    def part_1(self) -> int:
        return some_computation()

    def part_2(self) -> int:
        return some_other_computation()

    # or:

    def solve(self) -> tuple[int, int]:
        part_1 = 0
        total = 0

        for i in range(10):
            result = some_computation()
            if i == 0:
                part_1 = result

            total += result

        return result

Saving Answers

Once you've solved the puzzle, you can decorate your answer function (solve or part_N) with the @answer decorator. It asserts that the value returned from the function is whatever you pass to the decorator:

class Solution(TextSolution):
    _year = 2022
    _day = 5

    @answer(123)
    def part_1(self) -> int:
        return 123

    @answer(234)
    def part_2(self) -> int:
        return 123 # err!

This is helpful for ensuring your answer doesn't change when editing your code after you've solved the puzzle. It's included as a comment in the template. It's ignored when running against test input, so it's easy to verify as you go.

Debugging

The base class includes a self.debug method which will pretty-print all manner of inputs. These only show up when the --debug flag is used, making it a convenient way to show debugging info selectively.

Linting & Type Checking

I recommend the following tools:

  • Ruff, a lightning-fast linter (to help you catch bugs)
  • Pyright, a type checker to identify logical errors (also available in VSCode using their Python plugin)

If you have both available, then just lint will run them both. I've included a simple ruff configuration file to help get you started.

Marking Slow Solutions

If you're running many solutions at once and want to exclude individual parts of solutions (or entire days), you can mark individual functions with the @slow decorator. They'll print a warning, but won't actually run the solution.