Skip to content

Types & Symmetry API

blacksquare.symmetry.Symmetry

Bases: Enum

A class representing the possible symmetry types of a crossword grid.

Source code in src/blacksquare/symmetry.py
class Symmetry(enum.Enum):
    """A class representing the possible symmetry types of a crossword grid."""

    ROTATIONAL = "rotational"
    FULL = "full"
    VERTICAL = "vertical"
    HORIZONTAL = "horizontal"
    BIAXIAL = "biaxial"
    NE_DIAGONAL = "ne_diagonal"
    NW_DIAGONAL = "nw_diagonal"

    @property
    def is_multi_image(self) -> bool:
        return self in {Symmetry.FULL, Symmetry.BIAXIAL}

    @property
    def requires_square(self) -> bool:
        return self in {Symmetry.FULL, Symmetry.NE_DIAGONAL, Symmetry.NW_DIAGONAL}

    def apply(
        self, grid: np.ndarray, force_list: bool = False
    ) -> SymmetryResult | list[SymmetryResult]:
        """Applies the symmetry group to an input array, and returns all images of the
        input under that symmetry.

        Args:
            grid: The input grid.
            force_list: Whether to return single-image symmetry groups as lists, for
                consistent typing. Defaults to False.

        Returns:
            If the symmetry type has only a single image and force_list is false,
            the return is a single SymmetryResult. Otherwise, the result is a list of
            all SymmetryResults.
        """
        if self == Symmetry.ROTATIONAL:
            images = SymmetryResult(np.rot90(grid, k=2), False)
        elif self == Symmetry.FULL:
            images = [
                SymmetryResult(np.fliplr(grid), False),
                SymmetryResult(np.flipud(grid), False),
                SymmetryResult(np.fliplr(np.flipud(grid)), False),
                SymmetryResult(np.transpose(grid), True),
                SymmetryResult(np.transpose(np.fliplr(grid)), True),
                SymmetryResult(np.transpose(np.flipud(grid)), True),
                SymmetryResult(np.transpose(np.fliplr(np.flipud(grid))), True),
            ]
        elif self == Symmetry.VERTICAL:
            images = SymmetryResult(np.fliplr(grid), False)
        elif self == Symmetry.HORIZONTAL:
            images = SymmetryResult(np.flipud(grid), False)
        elif self == Symmetry.BIAXIAL:
            images = [
                SymmetryResult(np.fliplr(grid), False),
                SymmetryResult(np.flipud(grid), False),
                SymmetryResult(np.fliplr(np.flipud(grid)), False),
            ]
        elif self == Symmetry.NE_DIAGONAL:
            images = SymmetryResult(np.transpose(np.rot90(grid, k=2)), True)
        elif self == Symmetry.NW_DIAGONAL:
            images = SymmetryResult(np.transpose(grid), True)

        if not isinstance(images, list) and force_list:
            return [images]
        else:
            return images

apply

apply(
    grid: ndarray, force_list: bool = False
) -> SymmetryResult | list[SymmetryResult]

Applies the symmetry group to an input array, and returns all images of the input under that symmetry.

Parameters:

Name Type Description Default
grid ndarray

The input grid.

required
force_list bool

Whether to return single-image symmetry groups as lists, for consistent typing. Defaults to False.

False

Returns:

Type Description
SymmetryResult | list[SymmetryResult]

If the symmetry type has only a single image and force_list is false,

SymmetryResult | list[SymmetryResult]

the return is a single SymmetryResult. Otherwise, the result is a list of

SymmetryResult | list[SymmetryResult]

all SymmetryResults.

Source code in src/blacksquare/symmetry.py
def apply(
    self, grid: np.ndarray, force_list: bool = False
) -> SymmetryResult | list[SymmetryResult]:
    """Applies the symmetry group to an input array, and returns all images of the
    input under that symmetry.

    Args:
        grid: The input grid.
        force_list: Whether to return single-image symmetry groups as lists, for
            consistent typing. Defaults to False.

    Returns:
        If the symmetry type has only a single image and force_list is false,
        the return is a single SymmetryResult. Otherwise, the result is a list of
        all SymmetryResults.
    """
    if self == Symmetry.ROTATIONAL:
        images = SymmetryResult(np.rot90(grid, k=2), False)
    elif self == Symmetry.FULL:
        images = [
            SymmetryResult(np.fliplr(grid), False),
            SymmetryResult(np.flipud(grid), False),
            SymmetryResult(np.fliplr(np.flipud(grid)), False),
            SymmetryResult(np.transpose(grid), True),
            SymmetryResult(np.transpose(np.fliplr(grid)), True),
            SymmetryResult(np.transpose(np.flipud(grid)), True),
            SymmetryResult(np.transpose(np.fliplr(np.flipud(grid))), True),
        ]
    elif self == Symmetry.VERTICAL:
        images = SymmetryResult(np.fliplr(grid), False)
    elif self == Symmetry.HORIZONTAL:
        images = SymmetryResult(np.flipud(grid), False)
    elif self == Symmetry.BIAXIAL:
        images = [
            SymmetryResult(np.fliplr(grid), False),
            SymmetryResult(np.flipud(grid), False),
            SymmetryResult(np.fliplr(np.flipud(grid)), False),
        ]
    elif self == Symmetry.NE_DIAGONAL:
        images = SymmetryResult(np.transpose(np.rot90(grid, k=2)), True)
    elif self == Symmetry.NW_DIAGONAL:
        images = SymmetryResult(np.transpose(grid), True)

    if not isinstance(images, list) and force_list:
        return [images]
    else:
        return images

