Source code for dedoku.units

"""House models: rows, columns, and subgrids.

A *house* (called a *unit* in this library) is any group of nine cells that
must contain each digit exactly once. The shared behaviour lives in
:class:`Unit`; :class:`Row`, :class:`Column`, and :class:`Subgrid` are thin
subclasses that mostly provide a human-readable identity.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Iterator

from .cell import DIGITS

if TYPE_CHECKING:
    from .cell import Cell

__all__ = ["Unit", "Row", "Column", "Subgrid"]


[docs] class Unit: """Base class for any house of nine cells. :param index: Zero-based index of the unit within its kind (0-8). :type index: int :raises ValueError: If ``index`` is outside the 0-8 range. :cvar kind: Lower-case human-readable name of the unit kind (``"row"``, ``"column"``, or ``"subgrid"``). """ kind: str = "unit" def __init__(self, index: int) -> None: if index not in range(9): raise ValueError(f"unit index must be within 0-8, got {index}") self._index = index self._cells: list[Cell] = [] self._cells_view: tuple[Cell, ...] | None = None
[docs] def register(self, cell: Cell) -> None: """Add ``cell`` to the unit while the grid is being built. This is called by :meth:`dedoku.cell.Cell.attach` and must not be used afterwards. :param cell: The cell to register. :type cell: Cell :raises RuntimeError: If the unit already holds nine cells. """ if len(self._cells) >= 9: raise RuntimeError(f"{self.name} already holds nine cells") self._cells.append(cell) self._cells_view = None
# ------------------------------------------------------------------ # Identity # ------------------------------------------------------------------ @property def index(self) -> int: """int: Zero-based index of the unit within its kind.""" return self._index @property def name(self) -> str: """str: Human-readable, one-based name such as ``row 3``.""" return f"{self.kind} {self._index + 1}" # ------------------------------------------------------------------ # Cell access # ------------------------------------------------------------------ @property def cells(self) -> tuple[Cell, ...]: """tuple[Cell, ...]: The nine cells of the unit, in board order. The tuple is cached, since the wiring never changes once built. """ if self._cells_view is None: self._cells_view = tuple(self._cells) return self._cells_view def __iter__(self) -> Iterator[Cell]: """Iterate over the cells of the unit in board order. :returns: An iterator over the unit's cells. :rtype: Iterator[Cell] """ return iter(self._cells) def __len__(self) -> int: """Return the number of registered cells. :returns: The cell count (nine once the grid is fully built). :rtype: int """ return len(self._cells) def __getitem__(self, position: int) -> Cell: """Return the cell at ``position`` within the unit. :param position: Zero-based position inside the unit. :type position: int :returns: The requested cell. :rtype: Cell """ return self._cells[position] # ------------------------------------------------------------------ # Digit bookkeeping # ------------------------------------------------------------------
[docs] def solved_values(self) -> frozenset[int]: """Return the digits already placed inside the unit. :returns: The set of solved digits. :rtype: frozenset[int] """ return frozenset( cell.value for cell in self._cells if cell.value is not None )
[docs] def missing_values(self) -> frozenset[int]: """Return the digits still to be placed inside the unit. :returns: The complement of :meth:`solved_values`. :rtype: frozenset[int] """ return DIGITS - self.solved_values()
[docs] def unsolved_cells(self) -> tuple[Cell, ...]: """Return the cells of the unit that have no value yet. :returns: The unsolved cells, in board order. :rtype: tuple[Cell, ...] """ return tuple(cell for cell in self._cells if not cell.is_solved)
[docs] def cells_with_candidate(self, digit: int) -> tuple[Cell, ...]: """Return the unsolved cells that still allow ``digit``. :param digit: The digit to look for, between 1 and 9. :type digit: int :returns: The matching cells, in board order. :rtype: tuple[Cell, ...] """ return tuple( [cell for cell in self._cells if digit in cell.candidates] )
[docs] def is_valid(self) -> bool: """Check that no digit appears twice among the solved cells. :returns: ``True`` if every solved digit is unique in the unit. :rtype: bool """ values = [cell.value for cell in self._cells if cell.value is not None] return len(values) == len(set(values))
def __repr__(self) -> str: """Return a debugging representation of the unit. :returns: A string such as ``<Row 3>``. :rtype: str """ return f"<{type(self).__name__} {self._index + 1}>"
[docs] class Row(Unit): """A horizontal line of nine cells.""" kind = "row"
[docs] class Column(Unit): """A vertical line of nine cells.""" kind = "column"
[docs] class Subgrid(Unit): """A 3x3 box of nine cells. Subgrids are indexed left to right, top to bottom: subgrid 0 covers rows 0-2 and columns 0-2, subgrid 8 covers rows 6-8 and columns 6-8. """ kind = "subgrid"