Example #1
0
        public static void DrawPuzzle(Puzzle puzzle, Solution solution)
        {
            PrintColumnSolution(solution);

            for (int i = 0; i < 9; i++)
            {
                var row = puzzle.GetRow(i);

                if (i == 3 || i == 6)
                {
                    WriteLine("------+-------+------");
                }

                for (int j = 0; j < 9; j++)
                {
                    if (j == 3 || j == 6)
                    {
                        Write($"| {row.Segment[j]} ");
                    }
                    else if (j == 8)
                    {
                        Write($"{row.Segment[j]}");
                    }
                    else
                    {
                        Write($"{row.Segment[j]} ");
                    }
                }

                if (solution.Solved && i == solution.Row)
                {
                    Write("*");
                }
                WriteLine();
            }
            WriteLine(puzzle);


            void PrintColumnSolution(Solution solution)
            {
                if (!solution.Solved)
                {
                    return;
                }

                for (int i = 0; i < solution.Column; i++)
                {
                    Write("  ");
                    if (i == 2 || i == 5)
                    {
                        Write("  ");
                    }
                }
                Write("*");
                WriteLine();
            }
        }
Example #2
0
        public bool TrySolveRow(int index, out Solution solution)
        {
            if (_puzzle.SolvedForRow[index] == _effectiveCount &&
                TrySolveLine(_puzzle.GetRow(index), out int column, out int value))
            {
                solution = new Solution(
                    index,
                    column,
                    value,
                    nameof(NakedSinglesSolver),
                    null
                    );
                return(true);
            }

            solution = null;
            return(false);
        }
        public Solution SolveRow(int index)
        {
            var line = _puzzle.GetRow(index);

            (bool solved, int cell, int value) = SolveLine(line);

            var solution = new Solution
            {
                Solved = solved
            };

            if (solved)
            {
                solution.Column = cell;
                solution.Row    = index;
                solution.Value  = value;
                solution.Solver = this;
                return(solution);
            }

            return(solution);
        }
        public Solution Solve(int index)
        {
            var box = _puzzle.GetBox(index);

            // adjacent neighboring boxes
            // first two values in array are horizontal neighbors
            // second two values in array are vertical neighbors

            var offset = (index / 3) * 3;

            int[] avn = new int[]
            {
                (index + 1) % 3 + offset,
                (index + 2) % 3 + offset,
                (index + 3) % 9,
                (index + 6) % 9
            };

            // get adjacent neighboring boxes
            var ahnb1 = _puzzle.GetBox(avn[0]);
            var ahnb2 = _puzzle.GetBox(avn[1]);
            var avnb1 = _puzzle.GetBox(avn[2]);
            var avnb2 = _puzzle.GetBox(avn[3]);

            // iterate over the three rows in the box
            for (int i = 0; i < 3; i++)
            {
                // for each row, the neighboring rows are the same
                // the columns differ per cell
                var row1Index = i;
                var row2Index = (i + 1) % 3;
                var row3Index = (i + 2) % 3;

                // there are nine row to consider (three boxes)

                // baseline box
                var row2 = box.GetRow(row2Index);
                var row3 = box.GetRow(row3Index);

                // horizontal adjacent box 1 -- rows
                var ahnb1Row2 = ahnb1.GetRow(row2Index);
                var ahnb1Row3 = ahnb1.GetRow(row3Index);

                // horizontal adjacent box 2 -- rows
                var ahnb2Row2 = ahnb2.GetRow(row2Index);
                var ahnb2Row3 = ahnb2.GetRow(row3Index);

                // determine union of values of rows
                var row2Union = ahnb1Row2.Union(ahnb2Row2);
                var row3Union = ahnb1Row3.Union(ahnb2Row3);

                // get complete row that includes the the first row of box
                var firstRowIndex = box.GetRowOffsetForBox() + i;
                var firstRow      = _puzzle.GetRow(firstRowIndex);

                // the boundaries of the adjacent boxes don't matter
                // the appropriate rows have been merged (Union) at this point

                // determine union of values of row 1 -- these values are all off-limits
                // get complete row that includes the the first row of box
                var boxValues = box.AsValues();

                // determine disjoint set with baseline rows -- box row values are off-limits

                // determine disjoint set with baseline rows -- box row values are off-limits
                var row2Values = row2Union.DisjointSet(boxValues);
                var row3Values = row3Union.DisjointSet(boxValues);

                // determine disjoint set with baseline row -- looking for values now in that row
                var row2Candidates = row2Values.DisjointSet(firstRow.Segment);
                var row3Candidates = row3Values.DisjointSet(firstRow.Segment);

                // row candidates -- values in both row 2 and 3 but not in row 1 or in the box
                ReadOnlySpan <int> rowCandidates = new int[] {};

                if (row2[0] == 0 && row3[0] == 0)
                {
                    rowCandidates = row2Candidates.Intersect(row3Candidates);
                }
                else if (row2[0] == 0)
                {
                    rowCandidates = row2Candidates;
                }
                else if (row3[0] == 0)
                {
                    rowCandidates = row3Candidates;
                }

                if (rowCandidates.Length == 0)
                {
                    continue;
                }

                // cells
                for (int y = 0; y < 3; y++)
                {
                    var col1Index = y;
                    var col2Index = (y + 1) % 3;
                    var col3Index = (y + 2) % 3;

                    // baseline box
                    var col1 = box.GetColumn(col1Index);
                    var col2 = box.GetColumn(col2Index);
                    var col3 = box.GetColumn(col3Index);

                    // vertical adjacent box 1 -- cols
                    var avnb1Col1 = avnb1.GetColumn(col1Index);
                    var avnb1Col2 = avnb1.GetColumn(col2Index);
                    var avnb1Col3 = avnb1.GetColumn(col3Index);

                    // vertical adjacent box 2 -- cols
                    var avnb2Col1 = avnb2.GetColumn(col1Index);
                    var avnb2Col2 = avnb2.GetColumn(col2Index);
                    var avnb2Col3 = avnb2.GetColumn(col3Index);

                    // get complete column that includes the the first column of box
                    var firstColIndex = box.GetColumnOffsetForBox() + y;
                    var firstCol      = _puzzle.GetColumn(firstColIndex);

                    // determine union of values of cols
                    var col2Union = avnb1Col2.Union(avnb2Col2);
                    var col3Union = avnb1Col3.Union(avnb2Col3);

                    // determine disjoint set with baseline cols -- box col values are off-limits
                    var col2Values = col2Union.DisjointSet(boxValues);
                    var col3Values = col3Union.DisjointSet(boxValues);

                    // determine disjoint set with baseline col -- looking for values now in that col
                    var col2Candidates = col2Values.DisjointSet(firstCol.Segment);
                    var col3Candidates = col3Values.DisjointSet(firstCol.Segment);

                    ReadOnlySpan <int> colCandidates = new int[] { };

                    if (col2[0] == 0 && col3[0] == 0)
                    {
                        colCandidates = col2Candidates.Intersect(col3Candidates);
                    }
                    else if (col2[0] == 0)
                    {
                        colCandidates = col2Candidates;
                    }
                    else if (col3[0] == 0)
                    {
                        colCandidates = col3Candidates;
                    }

                    // col candidates -- values in both col 2 and 3 but not in col 1 or in the box
                    //var colCandidates = col2Candidates.Intersect(col3Candidates);

                    if (colCandidates.Length == 0)
                    {
                        continue;
                    }

                    var candidates = colCandidates.Intersect(rowCandidates);

                    if (candidates.Length == 1)
                    {
                        (var row, var column) = Puzzle.GetLocationForBoxCell(index, (i * 3) + y);
                        return(new Solution
                        {
                            Solved = true,
                            Value = candidates[0],
                            Row = row,
                            Column = column,
                            Solver = this
                        });
                    }
                }
            }

            return(new Solution
            {
                Solved = false
            });
        }
