Source code for dedoku.techniques.chute
"""Chute Remote Pairs technique.
A *chute* is a band (three stacked rows covering three subgrids) or a
stack (three adjacent columns covering three subgrids). Take two bivalue
cells with the same candidate pair ``{a, b}`` inside one chute, lying in
different subgrids and different lines, so they share no house. Exactly
three chute cells see neither of them: the intersection of the remaining
line and the remaining subgrid.
If digit ``a`` can appear in none of those three cells (neither as a value
nor as a candidate), the two pair cells cannot both hold ``a`` — otherwise
the remaining subgrid would have no home for ``a`` at all. Hence at least
one pair cell holds ``b``, and ``b`` can be eliminated from every cell that
sees both pair cells.
"""
from __future__ import annotations
from itertools import combinations
from typing import TYPE_CHECKING
from .base import Elimination, Step, Technique
if TYPE_CHECKING:
from ..cell import Cell
from ..grid import Grid
__all__ = ["ChuteRemotePairs"]
[docs]
class ChuteRemotePairs(Technique):
"""Eliminate via two same-pair bivalue cells spread across a chute."""
name = "Chute Remote Pairs"
[docs]
def apply(self, grid: Grid) -> Step | None:
"""Find the first productive chute remote pair on the board.
Bands (horizontal chutes) are searched before stacks.
:param grid: The board to inspect and mutate.
:type grid: Grid
:returns: The eliminations performed, or ``None`` if no productive
pattern exists.
:rtype: Step | None
"""
for is_band in (True, False):
for chute_index in range(3):
step = self._search_chute(grid, chute_index, is_band)
if step is not None:
return step
return None
def _search_chute(
self, grid: Grid, chute_index: int, is_band: bool
) -> Step | None:
"""Search a single chute for a productive remote pair.
:param grid: The board to inspect and mutate.
:type grid: Grid
:param chute_index: Zero-based index of the chute (0-2).
:type chute_index: int
:param is_band: ``True`` for a band of rows, ``False`` for a stack
of columns.
:type is_band: bool
:returns: The eliminations performed, or ``None``.
:rtype: Step | None
"""
lines = grid.rows if is_band else grid.columns
line_indices = range(3 * chute_index, 3 * chute_index + 3)
chute_lines = [lines[index] for index in line_indices]
bivalues = [
cell
for line in chute_lines
for cell in line
if cell.is_bivalue
]
def line_of(cell: Cell) -> int:
return cell.row_index if is_band else cell.column_index
chute_name = f"{'band' if is_band else 'stack'} {chute_index + 1}"
for first, second in combinations(bivalues, 2):
if first.candidates != second.candidates:
continue
if first.subgrid is second.subgrid:
continue
if line_of(first) == line_of(second):
continue
blind = self._blind_cells(grid, first, second, line_indices, is_band)
pair = sorted(first.candidates)
for absent, present in (tuple(pair), tuple(reversed(pair))):
if any(
cell.value == absent or absent in cell.candidates
for cell in blind
):
continue
eliminations: list[Elimination] = []
common = set(first.peers) & set(second.peers)
for cell in sorted(common, key=lambda c: c.position):
if cell.remove_candidate(present):
eliminations.append(
Elimination(cell.row_index, cell.column_index, present)
)
if eliminations:
pair_text = ", ".join(str(d) for d in pair)
return Step(
technique=self.name,
description=(
f"remote pair {{{pair_text}}} at {first.label} "
f"and {second.label} in {chute_name}: digit "
f"{absent} cannot occupy the chute cells seeing "
f"neither of them, so digit {present} is removed "
f"from every cell seeing both"
),
eliminations=tuple(eliminations),
)
return None
@staticmethod
def _blind_cells(
grid: Grid,
first: Cell,
second: Cell,
line_indices: range,
is_band: bool,
) -> tuple[Cell, ...]:
"""Return the three chute cells that see neither pair cell.
They form the intersection of the chute line and the chute subgrid
containing neither pair cell.
:param grid: The board being searched.
:type grid: Grid
:param first: One remote-pair cell.
:type first: Cell
:param second: The other remote-pair cell.
:type second: Cell
:param line_indices: The three line indices spanning the chute.
:type line_indices: range
:param is_band: ``True`` for a band of rows, ``False`` for a stack.
:type is_band: bool
:returns: The three cells seeing neither pair cell.
:rtype: tuple[Cell, ...]
"""
if is_band:
used_lines = {first.row_index, second.row_index}
else:
used_lines = {first.column_index, second.column_index}
remaining_line = next(
index for index in line_indices if index not in used_lines
)
used_boxes = {first.subgrid.index, second.subgrid.index}
remaining_box = next(
subgrid
for subgrid in grid.subgrids
if subgrid.index not in used_boxes
and any(
(cell.row_index if is_band else cell.column_index)
in line_indices
for cell in subgrid
)
)
return tuple(
cell
for cell in remaining_box
if (cell.row_index if is_band else cell.column_index)
== remaining_line
)