Source code for dedoku.techniques.intersections
"""Intersection removal techniques: Pointing and Claiming.
Both techniques exploit the three-cell intersection between a subgrid and a
line (row or column):
* **Pointing** — if, inside a subgrid, every home of a digit sits on the
same line, the digit must occupy that intersection, so it can be removed
from the rest of the line.
* **Claiming** (box/line reduction) — if, inside a line, every home of a
digit falls within the same subgrid, the digit is claimed by that box,
so it can be removed from the box cells outside the line.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from .base import Elimination, Step, Technique
if TYPE_CHECKING:
from ..grid import Grid
from ..units import Unit
__all__ = ["PointingCandidates", "ClaimingCandidates"]
[docs]
class PointingCandidates(Technique):
"""Remove a digit from a line when a subgrid confines it to that line."""
name = "Pointing Candidates"
[docs]
def apply(self, grid: Grid) -> Step | None:
"""Find the first pointing pair/triple that eliminates a candidate.
:param grid: The board to inspect and mutate.
:type grid: Grid
:returns: The eliminations performed, or ``None`` if no productive
pointing pattern exists.
:rtype: Step | None
"""
for subgrid in grid.subgrids:
for digit in sorted(subgrid.missing_values()):
homes = subgrid.cells_with_candidate(digit)
if not 2 <= len(homes) <= 3:
continue
line: Unit
if len({cell.row_index for cell in homes}) == 1:
line = homes[0].row
elif len({cell.column_index for cell in homes}) == 1:
line = homes[0].column
else:
continue
eliminations: list[Elimination] = []
for cell in line.cells_with_candidate(digit):
if cell.subgrid is not subgrid:
cell.remove_candidate(digit)
eliminations.append(
Elimination(cell.row_index, cell.column_index, digit)
)
if eliminations:
return Step(
technique=self.name,
description=(
f"in {subgrid.name}, digit {digit} is confined "
f"to {line.name}, so it is removed from the "
f"rest of the line"
),
eliminations=tuple(eliminations),
)
return None
[docs]
class ClaimingCandidates(Technique):
"""Remove a digit from a subgrid when a line confines it to that box."""
name = "Claiming Candidates"
[docs]
def apply(self, grid: Grid) -> Step | None:
"""Find the first claiming (box/line) reduction on the board.
:param grid: The board to inspect and mutate.
:type grid: Grid
:returns: The eliminations performed, or ``None`` if no productive
claiming pattern exists.
:rtype: Step | None
"""
for line in grid.rows + grid.columns:
for digit in sorted(line.missing_values()):
homes = line.cells_with_candidate(digit)
if not 2 <= len(homes) <= 3:
continue
if len({cell.subgrid.index for cell in homes}) != 1:
continue
subgrid = homes[0].subgrid
members = set(homes)
eliminations: list[Elimination] = []
for cell in subgrid.cells_with_candidate(digit):
if cell not in members:
cell.remove_candidate(digit)
eliminations.append(
Elimination(cell.row_index, cell.column_index, digit)
)
if eliminations:
return Step(
technique=self.name,
description=(
f"in {line.name}, digit {digit} is confined to "
f"{subgrid.name}, so it is removed from the "
f"rest of the box"
),
eliminations=tuple(eliminations),
)
return None