Source code for dedoku.techniques.base
"""Framework shared by every logical solving technique.
A technique inspects the board and, when its pattern is found, mutates the
grid (placing a value or eliminating candidates) and reports what it did as
an immutable :class:`Step`. The solver chains these steps until the puzzle
is solved or no technique applies.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, NamedTuple
if TYPE_CHECKING:
from ..grid import Grid
__all__ = ["Placement", "Elimination", "Step", "Technique"]
[docs]
class Placement(NamedTuple):
"""A digit placed at a board position.
Behaves exactly like the plain ``(row, column, digit)`` tuple it
replaces — unpacking, indexing, and equality with bare tuples all
keep working — while adding named field access.
:ivar row: Zero-based row index (0-8).
:vartype row: int
:ivar column: Zero-based column index (0-8).
:vartype column: int
:ivar digit: The digit placed, between 1 and 9.
:vartype digit: int
"""
row: int
column: int
digit: int
[docs]
class Elimination(NamedTuple):
"""A candidate removed from a board position.
Behaves exactly like the plain ``(row, column, digit)`` tuple it
replaces — unpacking, indexing, and equality with bare tuples all
keep working — while adding named field access.
:ivar row: Zero-based row index (0-8).
:vartype row: int
:ivar column: Zero-based column index (0-8).
:vartype column: int
:ivar digit: The candidate removed, between 1 and 9.
:vartype digit: int
"""
row: int
column: int
digit: int
[docs]
@dataclass(frozen=True)
class Step:
"""Immutable record of one successful technique application.
Coordinates are zero-based; the human-readable :attr:`description`
uses one-based ``RxCy`` labels. Any bare ``(row, column, digit)``
tuples passed to the constructor are normalised to
:class:`Placement` / :class:`Elimination` instances.
:ivar technique: Name of the technique that produced the step.
:vartype technique: str
:ivar description: Human-readable explanation of the deduction.
:vartype description: str
:ivar placements: Values placed on the board by this step.
:vartype placements: tuple[Placement, ...]
:ivar eliminations: Candidates removed from cells by this step.
:vartype eliminations: tuple[Elimination, ...]
"""
technique: str
description: str
placements: tuple[Placement, ...] = field(default=())
eliminations: tuple[Elimination, ...] = field(default=())
def __post_init__(self) -> None:
"""Normalise bare coordinate tuples to their named types."""
object.__setattr__(
self, "placements",
tuple(Placement(*entry) for entry in self.placements),
)
object.__setattr__(
self, "eliminations",
tuple(Elimination(*entry) for entry in self.eliminations),
)
[docs]
class Technique(ABC):
"""Abstract base class for a single logical solving strategy.
Concrete techniques must be stateless: all information lives in the
grid, so the same instance can be reused across puzzles.
:cvar name: Human-readable technique name shown in solving reports.
:cvar requires_unique_solution: ``True`` for uniqueness-based
techniques, whose deductions are only sound when the puzzle has
exactly one solution. The default pipeline can exclude them via
``SudokuSolver(assume_unique=False)``.
"""
name: str = "technique"
requires_unique_solution: bool = False
[docs]
@abstractmethod
def apply(self, grid: Grid) -> Step | None:
"""Search the grid and apply the first occurrence of the pattern.
Implementations must mutate the grid **only** when they return a
step, and each call must perform at most one deduction so that the
solver can always fall back to the simplest available technique.
:param grid: The board to inspect and mutate.
:type grid: Grid
:returns: A record of the deduction, or ``None`` if the pattern
does not occur anywhere on the board.
:rtype: Step | None
:raises dedoku.exceptions.ContradictionError: If applying
the deduction reveals an inconsistent board state.
"""
def __repr__(self) -> str:
"""Return a debugging representation of the technique.
:returns: A string such as ``<Technique 'Naked Pair'>``.
:rtype: str
"""
return f"<Technique {self.name!r}>"