blacksquare.types.Direction

Bases: Enum

An Enum representing the directions of words in a crossword.

Source code in src/blacksquare/types.py
class Direction(Enum):
    """An Enum representing the directions of words in a crossword."""

    ACROSS = "Across"
    DOWN = "Down"

    @property
    def opposite(self) -> Direction:
        if self == Direction.ACROSS:
            return Direction.DOWN
        else:
            return Direction.ACROSS

    def __lt__(self, other) -> bool:
        if isinstance(other, Direction):
            return self == Direction.ACROSS and other == Direction.DOWN
        return NotImplemented

    def __repr__(self):
        return f"<{self.value}>"

blacksquare.types.SpecialCellValue

Bases: Enum

An enum representing blank and empty cell values in a crossword.

Source code in src/blacksquare/types.py
class SpecialCellValue(Enum):
    "An enum representing blank and empty cell values in a crossword."

    BLACK = "Black"
    EMPTY = "Empty"

    @property
    def input_str_reprs(self) -> list[builtins.str]:
        if self == SpecialCellValue.BLACK:
            return [".", "#"]
        elif self == SpecialCellValue.EMPTY:
            return [" ", "-", "?", "_"]
        return []

    @property
    def str(self) -> builtins.str:
        if self == SpecialCellValue.BLACK:
            return "█"
        elif self == SpecialCellValue.EMPTY:
            return " "
        return ""

    def __repr__(self):
        return f"<{self.value}>"

blacksquare.types.Rebus

Represents a rebus cell in a crossword with across and down text values.

Source code in src/blacksquare/types.py
class Rebus:
    """Represents a rebus cell in a crossword with across and down text values."""

    _across: builtins.str
    _down: builtins.str

    def __init__(
        self,
        value: builtins.str | None = None,
        down: builtins.str | None = None,
        *,
        across: builtins.str | None = None,
    ) -> None:
        if value is not None and across is not None:
            raise ValueError(
                "Cannot specify both positional value and keyword 'across'"
            )

        if (
            value is not None
            and isinstance(value, str)
            and "/" in value
            and down is None
            and across is None
        ):
            parts = value.split("/")
            if len(parts) == 2:
                act_across = parts[0]
                act_down = parts[1]
            else:
                act_across = value
                act_down = value
        else:
            act_across = across if across is not None else value
            act_down = down

        if act_across is None and act_down is None:
            raise ValueError("Must specify at least one rebus value")
        elif act_across is not None and act_down is None:
            act_down = act_across
        elif act_across is None and act_down is not None:
            act_across = act_down

        assert act_across is not None and act_down is not None

        if not isinstance(act_across, str) or not isinstance(act_down, str):
            raise ValueError("Rebus values must be strings")

        clean_across = act_across.strip().upper()
        clean_down = act_down.strip().upper()

        if not clean_across or not clean_down:
            raise ValueError("Rebus values cannot be empty")

        self._across = clean_across
        self._down = clean_down

    @property
    def across(self) -> builtins.str:
        """The across string value of the rebus."""
        return self._across

    @property
    def down(self) -> builtins.str:
        """The down string value of the rebus."""
        return self._down

    @property
    def value(self) -> builtins.str | tuple[builtins.str, builtins.str]:
        """The value of the rebus. Returns str if symmetric, else (across, down) tuple."""
        if self.is_symmetric:
            return self._across
        return (self._across, self._down)

    @property
    def is_symmetric(self) -> bool:
        """Whether the across and down values are identical."""
        return self._across == self._down

    def get_value(self, direction: Direction) -> builtins.str:
        """Returns the string value for the given direction."""
        if direction == Direction.ACROSS:
            return self._across
        elif direction == Direction.DOWN:
            return self._down
        else:
            raise ValueError(f"Invalid direction: {direction}")

    def __str__(self) -> builtins.str:
        if self.is_symmetric:
            return self._across
        return f"{self._across}/{self._down}"

    def __repr__(self) -> builtins.str:
        if self.is_symmetric:
            return f"Rebus({repr(self._across)})"
        return f"Rebus(across={repr(self._across)}, down={repr(self._down)})"

    def __eq__(self, other: object) -> bool:
        if isinstance(other, Rebus):
            return self._across == other._across and self._down == other._down
        elif isinstance(other, str):
            clean = other.strip().upper()
            return self._across == clean and self._down == clean
        return False

    def __hash__(self) -> int:
        return hash((self._across, self._down))

across property

across: str

The across string value of the rebus.

down property

down: str

The down string value of the rebus.

is_symmetric property

is_symmetric: bool

Whether the across and down values are identical.

value property

value: str | tuple[str, str]

The value of the rebus. Returns str if symmetric, else (across, down) tuple.

get_value

get_value(direction: Direction) -> builtins.str

Returns the string value for the given direction.

Source code in src/blacksquare/types.py
def get_value(self, direction: Direction) -> builtins.str:
    """Returns the string value for the given direction."""
    if direction == Direction.ACROSS:
        return self._across
    elif direction == Direction.DOWN:
        return self._down
    else:
        raise ValueError(f"Invalid direction: {direction}")