Example #5
0
        public Solution Solve(int index)
        {
            var box = _puzzle.GetBox(index);

            // adjacent neighboring boxes
            // first two values in array are horizontal neighbors
            // second two values in array are vertical neighbors

            var offset = (index / 3) * 3;

            int[] avn = new int[]
            {
                (index + 1) % 3 + offset,
                (index + 2) % 3 + offset,
                (index + 3) % 9,
                (index + 6) % 9
            };

            // get adjacent neighboring boxes
            var ahnb1 = _puzzle.GetBox(avn[0]);
            var ahnb2 = _puzzle.GetBox(avn[1]);
            var avnb1 = _puzzle.GetBox(avn[2]);
            var avnb2 = _puzzle.GetBox(avn[3]);

            // iterate over the three rows in the box
            // goal is to solve a cell in this row (not other rows)
            // ititial logic (body of first for loop) can solve any row cell using
            // horizontal adjacent rows as input
            // later logic (body of embedded for loop) can solve a given cell, one at a time
            // using rows and/or columns as input
            for (int i = 0; i < 3; i++)
            {
                // calculate rows for i
                var row1Index = i;
                var row2Index = (i + 1) % 3;
                var row3Index = (i + 2) % 3;

                // target box
                var boxRow1 = box.GetRow(row1Index);

                if (!boxRow1.ContainsValue(0))
                {
                    continue;
                }

                var boxRow2 = box.GetRow(row2Index);
                var boxRow3 = box.GetRow(row3Index);


                // neighboring boxess
                // horizontal adjacent box 1 -- rows
                var ahnb1Row1 = ahnb1.GetRow(row1Index);
                var ahnb1Row2 = ahnb1.GetRow(row2Index);
                var ahnb1Row3 = ahnb1.GetRow(row3Index);

                // horizontal adjacent box 2 -- rows
                var ahnb2Row1 = ahnb1.GetRow(row1Index);
                var ahnb2Row2 = ahnb2.GetRow(row2Index);
                var ahnb2Row3 = ahnb2.GetRow(row3Index);

                // determine union of values of rows
                var adjRow2Union = ahnb1Row2.Union(ahnb2Row2);
                var adjRow3Union = ahnb1Row3.Union(ahnb2Row3);

                // get complete row that includes current row of box
                var currentRowIndex = box.GetRowOffsetForBox() + i;
                //TODO: consider an overload that only returns non-zero values
                var currentRow = _puzzle.GetRow(currentRowIndex);

                // get all values in box
                var boxLine = box.AsLine();
                //var boxValues = box.AsValues();

                // calculate full set of illegal values for row 1
                var boxRow1IllegalValues = boxLine.Union(currentRow);

                // determine disjoint set with baseline row -- looking for values in that row
                var row2Candidates    = adjRow2Union.DisjointSet(boxRow1IllegalValues);
                var row3Candidates    = adjRow3Union.DisjointSet(boxRow1IllegalValues);
                var boxRow1Candidates = row2Candidates.Intersect(row3Candidates);


                // determine if rows on their own present a solution
                if (boxRow1Candidates.Length == 1)
                {
                    (var justOne, var column) = boxRow1.IsJustOneElementUnsolved();
                    if (justOne)
                    {
                        var solverKind = nameof(Strategy.RowSolver);
                        return(GetSolution(index, i, column, boxRow1Candidates[0], solverKind));
                    }
                }

                // interate over columns for row
                // mostly targets row[column] cell
                for (int y = 0; y < 3; y++)
                {
                    if (boxRow1[y] != 0)
                    {
                        continue;
                    }

                    var col1Index = y;
                    var col2Index = (y + 1) % 3;
                    var col3Index = (y + 2) % 3;

                    // baseline box
                    var boxCol1 = box.GetColumn(col1Index);
                    var boxCol2 = box.GetColumn(col2Index);
                    var boxCol3 = box.GetColumn(col3Index);

                    // vertical adjacent box 1 -- cols
                    var avnb1Col1 = avnb1.GetColumn(col1Index);
                    var avnb1Col2 = avnb1.GetColumn(col2Index);
                    var avnb1Col3 = avnb1.GetColumn(col3Index);

                    // vertical adjacent box 2 -- cols
                    var avnb2Col1 = avnb2.GetColumn(col1Index);
                    var avnb2Col2 = avnb2.GetColumn(col2Index);
                    var avnb2Col3 = avnb2.GetColumn(col3Index);

                    // get complete column that includes the the first column of box
                    var currentColIndex = box.GetColumnOffsetForBox() + y;
                    var currentCol      = _puzzle.GetColumn(currentColIndex);

                    // calculate full set of illegal values for col 1
                    var boxCol1IllegalValues = boxLine.Union(currentCol);

                    // determine union of values of cols
                    var col2Union = avnb1Col2.Union(avnb2Col2);
                    var col3Union = avnb1Col3.Union(avnb2Col3);

                    // determine disjoint set with baseline col -- looking for values now in that col
                    var col2Candidates    = col2Union.DisjointSet(boxCol1IllegalValues);
                    var col3Candidates    = col3Union.DisjointSet(boxCol1IllegalValues);
                    var boxCol1Candidates = col2Candidates.Intersect(col3Candidates);

                    // determine if columns on their own present a solution
                    // can solve any cell in given box column
                    if (boxCol1Candidates.Length == 1)
                    {
                        (var justOne, var row) = boxCol1.IsJustOneElementUnsolved();
                        if (justOne)
                        {
                            var solverKind = nameof(Strategy.ColumnSolver);
                            return(GetSolution(index, row, y, boxCol1Candidates[0], solverKind));
                        }
                    }

                    // determine if rows and columns together present a solution
                    // test: SolutionAllSlotsEmpty
                    var rowAndColumnCandidates = boxRow1Candidates.Intersect(boxCol1Candidates);
                    if (rowAndColumnCandidates.Length == 1)
                    {
                        if (boxCol1[i] == 0)
                        {
                            var solverKind = nameof(Strategy.RowColumnSolver);
                            return(GetSolution(index, i, y, rowAndColumnCandidates[0], solverKind));
                        }
                    }

                    // determine if various combinations of rows and columns present a solution

                    // col1[row] is candidate for all following cases

                    // the following set of cases involve a row or a column with:
                    // a candidate, one cell == 0 and the other non zero
                    // the cases are looking for a set of rows and columns
                    // that project the same value except for the row or column with
                    // the non-zero value (which doesn't require a projected value)

                    // check rows
                    if (boxRow1Candidates.Length > 0)
                    {
                        bool     solved;
                        Solution solution;
                        var      solverKind1 = nameof(Strategy.Column2CandidateColumn3BlockedRowSolver);

                        // row1[col3] has a value, so col3 don't need to participate in providing a candidate
                        if (col2Candidates.Length > 0 &&
                            boxRow1[col2Index] == 0 && boxRow1[col3Index] != 0 &&
                            ((solved, solution) = CheckForIntersectionCandidates(boxRow1Candidates, col2Candidates, solverKind1)).solved)
                        {
                            return(solution);
                        }

                        var solverKind2 = nameof(Strategy.Column2BlockedColumn3CandidateRowSolver);
                        // row1[col2] has a value, so col2 don't need to participate in providing a candidate
                        if (col3Candidates.Length > 0 &&
                            boxRow1[col2Index] != 0 && boxRow1[col3Index] == 0 &&
                            ((solved, solution) = CheckForIntersectionCandidates(boxRow1Candidates, col3Candidates, solverKind2)).solved)
                        {
                            return(solution);
                        }
                    }

                    // check columns
                    if (boxCol1Candidates.Length > 0)
                    {
                        bool     solved;
                        Solution solution;
                        var      solver1 = nameof(Strategy.Row2CandidateRow3BlockedColumnSolver);
                        // col1[row3] has a value, so row3 don't need to participate in providing a candidate
                        if (row2Candidates.Length > 0 &&
                            boxCol1[row2Index] == 0 && boxCol1[row3Index] != 0 &&
                            ((solved, solution) = CheckForIntersectionCandidates(boxCol1Candidates, row2Candidates, solver1)).solved)
                        {
                            return(solution);
                        }

                        var solver2 = nameof(Strategy.Row2BlockedRow3CandidateColumnSolver);
                        // col1[row2] has a value, so row2 don't need to participate in providing a candidate
                        // test: SolutionTwoSlotsEmptyInColumn
                        if (row3Candidates.Length > 0 &&
                            boxCol1[row2Index] != 0 && boxCol1[row3Index] == 0 &&
                            ((solved, solution) = CheckForIntersectionCandidates(boxCol1Candidates, row3Candidates, solver2)).solved)
                        {
                            return(solution);
                        }
                    }

                    // the following set of cases involve a row or column with:
                    // all cells filled, such that a row or column doesn't need to participate in providing a candidate

                    // col3 is blocked, so col3 doesn't need to participate in providing a candidate
                    var col2Blocked = boxCol2.GetUnsolvedCount() == 0;
                    var col3Blocked = boxCol3.GetUnsolvedCount() == 0;
                    var row2Blocked = boxRow2.GetUnsolvedCount() == 0;
                    var row3Blocked = boxRow3.GetUnsolvedCount() == 0;

                    if (boxCol1.GetUnsolvedCount() == 1)
                    {
                        if (col3Blocked &&
                            col2Candidates.Length == 1)
                        {
                            var solverKind = nameof(Strategy.Column2CandidateColumn3Blocked);
                            return(GetSolution(index, i, y, col2Candidates[0], solverKind));
                        }
                    }

                    // following set of cases test hidden singles in columns or rows

                    // pattern
                    // box 1 col1 is full
                    // box 2 has one value not in col1
                    // only one slot available in col1 in current box
                    // means that current cell must have that value

                    {
                        var solverKind  = nameof(Strategy.ColumnLastPossibleSlot);
                        var candidates2 = avnb2.AsLine().DisjointSet(currentCol);

                        foreach (var candidate in candidates2)
                        {
                            if (!boxLine.ContainsValue(candidate) &&
                                !currentRow.ContainsValue(candidate) &&
                                CheckForValueInCellOrRow(candidate, box, col1Index, row2Index, row3Index) &&
                                CheckForValueInCellOrRow(candidate, avnb1, col1Index, 0, 1, 2)
                                )
                            {
                                solverKind += "-1";
                                return(GetSolution(index, i, y, candidate, solverKind));
                            }
                        }

                        var candidates1 = avnb1.AsLine().DisjointSet(currentCol);

                        foreach (var candidate in candidates1)
                        {
                            if (!boxLine.ContainsValue(candidate) &&
                                !currentRow.ContainsValue(candidate) &&
                                CheckForValueInCellOrRow(candidate, box, col1Index, row2Index, row3Index) &&
                                CheckForValueInCellOrRow(candidate, avnb2, col1Index, 0, 1, 2)
                                )
                            {
                                solverKind += "-2";
                                return(GetSolution(index, i, y, candidate, solverKind));
                            }
                        }
                    }

/*
 *                      bool solved;
 *                      Solution solution;
 *
 *                      // start with columns
 *                      // test each cell in avnb1.col1
 *                      // avnb2 presents candidate
 *                      if (((solved, solution) = CheckForDisjointCandidates(avnb2.AsLine().Segment, currentCol.Segment, solverKind)).solved &&
 *                          !boxLine.ContainsValue(solution.Value) &&
 *                          !currentRow.ContainsValue(solution.Value) &&
 *                          CheckForValueInCellOrRow(solution.Value,box,col1Index,row2Index,row3Index) &&
 *                          CheckForValueInCellOrRow(solution.Value,avnb1,col1Index,0,1,2)
 *                      )
 *                      {
 *                          solution.SolverKind += "-1";
 *                          return solution;
 *                      }
 *
 *                      // test avnb2.col1 solved
 *                      // avnb1 presents candidate
 *                      if (((solved, solution) = CheckForDisjointCandidates(avnb1.AsLine().Segment, currentCol.Segment, solverKind)).solved &&
 *                          !boxLine.ContainsValue(solution.Value) &&
 *                          !currentRow.Segment.Contains(solution.Value) &&
 *                          CheckForValueInCellOrRow(solution.Value,box,col1Index,row2Index,row3Index) &&
 *                          CheckForValueInCellOrRow(solution.Value,avnb2,col1Index,0,1,2)
 *                          )
 *                      {
 *                              solution.SolverKind += "-2";
 *                              return solution;
 *                      }
 *                  }
 */
                    {
                        var solverKind  = nameof(Strategy.RowLastPossibleSlot);
                        var candidates2 = ahnb2.AsLine().DisjointSet(currentRow);

                        foreach (var candidate in candidates2)
                        {
                            if (!boxLine.ContainsValue(candidate) &&
                                !currentCol.Segment.Contains(candidate) &&
                                CheckForValueInCellOrColumn(candidate, box, row1Index, col2Index, col3Index) &&
                                CheckForValueInCellOrColumn(candidate, ahnb1, row1Index, 0, 1, 2)
                                )
                            {
                                solverKind += "-2";
                                return(GetSolution(index, i, y, candidate, solverKind));
                            }
                        }

                        var candidates1 = ahnb1.AsLine().DisjointSet(currentRow);

                        foreach (var candidate in candidates1)
                        {
                            if (!boxLine.ContainsValue(candidate) &&
                                !currentCol.Segment.Contains(candidate) &&
                                CheckForValueInCellOrColumn(candidate, box, row1Index, col2Index, col3Index) &&
                                CheckForValueInCellOrColumn(candidate, ahnb2, row1Index, 0, 1, 2)
                                )
                            {
                                solverKind += "-2";
                                return(GetSolution(index, i, y, candidate, solverKind));
                            }
                        }
                    }

/*
 *                      bool solved;
 *                      Solution solution;
 *
 *
 *
 *                      // solve for rows
 *                      // test each cell in avhb1.row1
 *                      // avhb2 presents candidate
 *                      if (((solved, solution) = CheckForDisjointCandidates(ahnb2.AsLine().Segment, currentRow.Segment, solverKind)).solved &&
 *                          !boxLine.ContainsValue(solution.Value) &&
 *                          !currentCol.Segment.Contains(solution.Value) &&
 *                          CheckForValueInCellOrColumn(solution.Value,box,row1Index,col2Index,col3Index) &&
 *                          CheckForValueInCellOrColumn(solution.Value,ahnb1,row1Index,0,1,2)
 *                      )
 *                      {
 *                          solution.SolverKind += "-1";
 *                          return solution;
 *                      }
 *
 *                      // test ahnb2.row1 solved
 *                      // ahnb1 presents candidate
 *                      if (((solved, solution) = CheckForDisjointCandidates(ahnb1.AsLine().Segment, currentRow.Segment, solverKind)).solved &&
 *                          !boxLine.ContainsValue(solution.Value) &&
 *                          !currentCol.Segment.Contains(solution.Value) &&
 *                          CheckForValueInCellOrColumn(solution.Value,box,row1Index,col2Index,col3Index) &&
 *                          CheckForValueInCellOrColumn(solution.Value,ahnb2,row1Index,0,1,2)
 *                      )
 *                      {
 *                              solution.SolverKind += "-2";
 *                              return solution;
 *                      }
 *                  }
 */
                    // Strategy where a column or row has two empty cells and one is constrained
                    {
                        var solverKind = nameof(Strategy.ColumnLastTwoPossibleSlots);

                        if (currentCol.GetUnsolvedCount() == 2)
                        {
                            var missingValues = currentCol.GetMissingValues();
                            for (int k = 0; k < 2; k++)
                            {
                                var value      = missingValues[k];
                                var otherValue = missingValues[(k + 1) % 2];
                                if (currentRow.ContainsValue(value) &&
                                    !boxLine.ContainsValue(otherValue))
                                {
                                    return(GetSolution(index, i, y, otherValue, solverKind));
                                }
                            }
                        }
                    }

                    {
                        var solverKind = nameof(Strategy.RowLastTwoPossibleSlots);

                        if (currentRow.GetUnsolvedCount() == 2)
                        {
                            var missingValues = currentRow.GetMissingValues();
                            for (int k = 0; k < 2; k++)
                            {
                                var value      = missingValues[k];
                                var otherValue = missingValues[(k + 1) % 2];
                                if (currentCol.ContainsValue(value) &&
                                    !boxLine.ContainsValue(otherValue))
                                {
                                    return(GetSolution(index, i, y, otherValue, solverKind));
                                }
                            }
                        }
                    }

                    bool CheckRowForValue(int searchValue, Box box, int row)
                    {
                        // get complete row that includes current row of box
                        var rowIndex = box.GetRowOffsetForBox() + row;
                        //TODO: consider an overload that only returns non-zero values
                        var targetRow = _puzzle.GetRow(rowIndex);

                        return(targetRow.Segment.Contains(searchValue));
                    }

                    bool CheckForValueInCellOrColumn(int value, Box box, int row, params int[] cols)
                    {
                        foreach (int col in cols)
                        {
                            var targetColumn = box.GetColumn(col);
                            if (targetColumn[row] != 0 ||
                                CheckColumnForValue(value, box, col))
                            {
                            }
                            else
                            {
                                return(false);
                            }
                        }
                        return(true);
                    }

                    bool CheckForValueInCellOrRow(int value, Box box, int col, params int[] rows)
                    {
                        foreach (int row in rows)
                        {
                            var targetRow = box.GetRow(row);
                            if (targetRow[col] != 0 ||
                                CheckRowForValue(value, box, row))
                            {
                            }
                            else
                            {
                                return(false);
                            }
                        }
                        return(true);
                    }

                    bool CheckColumnForValue(int searchValue, Box box, int col)
                    {
                        // get complete column that includes current column of box
                        var colIndex = box.GetColumnOffsetForBox() + col;
                        //TODO: consider an overload that only returns non-zero values
                        var targetCol = _puzzle.GetColumn(colIndex);

                        return(targetCol.Segment.Contains(searchValue));
                    }

                    (bool solved, Solution solution) CheckForIntersectionCandidates(ReadOnlySpan <int> candidate1, ReadOnlySpan <int> candidate2, string solverKind)
                    {
                        var candidates = candidate1.Intersect(candidate2);

                        if (candidates.Length == 1)
                        {
                            return(true, GetSolution(index, i, y, candidates[0], solverKind));
                        }
                        return(false, Solution.False);
                    }

                    (bool solved, Solution solution) CheckForDisjointCandidates(ReadOnlySpan <int> line, ReadOnlySpan <int> candidate2, string solverKind)
                    {
                        var candidates = line.DisjointSet(candidate2);

                        if (candidates.Length == 1)
                        {
                            return(true, GetSolution(index, i, y, candidates[0], solverKind));
                        }
                        return(false, Solution.False);
                    }
                }


                Solution GetSolution(int box, int row, int column, int value, string solverKind)
                {
                    (var r, var c) = Puzzle.GetLocationForBoxCell(index, (row * 3) + column);
                    return(new Solution
                    {
                        Solved = true,
                        Value = value,
                        Row = r,
                        Column = c,
                        Solver = this,
                        SolverKind = solverKind
                    });
                }
            }

            return(new Solution
            {
                Solved = false
            });
        }
        // find a box row/column with just one missing value
        // find a value that is present in the other two rows/columns but not this box
        // the value can be placed in the open spot if everything lines up
        public Solution Solve(int index)
        {
            var box = _puzzle.GetBox(index);

            // adjacent neighboring boxes
            // first two values in array are horizontal neighbors
            // second two values in array are vertical neighbors

            var offset = (index / 3) * 3;

            int[] avn = new int[]
            {
                (index + 1) % 3 + offset,
                (index + 2) % 3 + offset,
                (index + 3) % 9,
                (index + 6) % 9
            };

            var ahnb1 = _puzzle.GetBox(avn[0]);
            var ahnb2 = _puzzle.GetBox(avn[1]);
            var avnb1 = _puzzle.GetBox(avn[2]);
            var avnb2 = _puzzle.GetBox(avn[3]);

            // find intersection of values for adjacent rows
            for (int i = 0; i < 3; i++)
            {
                (var rowJustOne, var rowCandidateIndex) = box.GetRow(i).IsJustOneElementUnsolved();
                if (!rowJustOne)
                {
                    continue;
                }

                // calculate indexes of adjacent rows
                var row2Index = (i + 1) % 3;
                var row3Index = (i + 2) % 3;

                // baseline box
                var row2 = box.GetRow(row2Index);
                var row3 = box.GetRow(row3Index);

                // horizontal adjacent box 1 -- rows
                var ahnb1Row2 = ahnb1.GetRow(row2Index);
                var ahnb1Row3 = ahnb1.GetRow(row3Index);

                // horizontal adjacent box 2 -- rows
                var ahnb2Row2 = ahnb2.GetRow(row2Index);
                var ahnb2Row3 = ahnb2.GetRow(row3Index);

                // get complete row that includes the the first row of box
                var firstRowIndex = box.GetRowOffsetForBox() + i;
                var firstRow      = _puzzle.GetRow(firstRowIndex);

                // determine union of values of rows
                var row2Union = ahnb1Row2.Union(ahnb2Row2);
                var row3Union = ahnb1Row3.Union(ahnb2Row3);

                // the boundaries of the adjacent boxes don't matter
                // the appropriate rows have been merged (Union) at this point

                // determine union of values of row 1 -- these values are all off-limits

                // determine disjoint set with baseline rows -- box row values are off-limits
                var row2Values = row2Union.DisjointSet(row2.Segment);
                var row3Values = row3Union.DisjointSet(row3.Segment);

                // determine disjoint set with baseline row -- looking for values now in that row
                var row2Candidates = row2Values.DisjointSet(firstRow.Segment);
                var row3Candidates = row3Values.DisjointSet(firstRow.Segment);

                // row candidates -- values in both row 2 and 3 but not in row 1 or in the box
                var rowCandidates = row2Candidates.Intersect(row3Candidates);

                if (rowCandidates.Length == 1)
                {
                    (var r, var c) = Puzzle.GetLocationForBoxCell(index, i * 3 + rowCandidateIndex);
                    return(new Solution
                    {
                        Solved = true,
                        Row = r,
                        Column = c,
                        Value = rowCandidates[0],
                        Solver = this
                    });
                }

                // find intersection of values for adjacent columns
                (var colJustOne, var candidateIndex) = box.GetColumn(i).IsJustOneElementUnsolved();
                if (!colJustOne)
                {
                    continue;
                }

                var col1Index = i;
                var col2Index = (i + 1) % 3;
                var col3Index = (i + 2) % 3;

                // baseline box
                var col1 = box.GetColumn(col1Index);
                var col2 = box.GetColumn(col2Index);
                var col3 = box.GetColumn(col3Index);

                // vertical adjacent box 1 -- cols
                var avnb1Col1 = avnb1.GetColumn(col1Index);
                var avnb1Col2 = avnb1.GetColumn(col2Index);
                var avnb1Col3 = avnb1.GetColumn(col3Index);

                // vertical adjacent box 2 -- cols
                var avnb2Col1 = avnb2.GetColumn(col1Index);
                var avnb2Col2 = avnb2.GetColumn(col2Index);
                var avnb2Col3 = avnb2.GetColumn(col3Index);

                // get complete column that includes the the first column of box
                var firstColIndex = box.GetColumnOffsetForBox() + i;
                var firstCol      = _puzzle.GetColumn(firstColIndex);

                // determine union of values of cols
                var col2Union = avnb1Col2.Union(avnb2Col2);
                var col3Union = avnb1Col3.Union(avnb2Col3);

                // determine disjoint set with baseline cols -- box col values are off-limits
                var col2Values = col2Union.DisjointSet(col2.Segment);
                var col3Values = col3Union.DisjointSet(col3.Segment);

                // determine disjoint set with baseline col -- looking for values now in that col
                var col2Candidates = col2Values.DisjointSet(firstCol.Segment);
                var col3Candidates = col3Values.DisjointSet(firstCol.Segment);

                // col candidates -- values in both col 2 and 3 but not in col 1 or in the box
                var colCandidates = col2Candidates.Intersect(col3Candidates);


                if (colCandidates.Length == 1)
                {
                    (var r, var c) = Puzzle.GetLocationForBoxCell(index, (candidateIndex % 3) * 3 + i);
                    return(new Solution
                    {
                        Solved = true,
                        Row = r,
                        Column = c,
                        Value = colCandidates[0],
                        Solver = this
                    });
                }
            }

            return(new Solution
            {
                Solved = false
            });
        }
