Skip to content

Commit

Permalink
docs: day18
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-ong committed Dec 30, 2023
1 parent 38247b5 commit ca683ab
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 6 deletions.
1 change: 1 addition & 0 deletions day18/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Day18 solution."""
73 changes: 67 additions & 6 deletions day18/day18a.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""day18 solution"""
"""day18 solution."""

from dataclasses import dataclass
from enum import StrEnum
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -51,6 +72,8 @@ def generate_offsets(


class Matrix:
"""2d array representing world."""

contents: list[list[Tile]]

min_pos: Position
Expand All @@ -63,23 +86,33 @@ 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 = [
[Tile() for _ in range(self.num_cols)] for _ in range(self.num_rows)
]

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:
self.contents[offset.row][offset.col] = EdgeTile(color=command.color)
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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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)

Expand Down
13 changes: 13 additions & 0 deletions day18/day18b.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Day18b solution."""
from dataclasses import dataclass
from enum import IntEnum

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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()

Expand All @@ -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))
Expand Down
1 change: 1 addition & 0 deletions day18/lib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""day18 library modules."""
10 changes: 10 additions & 0 deletions day18/lib/tile.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
"""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

# 38 -> 48 for background
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}"


@dataclass(kw_only=True)
class HoleTile(Tile):
"""Dug out tile."""

contents: str = " "
1 change: 1 addition & 0 deletions day18/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""day18 tests."""
2 changes: 2 additions & 0 deletions day18/tests/test_day18a.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions day18/tests/test_day18b.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down

0 comments on commit ca683ab

Please sign in to comment.