Source code for dedoku.techniques.wings

"""Wing techniques built on a pivot cell: Y-Wing and XYZ-Wing.

A Y-Wing consists of a bivalue *pivot* ``{x, y}`` seeing two bivalue
*pincers* ``{x, z}`` and ``{y, z}``. Whatever the pivot holds, one of the
pincers is forced to ``z``, so ``z`` can be eliminated from every cell
that sees both pincers.

The XYZ-Wing extends the pattern with a trivalue pivot ``{x, y, z}``: the
pivot itself may also hold ``z``, so the elimination only applies to cells
that see all three pattern cells.
"""

from __future__ import annotations

from itertools import combinations
from typing import TYPE_CHECKING

from .base import Elimination, Step, Technique

if TYPE_CHECKING:
    from ..grid import Grid

__all__ = ["YWing", "XYZWing"]


[docs] class YWing(Technique): """Eliminate the shared pincer digit of an XY-Wing pattern.""" name = "Y-Wing"
[docs] def apply(self, grid: Grid) -> Step | None: """Find the first productive Y-Wing on the board. :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 """ bivalues = [cell for cell in grid.cells if cell.is_bivalue] for pivot in bivalues: pincer_pool = [ cell for cell in bivalues if pivot.sees(cell) and cell.candidates != pivot.candidates and len(cell.candidates & pivot.candidates) == 1 ] for first, second in combinations(pincer_pool, 2): shared = first.candidates & second.candidates if len(shared) != 1: continue digit = next(iter(shared)) if digit in pivot.candidates: continue if (first.candidates | second.candidates) - shared \ != pivot.candidates: 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(digit): eliminations.append( Elimination(cell.row_index, cell.column_index, digit) ) if eliminations: return Step( technique=self.name, description=( f"Y-Wing with pivot {pivot.label} and pincers " f"{first.label}, {second.label}: one pincer " f"must hold {digit}, so it is removed from " f"every cell seeing both pincers" ), eliminations=tuple(eliminations), ) return None
[docs] class XYZWing(Technique): """Eliminate the digit shared by a trivalue pivot and two pincers.""" name = "XYZ-Wing"
[docs] def apply(self, grid: Grid) -> Step | None: """Find the first productive XYZ-Wing on the board. :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 """ pivots = [ cell for cell in grid.cells if not cell.is_solved and len(cell.candidates) == 3 ] bivalues = [cell for cell in grid.cells if cell.is_bivalue] for pivot in pivots: pincer_pool = [ cell for cell in bivalues if pivot.sees(cell) and cell.candidates < pivot.candidates ] for first, second in combinations(pincer_pool, 2): if first.candidates == second.candidates: continue shared = first.candidates & second.candidates if len(shared) != 1: continue digit = next(iter(shared)) eliminations: list[Elimination] = [] common = ( set(pivot.peers) & set(first.peers) & set(second.peers) ) for cell in sorted(common, key=lambda c: c.position): if cell.remove_candidate(digit): eliminations.append( Elimination(cell.row_index, cell.column_index, digit) ) if eliminations: return Step( technique=self.name, description=( f"XYZ-Wing with pivot {pivot.label} and " f"pincers {first.label}, {second.label}: one " f"of the three must hold {digit}, so it is " f"removed from every cell seeing all of them" ), eliminations=tuple(eliminations), ) return None