diff --git a/day18/__init__.py b/day18/__init__.py index e69de29..b978876 100644 --- a/day18/__init__.py +++ b/day18/__init__.py @@ -0,0 +1 @@ +"""Day18 solution.""" diff --git a/day18/day18a.py b/day18/day18a.py index 895b6a8..b91fcd5 100644 --- a/day18/day18a.py +++ b/day18/day18a.py @@ -1,4 +1,4 @@ -"""day18 solution""" +"""day18 solution.""" from dataclasses import dataclass from enum import StrEnum @@ -12,25 +12,33 @@ @dataclass class Position: + """Simple 2d point.""" + row: int = 0 col: int = 0 class Direction(StrEnum): + """Cardinal direction in ``UDLR``.""" + Up = "U" Down = "D" Left = "L" Right = "R" def __repr__(self) -> str: # pragma: no cover + """Custom repr for easy printing.""" return str(self) def __str__(self) -> str: + """Up/Down/Left/Right.""" return self.name -@dataclass +@dataclass(frozen=True) class Command: + """Well defined command dataclass.""" + direction: Direction steps: int color: str @@ -39,6 +47,19 @@ class Command: def generate_offsets( position: Position, direction: Direction, steps: int ) -> list[Position]: + """Generate position offsets. + + Args: + position (Position): base position + direction (Direction): direction of travel + steps (int): number of steps to generate + + Raises: + AssertionError: If direciton is invalid. + + Returns: + list[Position]: list of new positions. + """ if direction == Direction.Right: return [Position(position.row, position.col + i) for i in range(1, steps + 1)] if direction == Direction.Left: @@ -51,6 +72,8 @@ def generate_offsets( class Matrix: + """2d array representing world.""" + contents: list[list[Tile]] min_pos: Position @@ -63,6 +86,7 @@ class Matrix: dug_tiles = 0 def __init__(self, min_pos: Position, max_pos: Position) -> None: + """Generate 2d array with min/max position for offsetting.""" self.num_rows = max_pos.row - min_pos.row + 1 self.num_cols = max_pos.col - min_pos.col + 1 self.contents = [ @@ -70,7 +94,17 @@ def __init__(self, min_pos: Position, max_pos: Position) -> None: ] def process_command(self, miner_pos: Position, command: Command) -> Position: - """Process command, returning miner's new position""" + """Process command. + + This moves the miner around. + + Args: + miner_pos (Position): miner's current position + command (Command): command to process + + Returns: + Position: miner's new position. + """ offsets = generate_offsets(miner_pos, command.direction, command.steps) self.wall_tiles += len(offsets) for offset in offsets: @@ -78,8 +112,7 @@ def process_command(self, miner_pos: Position, command: Command) -> Position: return offsets[-1] def dig_out(self) -> None: - # digs out the non-perimeter tiles, returning how many were "dug out" - + """Dig out non-perimeter tiles using flood-fill.""" # cache for what's been visited visited: list[list[bool]] = [ [False for _ in range(self.num_cols)] for _ in range(self.num_rows) @@ -105,7 +138,14 @@ def dig_out(self) -> None: to_process.put(generate_offsets(position, direction, 1)[0]) def is_oob(self, position: Position) -> bool: - """True if position out of bounds""" + """Returns if a position is out of bounds. + + Args: + position (Position): position to check. + + Returns: + bool: True if out of bounds. + """ return ( position.row < 0 or position.row >= self.num_rows @@ -114,10 +154,22 @@ def is_oob(self, position: Position) -> bool: ) def __str__(self) -> str: + """Custom __str__ for pretty printing matrix.""" return "\n".join("".join(str(tile) for tile in row) for row in self.contents) def get_matrix_range(commands: list[Command]) -> tuple[Position, Position]: + """Calculate minimum and maximum position in matrix. + + Since we start in the middle somewhere, we get negative positions. + This can be useds to offset the matrix when we construct it. + + Args: + commands (list[Command]): list of commands that will be run. + + Returns: + tuple[Position, Position]: [min,max] positions. + """ position = Position() max_row, max_col = 0, 0 min_row, min_col = 0, 0 @@ -131,6 +183,7 @@ def get_matrix_range(commands: list[Command]) -> tuple[Position, Position]: def get_input(path: str) -> list[Command]: + """Reads input from file into well-formed list of commands.""" commands = [] with open(path, encoding="utf-8") as file: for line in file: @@ -141,6 +194,13 @@ def get_input(path: str) -> list[Command]: def get_solution(commands: list[Command]) -> int: + """Calculates solution. + + 1. Pre-calculates the range + 2. Creates edge tiles + 3. Flood fill centre. + 4. Count tiles. + """ min_pos, max_pos = get_matrix_range(commands) matrix: Matrix = Matrix(min_pos, max_pos) @@ -159,6 +219,7 @@ def get_solution(commands: list[Command]) -> int: def main() -> None: + """Load data and then find solution to part1.""" commands: list[Command] = get_input(INPUT) get_solution(commands) diff --git a/day18/day18b.py b/day18/day18b.py index 0c0a629..cc362b7 100644 --- a/day18/day18b.py +++ b/day18/day18b.py @@ -1,3 +1,4 @@ +"""Day18b solution.""" from dataclasses import dataclass from enum import IntEnum @@ -7,11 +8,15 @@ @dataclass class Position: + """Simple 2d vector.""" + row: int = 0 col: int = 0 class Direction(IntEnum): + """Direction as an integer enum.""" + Right = 0 Down = 1 Left = 2 @@ -20,15 +25,19 @@ class Direction(IntEnum): @dataclass(init=False) class Command: + """Command from hexstring.""" + direction: Direction steps: int def __init__(self, hexcode: str): + """Converts from hexcode to well formed direction+steps.""" self.steps = int(hexcode[:5], 16) self.direction = Direction(int(hexcode[-1])) def get_input(path: str) -> list[Command]: + """Grabs input from file, parsing into well-formed commands.""" commands = [] with open(path, encoding="utf-8") as file: for line in file: @@ -39,6 +48,7 @@ def get_input(path: str) -> list[Command]: def process_command(command: Command, position: Position) -> Position: + """Process a command and return new position.""" if command.direction == Direction.Right: return Position(position.row, position.col + command.steps) if command.direction == Direction.Down: @@ -51,6 +61,7 @@ def process_command(command: Command, position: Position) -> Position: def calculate_area(positions: list[Position], perimeter: int) -> int: + """Calculate area using shoelace area.""" # total_area = shoelace_area + (perimeter_length // 2) + 1 # shoelace assumes that each point is in centre, but each @@ -67,6 +78,7 @@ def calculate_area(positions: list[Position], perimeter: int) -> int: def get_solution(commands: list[Command]) -> int: + """Get solution via processing commands then running shoelace area.""" positions: list[Position] = [] position = Position() @@ -80,6 +92,7 @@ def get_solution(commands: list[Command]) -> int: def main() -> None: + """Grab input and then pass it into solver.""" commands: list[Command] = get_input(INPUT) print(get_solution(commands)) diff --git a/day18/lib/__init__.py b/day18/lib/__init__.py index e69de29..dca6b0a 100644 --- a/day18/lib/__init__.py +++ b/day18/lib/__init__.py @@ -0,0 +1 @@ +"""day18 library modules.""" diff --git a/day18/lib/tile.py b/day18/lib/tile.py index bfde4d5..4bdd5d6 100644 --- a/day18/lib/tile.py +++ b/day18/lib/tile.py @@ -1,16 +1,22 @@ +"""Tile Class.""" from dataclasses import dataclass @dataclass class Tile: + """Tile for part 1, represents a non-dugout tile.""" + contents: str = "." def __str__(self) -> str: + """Custom str for easy printing.""" return self.contents @dataclass(kw_only=True) class EdgeTile(Tile): + """Edge tile (``#``).""" + contents: str = "#" color: str @@ -18,9 +24,11 @@ class EdgeTile(Tile): TEXT_WHITE = "\033[38;2;255;255;255m" def text_color(self, r: int, g: int, b: int) -> str: + """Return ansicode color of edge based on input.""" return f"\033[38;2;{r};{g};{b}m" def __str__(self) -> str: + """Return colored string of ``#`` based on hexcode.""" r, g, b = [int(self.color[i * 2 : i * 2 + 2], 16) for i in range(3)] return f"{self.text_color(r,g,b)}{self.contents}{self.TEXT_WHITE}" @@ -28,4 +36,6 @@ def __str__(self) -> str: @dataclass(kw_only=True) class HoleTile(Tile): + """Dug out tile.""" + contents: str = " " diff --git a/day18/tests/__init__.py b/day18/tests/__init__.py index e69de29..b441fe2 100644 --- a/day18/tests/__init__.py +++ b/day18/tests/__init__.py @@ -0,0 +1 @@ +"""day18 tests.""" diff --git a/day18/tests/test_day18a.py b/day18/tests/test_day18a.py index f264471..1e1e073 100644 --- a/day18/tests/test_day18a.py +++ b/day18/tests/test_day18a.py @@ -1,7 +1,9 @@ +"""test day18a main function.""" from day18.day18a import INPUT_SMALL, Command, Direction, get_input, get_solution def test_day18a() -> None: + """Test day18a.""" commands: list[Command] = get_input(INPUT_SMALL) assert len(commands) == 14 assert commands[0].steps == 6 and commands[0].direction == Direction.Right diff --git a/day18/tests/test_day18b.py b/day18/tests/test_day18b.py index 784f056..4003bbe 100644 --- a/day18/tests/test_day18b.py +++ b/day18/tests/test_day18b.py @@ -1,7 +1,9 @@ +"""test day18b main functions.""" from day18.day18b import INPUT_SMALL, Command, Direction, get_input, get_solution def test_day18b() -> None: + """Test day18b main function.""" commands: list[Command] = get_input(INPUT_SMALL) assert len(commands) == 14 assert commands[0].steps == 461937 and commands[0].direction == Direction.Right @@ -11,6 +13,7 @@ def test_day18b() -> None: def test_command() -> None: + """Test hex code conversion.""" assert Command("70c710").steps == 461937 assert Command("0dc571").steps == 56407 assert Command("5713f0").steps == 356671