Skip to content

Commit

Permalink
Fixed empty puzzle in show method
Browse files Browse the repository at this point in the history
  • Loading branch information
joshbduncan committed Jun 21, 2024
1 parent c6a77e4 commit c7bdea7
Show file tree
Hide file tree
Showing 6 changed files with 28 additions and 32 deletions.
2 changes: 1 addition & 1 deletion CHANGLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- added `-hk`, `--hide-key` to cli and `WordSearch.show()`, and `WordSearch.save()` methods, allowing user to hide the answer key during output
- only applies to cli output and saved PDF files
- the answer key will always be output on the solution page of a pdf
- `NoValidWordsError` for when a puzzle is generated but no valid words are available

### Fixed

- Bug creating false negatives in `WordSearchGenerator.no_duped_words()` method that is used when placing new words and filler characters
- Empty puzzle shown with the `show` method was called on a puzzle that has not been generated yet, or a puzzle with no placed/valid words.

### Changed

Expand Down
11 changes: 7 additions & 4 deletions src/word_search_generator/core/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from pathlib import Path
from typing import Iterable, Sized, TypeAlias

from .. import utils
from ..core.formatter import Formatter
from ..core.generator import Generator
from ..mask import CompoundMask, Mask
from ..utils import BoundingBox, find_bounding_box
from .directions import LEVEL_DIRS, Direction
from .validator import Validator
from .word import KeyInfo, KeyInfoJson, Word
Expand Down Expand Up @@ -182,10 +182,10 @@ def masked(self) -> bool:
return bool(self.masks)

@property
def bounding_box(self) -> tuple[tuple[int, int], tuple[int, int]]:
def bounding_box(self) -> BoundingBox:
"""Bounding box of the active puzzle area as a rectangle defined
by a tuple of (top-left edge as x, y, bottom-right edge as x, y)"""
return utils.find_bounding_box(self.mask, self.ACTIVE)
return find_bounding_box(self.mask, self.ACTIVE)

@property
def cropped_puzzle(self) -> Puzzle:
Expand Down Expand Up @@ -312,8 +312,11 @@ def show(self, *args, **kwargs) -> None:
"""Show the current puzzle with or without the solution.
Raises:
EmptyPuzzleError: Puzzle not yet generated or puzzle has no placed words.
MissingFormatterError: No puzzle formatter set.
"""
if not self.puzzle:
raise EmptyPuzzleError()
if not self.formatter:
raise MissingFormatterError()
print(self.formatter.show(self, *args, **kwargs))
Expand All @@ -326,7 +329,7 @@ def save(self, path: str | Path, format: str = "PDF", *args, **kwargs) -> str:
format: Type of file to save ("CSV", "JSON", "PDF"). Defaults to "PDF".
Raises:
EmptyPuzzleError: Puzzle not yet generated.
EmptyPuzzleError: Puzzle not yet generated or puzzle has no placed words.
MissingFormatterError: No puzzle formatter set.
Returns:
Expand Down
23 changes: 10 additions & 13 deletions src/word_search_generator/core/word.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from rich.style import Style

from ..utils import BoundingBox
from .game import Direction
from .validator import Validator

