Skip to content

Commit

Permalink
docs: day16
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-ong committed Dec 29, 2023
1 parent 65334d3 commit 4c3ce21
Show file tree
Hide file tree
Showing 12 changed files with 62 additions and 35 deletions.
1 change: 1 addition & 0 deletions day16/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Day16 solution."""
11 changes: 6 additions & 5 deletions day16/day16.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""day16 solution
"""
"""day16 solution."""


import os
Expand All @@ -16,23 +15,25 @@


def solve_task(task: Laser, world: World) -> int:
"""Calculates number of energized tiles"""
"""Calculates number of energized tiles."""
return world.solve(task).num_energized()


def solve_task_wrapper(args: tuple[Laser, World]) -> int:
"""Wraps solve_task in multiprocessing, since it only takes one arg"""
"""Wraps solve_task in multiprocessing, since it only takes one arg."""
task, world = args
return solve_task(task, world)


def part1(world: World) -> int:
"""Return number of energized tiles."""
laser = Laser(0, 0, Direction.EAST)
solved_world = world.solve(laser)
return solved_world.num_energized()


def part2(world: World) -> int:
"""Calculate most energized tiles by firing laser from every entrypoint."""
tasks = []
# part 2: brute force coz our impl is already cached/backtracked
# 1T -> 3.3s
Expand All @@ -54,7 +55,7 @@ def part2(world: World) -> int:


def main() -> None:
"""Main function"""
"""Read input and run part1/part2."""
world = get_input(INPUT)

print(part1(world))
Expand Down
8 changes: 5 additions & 3 deletions day16/lib/direction.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
"""Direction class
"""
"""Direction class."""
from enum import IntEnum


class Direction(IntEnum):
"""Simple direction enum"""
"""Simple direction enum."""

NORTH = 0
EAST = 1
SOUTH = 2
WEST = 3

def __str__(self) -> str:
"""Return ``NORTH`` etc."""
return self.name

def opposite(self) -> "Direction":
"""Returns opposite direction."""
int_value = int(self)
return Direction((int_value + 2) % len(Direction))

def offset(self, row: int, col: int) -> tuple[int, int]:
"""Offset a row/col by our direction."""
if self == Direction.NORTH:
return (row - 1, col)
if self == Direction.EAST:
Expand Down
9 changes: 4 additions & 5 deletions day16/lib/laser.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
"""laser instance class"""
"""laser instance class."""
from dataclasses import dataclass

from day16.lib.direction import Direction


@dataclass
@dataclass(frozen=True) # frozen so we can hash
class Laser:
"""Laser position + direction."""

row: int
col: int

direction: Direction

