Sometimes a cell has several candidates but is the only place in its unit a given digit can go. Today you add the hidden-single rule, which sees these placements naked singles miss and unblocks puzzles that would otherwise stall.
In each unit, assign any digit that has exactly one possible cell.
A hidden single is the mirror of a naked single. A naked single looks at a cell
and finds it has one candidate; a hidden single looks at a unit and finds a
digit with one possible home. Cell 38 might still list several candidates, but if it
is the only cell in its row that can take a 9, then 9 must go there - every other
cell in that row is already ruled out for it. Assigning 9 collapses cell 38 to
{9} and, through elimination, feeds the naked-single cascade again.
This rule matters because many puzzles that naked singles alone cannot finish are
still solvable by pure logic once hidden singles join in. The medium puzzle here
stalls under naked singles with dozens of cells undecided, then breaks open the
moment you spot that 9 has a single home in row 4. Two simple rules, applied
together, dissolve a large fraction of real puzzles - the next lesson makes them fail
loudly when a puzzle has no solution, then runs them together to a fixpoint.
// for every unit and digit, if exactly one cell can hold it, assign it therefunc PropagateHidden(cg [81]Set) [81]Set {for _, u := range Units() {for d := 1; d <= 9; d++ {spots := cellsWithCandidate(u, d, cg)if len(spots) == 1 { cg = AssignCG(cg, spots[0], d) }}}return cg}