Expand Down Expand Up @@ -53,13 +54,14 @@ def validate(
"""Validate the word against a list of validators.
Args:
validators (list[Validator]): Validators to test.
validators: Validators to test.
placed_words: Currently placed puzzle words.
Raises:
TypeError: Incorrect validator type provided.
Returns:
bool: Word passes all validators.
Word passes all validators.
"""
for validator in validators:
if not isinstance(validator, Validator):
Expand Down Expand Up @@ -100,7 +102,7 @@ def position(self, value: Position) -> None:
"""Set the start position of the Word in the puzzle.
Args:
val (Position): Tuple of (row, column)
value: Tuple of (row, column)
"""
self.start_row = value.row
self.start_column = value.column
Expand Down Expand Up @@ -147,7 +149,7 @@ def key_info_json(self) -> KeyInfoJson:

def key_string(
self,
bbox: tuple[tuple[int, int], tuple[int, int]],
bbox: BoundingBox,
lowercase: bool = False,
reversed_letters: bool = False,
) -> str:
Expand All @@ -161,7 +163,6 @@ def key_string(
lowercase: Should words be lowercase. Defaults to False.
reversed_letters: Should words letters be reversed. Defaults to False.
Returns:
Word placement information.
"""
Expand All @@ -178,16 +179,14 @@ def key_string(
+ f" @ {(col, row)}"
)

def offset_position_xy(
self, bbox: tuple[tuple[int, int], tuple[int, int]]
) -> Position:
def offset_position_xy(self, bbox: BoundingBox) -> Position:
"""Returns a string representation of the word position with
1-based indexing and a familiar (x, y) coordinate system. The
position will be offset by the puzzle bounding box when a puzzle
has been masked.
Args:
bbox (tuple[tuple[int, int], tuple[int, int]]): The current
bbox (BoundingBox): The current
puzzle bounding box.
"""
return Position(
Expand All @@ -203,14 +202,12 @@ def offset_position_xy(
),
)

def offset_coordinates(
self, bbox: tuple[tuple[int, int], tuple[int, int]]
) -> list[Position]:
def offset_coordinates(self, bbox: BoundingBox) -> list[Position]:
"""Returns a list of the Word letter coordinates, offset
by the puzzle bounding box.
Args:
bbox (tuple[tuple[int, int], tuple[int, int]]): The current
bbox (BoundingBox): The current
puzzle bounding box.
"""
return [
Expand Down
4 changes: 2 additions & 2 deletions src/word_search_generator/mask/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ..utils import find_bounding_box
from ..utils import BoundingBox, find_bounding_box


class MaskNotGenerated(Exception):
Expand Down Expand Up @@ -112,7 +112,7 @@ def puzzle_size(self, value: int) -> None:
self.reset_points()

@property
def bounding_box(self) -> tuple[tuple[int, int], tuple[int, int]] | None:
def bounding_box(self) -> BoundingBox | None:
"""Bounding box of the masked area as a rectangle defined
by a tuple of (top-left edge as x, y, bottom-right edge as x, y). Returned
points may lie outside of the puzzle bounds. This property is used
Expand Down
15 changes: 8 additions & 7 deletions src/word_search_generator/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

import math
import random
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypeAlias

from .words import WORD_LIST

if TYPE_CHECKING: # pragma: no cover
from .core.game import DirectionSet, Key, Puzzle, WordSet


BoundingBox: TypeAlias = tuple[tuple[int, int], tuple[int, int]]


def round_half_up(n: float, decimals: int = 0) -> float:
"""Round numbers in a consistent and familiar format."""
multiplier: int = 10**decimals
Expand Down Expand Up @@ -50,7 +53,7 @@ def in_bounds(x: int, y: int, width: int, height: int) -> bool:
def find_bounding_box(
grid: list[list[str]],
edge: str,
) -> tuple[tuple[int, int], tuple[int, int]]:
) -> BoundingBox:
"""Bounding box of the masked area as a rectangle defined
by a tuple of (top-left edge as x, y, bottom-right edge as x, y)"""
size = len(grid)
Expand Down Expand Up @@ -78,7 +81,7 @@ def find_bounding_box(
return ((min_x, min_y), (max_x, max_y))


def stringify(puzzle: Puzzle, bbox: tuple[tuple[int, int], tuple[int, int]]) -> str:
def stringify(puzzle: Puzzle, bbox: BoundingBox) -> str:
"""Convert puzzle array of nested lists into a string."""
min_x, min_y = bbox[0]
max_x, max_y = bbox[1]
Expand Down Expand Up @@ -110,7 +113,7 @@ def get_word_list_list(key: Key) -> list[str]:

def get_answer_key_list(
words: WordSet,
bbox: tuple[tuple[int, int], tuple[int, int]],
bbox: BoundingBox,
lowercase: bool = False,
reversed_letters: bool = False,
) -> list[str]:
Expand All @@ -132,9 +135,7 @@ def get_answer_key_list(
]


def get_answer_key_str(
words: WordSet, bbox: tuple[tuple[int, int], tuple[int, int]]
) -> str:
def get_answer_key_str(words: WordSet, bbox: BoundingBox) -> str:
"""Return a easy to read answer key for display. Resulting coordinates
will be offset by the supplied values. Used for masked puzzles.
Expand Down
5 changes: 0 additions & 5 deletions src/word_search_generator/word_search/word_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
Game,
MissingGeneratorError,
MissingWordError,
NoValidWordsError,
PuzzleSizeError,
WordSet,
)
Expand Down Expand Up @@ -183,8 +182,6 @@ def show(
lowercase: bool = False,
hide_key: bool = False,
reversed_letters: bool = False,
*args,
**kwargs,
):
return super().show(
solution=solution,
Expand Down Expand Up @@ -296,8 +293,6 @@ def generate(self, reset_size: bool = False) -> None:
if not self.mask or len(self.mask) != self.size:
self._mask = self._build_puzzle(self.size, self.ACTIVE)
self._puzzle = self.generator.generate(self)
if not self.masked and not self.placed_words:
raise NoValidWordsError("No valid words have been added to the puzzle.")
if self.require_all_words and self.unplaced_hidden_words:
raise MissingWordError("All words could not be placed in the puzzle.")

Expand Down

0 comments on commit c7bdea7

Please sign in to comment.