Example #7
0
        public bool TrySolveBox(int index, out Solution solution)
        {
            Box box          = _puzzle.GetBox(index);
            int rowOffset    = box.GetRowOffset();
            int columnOffset = box.GetColumnOffset();

            // get adjacent neighboring boxes
            Box ahnb1 = box.FirstHorizontalNeighbor;
            Box ahnb2 = box.SecondHorizontalNeighbor;
            Box avnb1 = box.FirstVerticalNeighbor;
            Box avnb2 = box.SecondVerticalNeighbor;

            // iterate over the three rows in the box
            // goal is to solve a cell in the "i" row
            // initial logic (body of first for loop) can solve any row cell using
            // horizontal adjacent rows as input
            // later logic (body of embedded for loop) can solve a given cell, one at a time
            // using rows and/or columns as input
            for (int i = 0; i < 3; i++)
            {
                // target box
                Line boxRow1 = box.GetRow(i);

                // check if row contains a zero
                if (!boxRow1.ContainsValue(0))
                {
                    continue;
                }

                // calculate rows for i
                int row1Index = i;
                int row2Index = (i + 1) % 3;
                int row3Index = (i + 2) % 3;

                Line boxRow2 = box.GetRow(row2Index);
                Line boxRow3 = box.GetRow(row3Index);

                // get all values in box
                Line boxLine = box.AsLine();

                // neighboring boxes
                // horizontal adjacent box 1 -- rows
                Line ahnb1Row1 = ahnb1.GetRow(row1Index);
                Line ahnb1Row2 = ahnb1.GetRow(row2Index);
                Line ahnb1Row3 = ahnb1.GetRow(row3Index);

                // horizontal adjacent box 2 -- rows
                Line ahnb2Row1 = ahnb1.GetRow(row1Index);
                Line ahnb2Row2 = ahnb2.GetRow(row2Index);
                Line ahnb2Row3 = ahnb2.GetRow(row3Index);

                // determine union of values of rows
                ReadOnlySpan <int> adjRow2Union = ahnb1Row2.Union(ahnb2Row2);
                ReadOnlySpan <int> adjRow3Union = ahnb1Row3.Union(ahnb2Row3);

                // get complete row that includes current row of box
                int  currentRowIndex = rowOffset + i;
                Line currentRow      = _puzzle.GetRow(currentRowIndex);

                // calculate full set of illegal values for row 1
                ReadOnlySpan <int> boxRow1IllegalValues = boxLine.Union(currentRow);

                // determine disjoint set with baseline row -- looking for values in that row
                ReadOnlySpan <int> row2Candidates    = adjRow2Union.DisjointSet(boxRow1IllegalValues);
                ReadOnlySpan <int> row3Candidates    = adjRow3Union.DisjointSet(boxRow1IllegalValues);
                ReadOnlySpan <int> boxRow1Candidates = row2Candidates.Intersect(row3Candidates);

                // determine if rows on their own present a solution
                if (boxRow1Candidates.Length == 1)
                {
                    if (boxRow1.IsJustOneElementUnsolved(out int column))
                    {
                        solution = GetSolution(index, i, column, boxRow1Candidates[0], nameof(Strategy.RowSolver));
                        return(true);
                    }
                }

                // iterate over columns for row
                // mostly targets row[column] cell
                for (int y = 0; y < 3; y++)
                {
                    // check if current value is 0
                    if (boxRow1[y] != 0)
                    {
                        continue;
                    }

                    int col1Index = y;
                    int col2Index = (y + 1) % 3;
                    int col3Index = (y + 2) % 3;

                    // baseline box
                    Line boxCol1 = box.GetColumn(col1Index);
                    Line boxCol2 = box.GetColumn(col2Index);
                    Line boxCol3 = box.GetColumn(col3Index);

                    // vertical adjacent box 1 -- cols
                    Line avnb1Col1 = avnb1.GetColumn(col1Index);
                    Line avnb1Col2 = avnb1.GetColumn(col2Index);
                    Line avnb1Col3 = avnb1.GetColumn(col3Index);

                    // vertical adjacent box 2 -- cols
                    Line avnb2Col1 = avnb2.GetColumn(col1Index);
                    Line avnb2Col2 = avnb2.GetColumn(col2Index);
                    Line avnb2Col3 = avnb2.GetColumn(col3Index);

                    // get complete column that includes the the first column of box
                    int  currentColIndex = columnOffset + y;
                    Line currentCol      = _puzzle.GetColumn(currentColIndex);

                    // calculate full set of illegal values for col 1
                    ReadOnlySpan <int> boxCol1IllegalValues = boxLine.Union(currentCol);

                    // determine union of values of cols
                    ReadOnlySpan <int> col2Union = avnb1Col2.Union(avnb2Col2);
                    ReadOnlySpan <int> col3Union = avnb1Col3.Union(avnb2Col3);

                    // determine disjoint set with baseline col -- looking for values now in that col
                    ReadOnlySpan <int> col2Candidates    = col2Union.DisjointSet(boxCol1IllegalValues);
                    ReadOnlySpan <int> col3Candidates    = col3Union.DisjointSet(boxCol1IllegalValues);
                    ReadOnlySpan <int> boxCol1Candidates = col2Candidates.Intersect(col3Candidates);

                    // determine if columns on their own present a solution
                    // can solve any cell in given box column
                    if (boxCol1Candidates.Length == 1)
                    {
                        if (boxCol1.IsJustOneElementUnsolved(out int row))
                        {
                            solution = GetSolution(index, row, y, boxCol1Candidates[0], nameof(Strategy.ColumnSolver));
                            return(true);
                        }
                    }

                    // determine if rows and columns together present a solution
                    // test: SolutionAllSlotsEmpty
                    ReadOnlySpan <int> rowAndColumnCandidates = boxRow1Candidates.Intersect(boxCol1Candidates);
                    if (rowAndColumnCandidates.Length == 1)
                    {
                        if (boxCol1[i] == 0)
                        {
                            solution = GetSolution(index, i, y, rowAndColumnCandidates[0], nameof(Strategy.RowColumnSolver));
                            return(true);
                        }
                    }

                    // determine if various combinations of rows and columns present a solution

                    // col1[row] is candidate for all following cases

                    // the following set of cases involve a row or a column with:
                    // a candidate, one cell == 0 and the other non zero
                    // the cases are looking for a set of rows and columns
                    // that project the same value except for the row or column with
                    // the non-zero value (which doesn't require a projected value)

                    // check rows
                    if (boxRow1Candidates.Length > 0)
                    {
                        var solverKind = nameof(Strategy.ColumnCandidateColumnBlockedRowSolver);

                        // row1[col3] has a value, so col3 don't need to participate in providing a candidate
                        if (col2Candidates.Length > 0 &&
                            boxRow1[col2Index] == 0 && boxRow1[col3Index] != 0 &&
                            TrySolveForIntersectionCandidates(boxRow1Candidates, col2Candidates, solverKind, out solution))
                        {
                            return(true);
                        }

                        // row1[col2] has a value, so col2 don't need to participate in providing a candidate
                        if (col3Candidates.Length > 0 &&
                            boxRow1[col2Index] != 0 && boxRow1[col3Index] == 0 &&
                            TrySolveForIntersectionCandidates(boxRow1Candidates, col3Candidates, solverKind, out solution))
                        {
                            return(true);
                        }
                    }

                    // check columns
                    if (boxCol1Candidates.Length > 0)
                    {
                        var solverKind = nameof(Strategy.RowCandidateRowBlockedColumnSolver);

                        // col1[row3] has a value, so row3 don't need to participate in providing a candidate
                        if (row2Candidates.Length > 0 &&
                            boxCol1[row2Index] == 0 && boxCol1[row3Index] != 0 &&
                            TrySolveForIntersectionCandidates(boxCol1Candidates, row2Candidates, solverKind, out solution))
                        {
                            return(true);
                        }

                        // col1[row2] has a value, so row2 don't need to participate in providing a candidate
                        // test: SolutionTwoSlotsEmptyInColumn
                        if (row3Candidates.Length > 0 &&
                            boxCol1[row2Index] != 0 && boxCol1[row3Index] == 0 &&
                            TrySolveForIntersectionCandidates(boxCol1Candidates, row3Candidates, solverKind, out solution))
                        {
                            return(true);
                        }
                    }

                    // the following set of cases involve a row or column with:
                    // all cells filled, such that a row or column doesn't need to participate in providing a candidate

                    // col3 is blocked, so col3 doesn't need to participate in providing a candidate
                    bool col2Blocked = boxCol2.GetUnsolvedCount() == 0;
                    bool col3Blocked = boxCol3.GetUnsolvedCount() == 0;
                    bool row2Blocked = boxRow2.GetUnsolvedCount() == 0;
                    bool row3Blocked = boxRow3.GetUnsolvedCount() == 0;

                    if (boxCol1.GetUnsolvedCount() == 1)
                    {
                        var solverKind = nameof(Strategy.ColumnCandidateColumnBlocked);

                        // Column 1 one slot open, column 2 blocked, column 3 offers a candidate
                        if (col2Blocked &&
                            col3Candidates.Length == 1)
                        {
                            solution = GetSolution(index, i, y, col3Candidates[0], solverKind);
                            return(true);
                        }

                        // Column 1 one slot open, column 2 offers a candidate, column 3 blocked
                        if (col3Blocked &&
                            col2Candidates.Length == 1)
                        {
                            solution = GetSolution(index, i, y, col2Candidates[0], solverKind);
                            return(true);
                        }
                    }

                    if (boxRow1.GetUnsolvedCount() == 1)
                    {
                        var solverKind = nameof(Strategy.RowCandidateRowBlocked);
                        // Row 1 one slot open, row 2 blocked, row 3 offers a candidate
                        if (row2Blocked &&
                            row3Candidates.Length == 1)
                        {
                            solution = GetSolution(index, i, y, row3Candidates[0], solverKind);
                            return(true);
                        }

                        // Column 1 one slot open, column 2 offers a candidate, column 3 blocked
                        if (row3Blocked &&
                            row2Candidates.Length == 1)
                        {
                            solution = GetSolution(index, i, y, row2Candidates[0], solverKind);
                            return(true);
                        }
                    }

                    // following set of cases test hidden singles in columns or rows

                    // pattern
                    // box 1 col1 is full
                    // box 2 has one value not in col1
                    // only one slot available in col1 in current box
                    // means that current cell must have that value

                    {
                        var solverKind = nameof(Strategy.ColumnLastPossibleSlot);

                        var candidates1 = avnb1.AsLine().DisjointSet(currentCol);

                        foreach (var candidate in candidates1)
                        {
                            if (!boxLine.ContainsValue(candidate) &&
                                !currentRow.ContainsValue(candidate) &&
                                CheckForValueInCellOrRow(candidate, box, col1Index, row2Index, row3Index) &&
                                CheckForValueInCellOrRow(candidate, avnb2, col1Index, 0, 1, 2)
                                )
                            {
                                solution = GetSolution(index, i, y, candidate, solverKind);
                                return(true);
                            }
                        }

                        var candidates2 = avnb2.AsLine().DisjointSet(currentCol);

                        foreach (var candidate in candidates2)
                        {
                            if (!boxLine.ContainsValue(candidate) &&
                                !currentRow.ContainsValue(candidate) &&
                                CheckForValueInCellOrRow(candidate, box, col1Index, row2Index, row3Index) &&
                                CheckForValueInCellOrRow(candidate, avnb1, col1Index, 0, 1, 2)
                                )
                            {
                                solution = GetSolution(index, i, y, candidate, solverKind);
                                return(true);
                            }
                        }
                    }

                    {
                        var solverKind = nameof(Strategy.RowLastPossibleSlot);

                        var candidates1 = ahnb1.AsLine().DisjointSet(currentRow);

                        foreach (var candidate in candidates1)
                        {
                            if (!boxLine.ContainsValue(candidate) &&
                                !currentCol.Segment.Contains(candidate) &&
                                CheckForValueInCellOrColumn(candidate, box, row1Index, col2Index, col3Index) &&
                                CheckForValueInCellOrColumn(candidate, ahnb2, row1Index, 0, 1, 2)
                                )
                            {
                                solution = GetSolution(index, i, y, candidate, solverKind);
                                return(true);
                            }
                        }

                        var candidates2 = ahnb2.AsLine().DisjointSet(currentRow);

                        foreach (var candidate in candidates2)
                        {
                            if (!boxLine.ContainsValue(candidate) &&
                                !currentCol.Segment.Contains(candidate) &&
                                CheckForValueInCellOrColumn(candidate, box, row1Index, col2Index, col3Index) &&
                                CheckForValueInCellOrColumn(candidate, ahnb1, row1Index, 0, 1, 2)
                                )
                            {
                                solution = GetSolution(index, i, y, candidate, solverKind);
                                return(true);
                            }
                        }
                    }

                    // Strategy where a column or row has two empty cells and one is constrained
                    {
                        var solverKind = nameof(Strategy.ColumnLastTwoPossibleSlots);

                        if (currentCol.GetUnsolvedCount() == 2)
                        {
                            var missingValues = currentCol.GetMissingValues();
                            for (int k = 0; k < 2; k++)
                            {
                                var value      = missingValues[k];
                                var otherValue = missingValues[(k + 1) % 2];
                                if (currentRow.ContainsValue(value) &&
                                    !boxLine.ContainsValue(otherValue))
                                {
                                    solution = GetSolution(index, i, y, otherValue, solverKind);
                                    return(true);
                                }
                            }
                        }
                    }

                    {
                        var solverKind = nameof(Strategy.RowLastTwoPossibleSlots);

                        if (currentRow.GetUnsolvedCount() == 2)
                        {
                            var missingValues = currentRow.GetMissingValues();
                            for (int k = 0; k < 2; k++)
                            {
                                var value      = missingValues[k];
                                var otherValue = missingValues[(k + 1) % 2];
                                if (currentCol.ContainsValue(value) &&
                                    !boxLine.ContainsValue(otherValue))
                                {
                                    solution = GetSolution(index, i, y, otherValue, solverKind);
                                    return(true);
                                }
                            }
                        }
                    }

                    {
                        var solverKind = nameof(Strategy.LastInRowOrColumn);
                        (var valuesFound, var values) = FindMissingValues(currentCol, currentRow);

                        for (var j = 1; j < 10; j++)
                        {
                            if (values[j])
                            {
                                continue;
                            }

                            var columnIndex   = box.GetColumnOffset() + y;
                            var rowIndex      = box.GetRowOffset() + i;
                            var foundSolution = 0;

                            // try with columns for the row
                            for (int l = 0; l < 9; l++)
                            {
                                var cell = currentRow[l];
                                if (cell != 0 || l == columnIndex)
                                {
                                    continue;
                                }

                                if (_puzzle.GetColumn(l).ContainsValue(j))
                                {
                                    foundSolution = j;
                                }
                                else
                                {
                                    foundSolution = 0;
                                    break;
                                }
                            }

                            if (foundSolution != 0)
                            {
                                solution = GetSolution(index, i, y, foundSolution, solverKind);
                                return(true);
                            }

                            // try with rows for the column
                            for (int l = 0; l < 9; l++)
                            {
                                var cell = currentCol[l];
                                if (cell != 0 || l == columnIndex)
                                {
                                    continue;
                                }

                                if (_puzzle.GetRow(l).ContainsValue(j))
                                {
                                    foundSolution = j;
                                }
                                else
                                {
                                    foundSolution = 0;
                                    break;
                                }
                            }

                            if (foundSolution != 0)
                            {
                                solution = GetSolution(index, i, y, foundSolution, solverKind);
                                return(true);
                            }
                        }
                    }

                    bool CheckRowForValue(int searchValue, Box box, int row)
                    {
                        // get complete row that includes current row of box
                        var rowIndex = box.GetRowOffset() + row;
                        //TODO: consider an overload that only returns non-zero values
                        var targetRow = _puzzle.GetRow(rowIndex);

                        return(targetRow.Segment.Contains(searchValue));
                    }

                    bool CheckForValueInCellOrColumn(int value, Box box, int row, params int[] cols)
                    {
                        foreach (int col in cols)
                        {
                            var targetColumn = box.GetColumn(col);
                            if (targetColumn[row] != 0 ||
                                ColumnContainsValue(value, box, col))
                            {
                            }
                            else
                            {
                                return(false);
                            }
                        }
                        return(true);
                    }

                    bool CheckForValueInCellOrRow(int value, Box box, int col, params int[] rows)
                    {
                        foreach (int row in rows)
                        {
                            var targetRow = box.GetRow(row);
                            if (targetRow[col] != 0 ||
                                CheckRowForValue(value, box, row))
                            {
                            }
                            else
                            {
                                return(false);
                            }
                        }
                        return(true);
                    }

                    // assumes lines are of the same length
                    (int valuesFound, bool[] values) FindMissingValues(Line line1, Line line2)
                    {
                        int valuesFound = 0;

                        bool[] values = new bool[10];

                        for (int i = 0; i < line1.Length; i++)
                        {
                            Update(line1[i]);
                            Update(line2[i]);
                        }

                        return(valuesFound, values);

                        void Update(int value)
                        {
                            if (value != 0 && !values[value])
                            {
                                values[value] = true;
                                valuesFound++;
                            }
                        }
                    }

                    bool ColumnContainsValue(int searchValue, Box box, int col)
                    {
                        // get complete column that includes current column of box
                        var colIndex  = box.GetColumnOffset() + col;
                        var targetCol = _puzzle.GetColumn(colIndex);

                        return(targetCol.Segment.Contains(searchValue));
                    }

                    bool TrySolveForIntersectionCandidates(ReadOnlySpan <int> candidate1, ReadOnlySpan <int> candidate2, string solverKind, out Solution solution)
                    {
                        var candidates = candidate1.Intersect(candidate2);

                        if (candidates.Length == 1)
                        {
                            solution = GetSolution(index, i, y, candidates[0], solverKind);
                            return(true);
                        }

                        solution = null;
                        return(false);
                    }
                }

                Solution GetSolution(int box, int row, int column, int value, string solverKind)
                {
                    (var r, var c) = Puzzle.GetLocationForBoxCell(index, (row * 3) + column);
                    return(new Solution(
                               r,
                               c,
                               value,
                               nameof(HiddenSinglesSolver),
                               solverKind));
                }
            }

            solution = null;
            return(false);
        }