From a562883c18b41633db95a59c92b869abe48567de Mon Sep 17 00:00:00 2001 From: alexo Date: Thu, 28 Dec 2023 15:08:36 +1100 Subject: [PATCH] day21: coverage --- day21/day21.py | 17 +++++++----- day21/input-cleaner.txt | 9 ------- day21/lib/classes.py | 53 ++++++++++++++++--------------------- day21/lib/parsers.py | 4 +-- day21/tests/input-11x11.txt | 11 ++++++++ day21/tests/input-5x5.txt | 5 ++++ day21/tests/test_classes.py | 37 ++++++++++++++++++++++++++ day21/tests/test_day21.py | 28 ++++++++++++++++++++ day21/tests/test_parsers.py | 10 +++++++ 9 files changed, 125 insertions(+), 49 deletions(-) delete mode 100644 day21/input-cleaner.txt create mode 100644 day21/tests/input-11x11.txt create mode 100644 day21/tests/input-5x5.txt create mode 100644 day21/tests/test_classes.py create mode 100644 day21/tests/test_day21.py create mode 100644 day21/tests/test_parsers.py diff --git a/day21/day21.py b/day21/day21.py index 63bd55d..c4f2781 100644 --- a/day21/day21.py +++ b/day21/day21.py @@ -23,7 +23,6 @@ FILE_SMALL = "day21/input-small.txt" FILE_MAIN = "day21/input.txt" -FILE_CLEANER = "day21/input-cleaner.txt" FILE = FILE_MAIN @@ -37,10 +36,12 @@ def mini_solve( nodes: Queue[PositionDist] = Queue() nodes.put(PositionDist(start_pos.row, start_pos.col, distance=0)) - while not nodes.empty(): + distance_reached: bool = False + while not nodes.empty() and not distance_reached: pos: PositionDist = nodes.get() if pos.distance >= steps + 1: - break + distance_reached = True + continue # expand distance: Optional[int] = distances[pos] maze_node: Optional[str] = maze[pos] @@ -99,11 +100,13 @@ def solve( print(distances.overlay(maze)) return distances.calc_steps(sim_steps % 2) - # big - - print("brute force", distances.calc_steps(sim_steps % 2)) + # print("brute force", distances.calc_steps(sim_steps % 2)) if not isinstance(distances, DistanceMazes): - raise ValueError("ya done goof here") + raise AssertionError("ya done goof here") + + if maze.num_cols <= 5: + distances.overlay(maze) + giant_parser = GiantNodeParser(distances, boards_to_edge) remainder = steps % 2 result = 0 diff --git a/day21/input-cleaner.txt b/day21/input-cleaner.txt deleted file mode 100644 index 51e71c5..0000000 --- a/day21/input-cleaner.txt +++ /dev/null @@ -1,9 +0,0 @@ -......... -......... -......... -......... -....S.... -......... -......... -......... -......... \ No newline at end of file diff --git a/day21/lib/classes.py b/day21/lib/classes.py index 52ef45d..1cc563e 100644 --- a/day21/lib/classes.py +++ b/day21/lib/classes.py @@ -1,7 +1,7 @@ """classes""" -from abc import ABC +from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass from enum import Enum @@ -59,41 +59,35 @@ def __init__(self, data: list[str]) -> None: def __str__(self) -> str: return "\n".join(row for row in self.grid) - def __getitem__(self, position: Position, wrap: bool = True) -> Optional[str]: - """Get item via position. Returns None if out of bounds""" + def __getitem__(self, position: Position) -> Optional[str]: + """Get item via position. Always wraps""" if not isinstance(position, Position): - raise ValueError(f"position is not a Position, {type(position)}") - if not wrap and self.is_oob(position): - return None + raise AssertionError(f"position is not a Position, {type(position)}") + row = position.row % self.num_rows col = position.col % self.num_cols return self.grid[row][col] - def is_oob(self, position: Position) -> bool: - """true if position is out of bounds""" - return ( - position.row < 0 - or position.row >= self.num_rows - or position.col < 0 - or position.col >= self.num_cols - ) - class BaseDistanceMaze(ABC): + @abstractmethod def overlay(self, maze: Maze) -> str: - return "" + raise AssertionError("Not implemented") + @abstractmethod def calc_steps(self, remainder: int) -> int: """ calc steps, matching remainder == 1 or remainder == 0 - when modulo'ing by 2""" - return 0 + when modulo'ing by 2 + """ + @abstractmethod def __setitem__(self, position: Position, value: int) -> None: - raise NotImplementedError() + """Sets the 2d arrays value based on a position""" + @abstractmethod def __getitem__(self, position: Position) -> Optional[int]: - raise NotImplementedError() + """Get the integer distance based on position""" class DistanceMaze(BaseDistanceMaze): @@ -110,7 +104,7 @@ def __init__(self, num_rows: int, num_cols: int) -> None: def __setitem__(self, position: Position, value: int) -> None: """Sets item at given position. Silently fails if out of bounds""" if not isinstance(position, Position): - raise ValueError(f"position is not a Position, {type(position)}") + raise AssertionError(f"position is not a Position, {type(position)}") if self.is_oob(position): return self.grid[position.row][position.col] = value @@ -118,7 +112,7 @@ def __setitem__(self, position: Position, value: int) -> None: def __getitem__(self, position: Position) -> Optional[int]: """Get item via position. Returns None if out of bounds""" if not isinstance(position, Position): - raise ValueError(f"position is not a Position, {type(position)}") + raise AssertionError(f"position is not a Position, {type(position)}") if self.is_oob(position): return None return self.grid[position.row][position.col] @@ -205,12 +199,12 @@ def __getitem__(self, position: Position) -> int: result = self.grid[big_pos][sub_pos] if result is None: - raise ValueError("Unexpected result, None") + raise AssertionError("Unexpected result, None") return result def __setitem__(self, position: Position, value: int) -> None: if not isinstance(position, Position): - raise ValueError(f"position is not a Position, {type(position)}") + raise AssertionError(f"position is not a Position, {type(position)}") big_pos, sub_pos = self.get_split_pos(position) self.grid[big_pos][sub_pos] = value @@ -230,7 +224,7 @@ def overlay(self, maze: Maze) -> str: cols = sorted([coord.col for coord in coords]) if len(rows) == 0: - min_row, max_row, min_col, max_col = 0, 0, 0, 0 + raise AssertionError("grid has no subgrids!") else: min_row, max_row = rows[0], rows[-1] min_col, max_col = cols[0], cols[-1] @@ -250,9 +244,8 @@ def overlay(self, maze: Maze) -> str: return result def calc_steps(self, remainder: int) -> int: - result = 0 - for sub_grid in self.grid.values(): - result += sub_grid.calc_steps(remainder) + distance_mazes = self.grid.values() + result = sum(sub_grid.calc_steps(remainder) for sub_grid in distance_mazes) return result @@ -331,7 +324,7 @@ def get_node(self, node_type: GiantNodeType) -> DistanceMaze: # noqa: C901 return self.distance_mazes.get_big_grid(Position(-1, -self.edge_dist + 1)) elif node_type == GiantNodeType.NORTH_WEST_SMALL: return self.distance_mazes.get_big_grid(Position(-1, -self.edge_dist)) - raise ValueError(f"Unknown node_type: {node_type}") + raise AssertionError(f"Unknown node_type: {node_type}") def get_node_count(self, node_type: GiantNodeType) -> int: remainder = self.full_edge_dist % 2 @@ -366,4 +359,4 @@ def get_node_count(self, node_type: GiantNodeType) -> int: GiantNodeType.SOUTH_WEST_SMALL, ]: return self.full_edge_dist - raise ValueError(f"Unknown node type: {node_type}") + raise AssertionError(f"Unknown node type: {node_type}") diff --git a/day21/lib/parsers.py b/day21/lib/parsers.py index 6aa9da0..229a5e0 100644 --- a/day21/lib/parsers.py +++ b/day21/lib/parsers.py @@ -7,12 +7,10 @@ def parse_maze(filename: str) -> tuple[Position, Maze]: with open(filename, encoding="utf8") as file: for row, line in enumerate(file): line = line.strip() - if len(line) == 0: - continue if "S" in line: col = line.index("S") start_pos = Position(row, col) maze_rows.append(line) if start_pos is None: - raise ValueError("no start position!") + raise AssertionError("no start position!") return start_pos, Maze(maze_rows) diff --git a/day21/tests/input-11x11.txt b/day21/tests/input-11x11.txt new file mode 100644 index 0000000..fd0db6b --- /dev/null +++ b/day21/tests/input-11x11.txt @@ -0,0 +1,11 @@ +........... +........... +........... +........... +........... +.....S..... +........... +........... +........... +........... +........... diff --git a/day21/tests/input-5x5.txt b/day21/tests/input-5x5.txt new file mode 100644 index 0000000..417ae59 --- /dev/null +++ b/day21/tests/input-5x5.txt @@ -0,0 +1,5 @@ +..... +..... +..S.. +..... +..... diff --git a/day21/tests/test_classes.py b/day21/tests/test_classes.py new file mode 100644 index 0000000..3e34827 --- /dev/null +++ b/day21/tests/test_classes.py @@ -0,0 +1,37 @@ +import pytest + +from day21.lib.classes import ( + DistanceMaze, + DistanceMazes, + GiantNodeParser, + Maze, + Position, + PositionDist, +) + + +def test_position() -> None: + position: Position = Position(21, 22) + assert str(position) == "21, 22" + + +def test_position_dist() -> None: + pos_dist = PositionDist(21, 22, distance=69) + assert str(pos_dist) == "21, 22, d=69" + + +def test_maze() -> None: + maze: Maze = Maze(["...", ".S.", "..."]) + + assert str(maze) == "...\n.S.\n..." + + +def test_distance_maze() -> None: + maze: DistanceMaze = DistanceMaze(5, 5) + maze[Position(69, 69)] = 22 + assert maze[Position(69, 69)] is None + + +def test_giant_node_parser() -> None: + with pytest.raises(ValueError): + GiantNodeParser(DistanceMazes(5, 5), 2) diff --git a/day21/tests/test_day21.py b/day21/tests/test_day21.py new file mode 100644 index 0000000..5c43e38 --- /dev/null +++ b/day21/tests/test_day21.py @@ -0,0 +1,28 @@ +import pytest + +from day21.day21 import FILE_SMALL, solve +from day21.lib.parsers import parse_maze + +INPUT_11x11 = "day21/tests/input-11x11.txt" +INPUT_5x5 = "day21/tests/input-5x5.txt" + + +def test_day21() -> None: + start_pos, maze = parse_maze(FILE_SMALL) + + assert solve(start_pos, maze, 6) == 16 + assert solve(start_pos, maze, 30) == 42 + + start_pos, maze = parse_maze(INPUT_5x5) + with pytest.raises(ValueError): + solve(start_pos, maze, maze.num_rows, True) + + assert solve(start_pos, maze, int(maze.num_rows * 3.5), True) == 324 + assert solve(start_pos, maze, int(maze.num_rows * 4.5), True) == 529 + + start_pos, maze = parse_maze(INPUT_11x11) + with pytest.raises(ValueError): + solve(start_pos, maze, maze.num_rows, True) + + assert solve(start_pos, maze, int(maze.num_rows * 3.5), True) == 1521 + assert solve(start_pos, maze, int(maze.num_rows * 4.5), True) == 2500 diff --git a/day21/tests/test_parsers.py b/day21/tests/test_parsers.py new file mode 100644 index 0000000..0d4184f --- /dev/null +++ b/day21/tests/test_parsers.py @@ -0,0 +1,10 @@ +from day21.day21 import FILE_SMALL +from day21.lib.parsers import parse_maze + + +def test_parser() -> None: + start_pos, maze = parse_maze(FILE_SMALL) + assert start_pos.col == 5 + assert start_pos.row == 5 + assert maze.num_cols == 11 + assert maze.num_cols == 11