def __hash__(self) -> int:
return hash(str(self.row) + ":" + str(self.col) + "|" + str(self.direction))
3 changes: 2 additions & 1 deletion day16/lib/parsers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Parsers for input file."""
from day16.lib.cells import Cell
from day16.lib.world import World


def get_input(path: str) -> World:
"""Read input file"""
"""Read input file and return well formed :class:World."""
with open(path) as file:
all_cells: list[list[Cell]] = []
for line in file:
Expand Down
17 changes: 13 additions & 4 deletions day16/lib/world.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
"""Well defined world classes."""
from dataclasses import dataclass, field

from day16.lib.cells import Cell
from day16.lib.laser import Laser


class SolvedWorld:
"""A solved world class, stores how many lasers are in each tile."""

data: list[list[list[Laser]]]

def __init__(self, num_rows: int, num_cols: int):
"""Initialises empty 2d array of lists of lasers."""
self.data = [[[] for _ in range(num_cols)] for _ in range(num_rows)]

def already_solved(self, laser: Laser) -> bool:
"""Returns true if laser already calculated"""
"""Returns true if laser already calculated."""
solutions = self.data[laser.row][laser.col]
if laser in solutions:
return True
return False

def add_laser(self, laser: Laser) -> None:
"""Adds laser to cell"""
"""Adds laser to cell."""
solutions = self.data[laser.row][laser.col]
solutions.append(laser)

def __str__(self) -> str:
"""Custom str to show how many lasers on each tile."""
row_strs = []
for row in self.data:
row_str = "".join(str(len(col)) for col in row)
row_strs.append(row_str)
return "\n".join(row_strs)

def num_energized(self) -> int:
"""Return number of energized cells"""
"""Return number of energized cells."""
result: int = 0
for row in self.data:
result += sum(1 if len(col) >= 1 else 0 for col in row)
Expand All @@ -39,16 +44,20 @@ def num_energized(self) -> int:

@dataclass
class World:
"""The input world (mirrors/empty tiles)."""

data: list[list[Cell]]

num_rows: int = field(init=False, repr=False)
num_cols: int = field(init=False, repr=False)

def __post_init__(self) -> None:
"""Initializes our num_rows/num_cols."""
self.num_rows = len(self.data)
self.num_cols = len(self.data[0])

def solve(self, start_laser: Laser) -> SolvedWorld:
"""Solve our world."""
solved_world = SolvedWorld(self.num_rows, self.num_cols)
active_lasers = [start_laser]
while len(active_lasers) > 0:
Expand All @@ -65,7 +74,7 @@ def solve(self, start_laser: Laser) -> SolvedWorld:
return solved_world

def is_oob(self, laser: Laser) -> bool:
"""True if laser is out of bounds"""
"""True if laser is out of bounds."""
return (
laser.row < 0
or laser.row >= self.num_rows
Expand Down
1 change: 1 addition & 0 deletions day16/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for day16."""
36 changes: 21 additions & 15 deletions day16/tests/test_cells.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Tests for each cell."""
from day16.lib.cells import (
BackSlashCell,
Cell,
Expand All @@ -16,79 +17,84 @@


def test_dotcell() -> None:
"""Test ``DotCell``."""
laser: Laser = Laser(5, 5, Direction.NORTH)
cell: Cell = DotCell()
assert cell.next_lasers(laser) == [NORTH_LASER]

laser.direction = Direction.EAST
laser = Laser(5, 5, Direction.EAST)
assert cell.next_lasers(laser) == [EAST_LASER]

laser.direction = Direction.WEST
laser = Laser(5, 5, Direction.WEST)
assert cell.next_lasers(laser) == [WEST_LASER]

laser.direction = Direction.SOUTH
laser = Laser(5, 5, Direction.SOUTH)
assert cell.next_lasers(laser) == [SOUTH_LASER]


def test_dashcell() -> None:
"""Test ``DashCell``."""
laser: Laser = Laser(5, 5, Direction.NORTH)
cell: Cell = DashCell()

assert set(cell.next_lasers(laser)) == {WEST_LASER, EAST_LASER}

laser.direction = Direction.EAST
laser = Laser(5, 5, Direction.EAST)
assert cell.next_lasers(laser) == [EAST_LASER]

laser.direction = Direction.WEST
laser = Laser(5, 5, Direction.WEST)
assert cell.next_lasers(laser) == [WEST_LASER]

laser.direction = Direction.SOUTH
laser = Laser(5, 5, Direction.SOUTH)
assert set(cell.next_lasers(laser)) == {WEST_LASER, EAST_LASER}


def test_pipecell() -> None:
"""Test ``PipeCell``."""
laser: Laser = Laser(5, 5, Direction.NORTH)
cell: Cell = PipeCell()

assert cell.next_lasers(laser) == [NORTH_LASER]

laser.direction = Direction.EAST
laser = Laser(5, 5, Direction.EAST)
assert set(cell.next_lasers(laser)) == {NORTH_LASER, SOUTH_LASER}

laser.direction = Direction.WEST
laser = Laser(5, 5, Direction.WEST)
assert set(cell.next_lasers(laser)) == {NORTH_LASER, SOUTH_LASER}

laser.direction = Direction.SOUTH
laser = Laser(5, 5, Direction.SOUTH)
assert cell.next_lasers(laser) == [SOUTH_LASER]


def test_forwardslashcell() -> None:
"""Test ``ForwardSlashCell``."""
laser: Laser = Laser(5, 5, Direction.NORTH)
cell: Cell = ForwardSlashCell()

assert cell.next_lasers(laser) == [EAST_LASER]

laser.direction = Direction.EAST
laser = Laser(5, 5, Direction.EAST)
assert cell.next_lasers(laser) == [NORTH_LASER]

laser.direction = Direction.WEST
laser = Laser(5, 5, Direction.WEST)
assert cell.next_lasers(laser) == [SOUTH_LASER]

laser.direction = Direction.SOUTH
laser = Laser(5, 5, Direction.SOUTH)
assert cell.next_lasers(laser) == [WEST_LASER]


def test_backslashcell() -> None:
"""Test ``BackSlashCell``."""
laser: Laser = Laser(5, 5, Direction.NORTH)
cell: Cell = BackSlashCell()

assert cell.next_lasers(laser) == [WEST_LASER]

laser.direction = Direction.EAST
laser = Laser(5, 5, Direction.EAST)
assert cell.next_lasers(laser) == [SOUTH_LASER]

laser.direction = Direction.WEST
laser = Laser(5, 5, Direction.WEST)
assert cell.next_lasers(laser) == [NORTH_LASER]

laser.direction = Direction.SOUTH
laser = Laser(5, 5, Direction.SOUTH)
assert cell.next_lasers(laser) == [EAST_LASER]
3 changes: 3 additions & 0 deletions day16/tests/test_day16.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Test day16 main functions."""
from typing import TYPE_CHECKING

from day16.day16 import INPUT_SMALL, part1, part2
Expand All @@ -8,10 +9,12 @@


def test_part1() -> None:
"""Test part1."""
world: World = get_input(INPUT_SMALL)
assert part1(world) == 46


def test_part2() -> None:
"""Test part2."""
world: World = get_input(INPUT_SMALL)
assert part2(world) == 51
2 changes: 2 additions & 0 deletions day16/tests/test_direction.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Test direction."""
from day16.lib.direction import Direction


def test_direction() -> None:
"""Test ``Direction`` class."""
assert Direction.WEST.opposite() == Direction.EAST
assert Direction.EAST.opposite() == Direction.WEST
assert Direction.SOUTH.opposite() == Direction.NORTH
Expand Down
2 changes: 2 additions & 0 deletions day16/tests/test_world.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Test World class."""
from typing import TYPE_CHECKING

from day16.day16 import INPUT_SMALL
Expand All @@ -10,6 +11,7 @@


def test_world() -> None:
"""Test ``World`` class."""
world: World = get_input(INPUT_SMALL)
start_laser: Laser = Laser(0, 0, Direction.EAST)
solved_world: SolvedWorld = world.solve(start_laser)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ ignore = ["SIM108"]
docstring-code-format = true

# Todo: unmark this once we hit 100% comments
[tool.ruff.lint]
select = ["D"]
#[tool.ruff.lint]
#select = ["D"]

[tool.ruff.lint.pydocstyle]
convention = "google"
Expand Down

0 comments on commit 4c3ce21

Please sign in to comment.