diff --git a/SimpleSudokuSolver.Tests/Strategy/HiddenPairTests.cs b/SimpleSudokuSolver.Tests/Strategy/HiddenPairTests.cs new file mode 100644 index 0000000..747003b --- /dev/null +++ b/SimpleSudokuSolver.Tests/Strategy/HiddenPairTests.cs @@ -0,0 +1,80 @@ +using NUnit.Framework; +using SimpleSudokuSolver.Model; +using SimpleSudokuSolver.Strategy; + +namespace SimpleSudokuSolver.Tests.Strategy +{ + public class HiddenPairTests : BaseStrategyTest + { + private readonly ISudokuSolverStrategy _strategy = new HiddenPair(); + + [Test] + public void HiddenPairTest1() + { + var sudoku = new int[,] + { + // From: http://www.sudokuwiki.org/Hidden_Candidates + { 0,0,0,0,0,0,0,0,0 }, + { 9,0,4,6,0,7,0,0,0 }, + { 0,7,6,8,0,4,1,0,0 }, + { 3,0,9,7,0,1,0,8,0 }, + { 0,0,8,0,0,0,3,0,0 }, + { 0,5,0,3,0,8,7,0,2 }, + { 0,0,7,5,0,2,6,1,0 }, + { 0,0,0,4,0,3,2,0,8 }, + { 0,0,0,0,0,0,0,0,0 } + }; + + var sudokuPuzzle = new SudokuPuzzle(sudoku); + SolveUsingStrategy(sudokuPuzzle, _strategy); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 2); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 3); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 4); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 5); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 9); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 3); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 4); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 5); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 9); + } + + [Test] + public void HiddenPairTest2() + { + var sudoku = new int[,] + { + // From: http://www.sudokuwiki.org/Hidden_Candidates + { 7,2,0,4,0,8,0,3,0 }, + { 0,8,0,0,0,0,0,4,7 }, + { 4,0,1,0,7,6,8,0,2 }, + { 8,1,0,7,3,9,0,0,0 }, + { 0,0,0,8,5,1,0,0,0 }, + { 0,0,0,2,6,4,0,8,0 }, + { 2,0,9,6,8,0,4,1,3 }, + { 3,4,0,0,0,0,0,0,8 }, + { 1,6,8,9,4,3,2,7,5 } + }; + + var sudokuPuzzle = new SudokuPuzzle(sudoku); + SolveUsingStrategy(sudokuPuzzle, _strategy); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 2].CanBe, 5); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 2].CanBe, 6); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 1].CanBe, 9); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 2].CanBe, 3); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 2].CanBe, 6); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 2].CanBe, 7); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 6].CanBe, 6); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 6].CanBe, 9); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 6].CanBe, 1); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 6].CanBe, 5); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 6].CanBe, 9); + } + } +} diff --git a/SimpleSudokuSolver.Tests/Strategy/HiddenQuadTests.cs b/SimpleSudokuSolver.Tests/Strategy/HiddenQuadTests.cs new file mode 100644 index 0000000..066adff --- /dev/null +++ b/SimpleSudokuSolver.Tests/Strategy/HiddenQuadTests.cs @@ -0,0 +1,83 @@ +using NUnit.Framework; +using SimpleSudokuSolver.Model; +using SimpleSudokuSolver.Strategy; + +namespace SimpleSudokuSolver.Tests.Strategy +{ + public class HiddenQuadTests : BaseStrategyTest + { + private readonly ISudokuSolverStrategy _strategy = new HiddenQuad(); + + [Test] + public void HiddenQuadTest1() + { + var sudoku = new int[,] + { + // From: http://www.sudokuwiki.org/Hidden_Candidates + { 6,5,0,0,8,7,0,2,4 }, + { 0,0,0,6,4,9,0,5,0 }, + { 0,4,0,0,2,5,0,0,0 }, + { 5,7,0,4,3,8,0,6,1 }, + { 0,0,0,5,0,1,0,0,0 }, + { 3,1,0,9,0,2,0,8,5 }, + { 0,0,0,8,9,0,0,1,0 }, + { 0,0,0,2,1,3,0,0,0 }, + { 1,3,0,7,5,0,0,9,8 } + }; + + var sudokuPuzzle = new SudokuPuzzle(sudoku); + SolveUsingStrategy(sudokuPuzzle, _strategy); + + // manual adaptation of CanBe so HiddenQuad can be applied + sudokuPuzzle.Cells[2, 6].CanBe.Remove(9); + sudokuPuzzle.Cells[4, 6].CanBe.Remove(2); + sudokuPuzzle.Cells[4, 6].CanBe.Remove(9); + sudokuPuzzle.Cells[6, 6].CanBe.Remove(2); + sudokuPuzzle.Cells[6, 6].CanBe.Remove(4); + sudokuPuzzle.Cells[8, 6].CanBe.Remove(4); + + SolveUsingStrategy(sudokuPuzzle, _strategy); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[6, 6].CanBe, 6); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[7, 6].CanBe, 6); + } + + [Test] + public void HiddenQuadTest2() + { + var sudoku = new int[,] + { + // From: http://www.sudokuwiki.org/Hidden_Candidates + { 9,0,1,5,0,0,0,4,6 }, + { 4,2,5,0,9,0,0,8,1 }, + { 8,6,0,0,1,0,0,2,0 }, + { 5,0,2,0,0,0,0,0,0 }, + { 0,1,9,0,0,0,4,6,0 }, + { 6,0,0,0,0,0,0,0,2 }, + { 1,9,6,0,4,0,2,5,3 }, + { 2,0,0,0,6,0,8,1,7 }, + { 0,0,0,0,0,1,6,9,4 } + }; + + var sudokuPuzzle = new SudokuPuzzle(sudoku); + SolveUsingStrategy(sudokuPuzzle, _strategy); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 3].CanBe, 3); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 3].CanBe, 7); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 3].CanBe, 8); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 5].CanBe, 3); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 5].CanBe, 7); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 5].CanBe, 8); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 3].CanBe, 3); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 3].CanBe, 7); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 3].CanBe, 8); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 5].CanBe, 3); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 5].CanBe, 5); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 5].CanBe, 7); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 5].CanBe, 8); + } + } +} diff --git a/SimpleSudokuSolver.Tests/Strategy/HiddenTripleTests.cs b/SimpleSudokuSolver.Tests/Strategy/HiddenTripleTests.cs new file mode 100644 index 0000000..08cc0d9 --- /dev/null +++ b/SimpleSudokuSolver.Tests/Strategy/HiddenTripleTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using SimpleSudokuSolver.Model; +using SimpleSudokuSolver.Strategy; + +namespace SimpleSudokuSolver.Tests.Strategy +{ + public class HiddenTripleTests : BaseStrategyTest + { + private readonly ISudokuSolverStrategy _strategy = new HiddenTriple(); + + [Test] + public void HiddenTripleTest1() + { + var sudoku = new int[,] + { + // From: http://www.sudokuwiki.org/Hidden_Candidates + { 0,0,0,0,0,1,0,3,0 }, + { 2,3,1,0,9,0,0,0,0 }, + { 0,6,5,0,0,3,1,0,0 }, + { 6,7,8,9,2,4,3,0,0 }, + { 1,0,3,0,5,0,0,0,6 }, + { 0,0,0,1,3,6,7,0,0 }, + { 0,0,9,3,6,0,5,7,0 }, + { 0,0,6,0,1,9,8,4,3 }, + { 3,0,0,0,0,0,0,0,0 } + }; + + var sudokuPuzzle = new SudokuPuzzle(sudoku); + SolveUsingStrategy(sudokuPuzzle, _strategy); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 3].CanBe, 4); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 3].CanBe, 7); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 3].CanBe, 8); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 6].CanBe, 4); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 6].CanBe, 9); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 4); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 7); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 8); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 9); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[1, 8].CanBe, 5); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[2, 8].CanBe, 2); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[2, 8].CanBe, 9); + + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 8].CanBe, 2); + CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 8].CanBe, 9); + } + } +} diff --git a/SimpleSudokuSolver/DefaultSolver.cs b/SimpleSudokuSolver/DefaultSolver.cs index e66c285..c3579a4 100644 --- a/SimpleSudokuSolver/DefaultSolver.cs +++ b/SimpleSudokuSolver/DefaultSolver.cs @@ -36,7 +36,10 @@ public DefaultSolver(params ISudokuSolverStrategy[] strategies) new LockedCandidates(), new NakedPair(), new NakedTriple(), - new NakedQuad() + new NakedQuad(), + new HiddenPair(), + new HiddenTriple(), + new HiddenQuad() }; } } diff --git a/SimpleSudokuSolver/Strategy/HiddenPair.cs b/SimpleSudokuSolver/Strategy/HiddenPair.cs new file mode 100644 index 0000000..76485b0 --- /dev/null +++ b/SimpleSudokuSolver/Strategy/HiddenPair.cs @@ -0,0 +1,50 @@ +using SimpleSudokuSolver.Model; +using System.Collections.Generic; +using System.Linq; + +namespace SimpleSudokuSolver.Strategy +{ + /// + /// Strategy looks for two cells in the same row / column / block that have two candidate values that cannot + /// be in any other cell of the same row / column / block. + /// If such two cells are found, all other candidate values from those two cells can be removed. + /// + /// + /// See also: + /// - https://sudoku9x9.com/hidden_pair.html + /// - http://www.sudokuwiki.org/Hidden_Candidates + /// + public class HiddenPair : HiddenPairTripleQuadBase, ISudokuSolverStrategy + { + public string StrategyName => "Hidden Pair"; + + public SingleStepSolution SolveSingleStep(SudokuPuzzle sudokuPuzzle) + { + return GetSingleStepSolution(sudokuPuzzle, StrategyName); + } + + protected override IEnumerable GetHiddenEliminations( + IEnumerable cells, SudokuPuzzle sudokuPuzzle) + { + var cellsWithNoValue = cells.Where(x => !x.HasValue).ToArray(); + var hiddenCandidates = GetHiddenCandidates(cellsWithNoValue, sudokuPuzzle, 2); + var eliminations = new List(); + + for (int i = 1; i <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle - 1; i++) + { + for (int j = i + 1; j <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle; j++) + { + if (hiddenCandidates.ContainsKey(i) && + hiddenCandidates.ContainsKey(j) && + hiddenCandidates[i].SequenceEqual(hiddenCandidates[j])) + { + eliminations.AddRange(GetEliminations(hiddenCandidates[i][0], sudokuPuzzle, i, j)); + eliminations.AddRange(GetEliminations(hiddenCandidates[i][1], sudokuPuzzle, i, j)); + } + } + } + + return eliminations; + } + } +} diff --git a/SimpleSudokuSolver/Strategy/HiddenPairTripleQuadBase.cs b/SimpleSudokuSolver/Strategy/HiddenPairTripleQuadBase.cs new file mode 100644 index 0000000..4f70ef3 --- /dev/null +++ b/SimpleSudokuSolver/Strategy/HiddenPairTripleQuadBase.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using SimpleSudokuSolver.Model; + +namespace SimpleSudokuSolver.Strategy +{ + public abstract class HiddenPairTripleQuadBase + { + protected abstract IEnumerable GetHiddenEliminations( + IEnumerable cells, SudokuPuzzle sudokuPuzzle); + + protected SingleStepSolution GetSingleStepSolution(SudokuPuzzle sudokuPuzzle, string strategyName) + { + var eliminations = new List(); + + foreach (var row in sudokuPuzzle.Rows) + { + eliminations.AddRange(GetHiddenEliminations(row.Cells, sudokuPuzzle)); + } + + foreach (var column in sudokuPuzzle.Columns) + { + eliminations.AddRange(GetHiddenEliminations(column.Cells, sudokuPuzzle)); + } + + foreach (var block in sudokuPuzzle.Blocks) + { + eliminations.AddRange(GetHiddenEliminations(block.Cells.OfType(), sudokuPuzzle)); + } + + return eliminations.Count > 0 ? + new SingleStepSolution(eliminations.Distinct().ToArray(), strategyName) : + null; + } + + /// + /// Returns a dictionary where key is one of + /// and value is the collection of cells containing that value, but only if the + /// contains a certain number of such cells (). + /// + /// Empty cells of a single row/column/block. + /// Sudoku puzzle. + /// Tells how many cells containing a value we are looking for. + /// See summary. + protected IDictionary GetHiddenCandidates(Cell[] cellsWithNoValue, SudokuPuzzle sudokuPuzzle, + params int[] numberOfCellsContainingValue) + { + var candidates = new Dictionary(); + + foreach (var cellValue in sudokuPuzzle.PossibleCellValues) + { + var valueInCells = cellsWithNoValue.Where(x => x.CanBe.Contains(cellValue)).ToArray(); + if (numberOfCellsContainingValue.Contains(valueInCells.Length)) + candidates.Add(cellValue, valueInCells); + } + + return candidates; + } + + /// + /// For each member of's : + /// - if member is part of , ignore it + /// - if member is not part of return it as an elimination + /// + /// Cell which is analyzed for eliminations. + /// Sudoku puzzle to which the belongs. + /// Values which are not elimination candidates. + /// See summary. + protected IEnumerable GetEliminations( + Cell cell, SudokuPuzzle sudokuPuzzle, params int[] valuesToExclude) + { + var eliminations = new List(); + var cellIndex = sudokuPuzzle.GetCellIndex(cell); + var eliminatedValues = cell.CanBe.Except(valuesToExclude); + foreach (var eliminatedValue in eliminatedValues) + { + eliminations.Add(new SingleStepSolution.Candidate(cellIndex.RowIndex, cellIndex.ColumnIndex, eliminatedValue)); + } + + return eliminations; + } + } +} diff --git a/SimpleSudokuSolver/Strategy/HiddenQuad.cs b/SimpleSudokuSolver/Strategy/HiddenQuad.cs new file mode 100644 index 0000000..f6dd64a --- /dev/null +++ b/SimpleSudokuSolver/Strategy/HiddenQuad.cs @@ -0,0 +1,64 @@ +using SimpleSudokuSolver.Model; +using System.Collections.Generic; +using System.Linq; + +namespace SimpleSudokuSolver.Strategy +{ + /// + /// Strategy looks for four cells in the same row / column / block that have IN TOTAL four candidate values + /// that cannot be in any other cell of the same row / column / block. + /// If such four cells are found, all other candidate values from those four cells can be removed. + /// + /// + /// See also: + /// - https://sudoku9x9.com/hidden_pair.html + /// - http://www.sudokuwiki.org/Hidden_Candidates + /// + public class HiddenQuad : HiddenPairTripleQuadBase, ISudokuSolverStrategy + { + public string StrategyName => "Hidden Quad"; + + public SingleStepSolution SolveSingleStep(SudokuPuzzle sudokuPuzzle) + { + return GetSingleStepSolution(sudokuPuzzle, StrategyName); + } + + protected override IEnumerable GetHiddenEliminations( + IEnumerable cells, SudokuPuzzle sudokuPuzzle) + { + var cellsWithNoValue = cells.Where(x => !x.HasValue).ToArray(); + var hiddenCandidates = GetHiddenCandidates(cellsWithNoValue, sudokuPuzzle, 2, 3, 4); + var eliminations = new List(); + + for (int i = 1; i <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle - 3; i++) + { + for (int j = i + 1; j <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle - 2; j++) + { + for (int k = j + 1; k <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle - 1; k++) + { + for (int m = k + 1; m <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle; m++) + { + if (hiddenCandidates.ContainsKey(i) && + hiddenCandidates.ContainsKey(j) && + hiddenCandidates.ContainsKey(k) && + hiddenCandidates.ContainsKey(m)) + { + var union = hiddenCandidates[i].Union(hiddenCandidates[j]). + Union(hiddenCandidates[k]).Union(hiddenCandidates[m]).Distinct().ToArray(); + if (union.Length == 4) + { + foreach (var cell in union) + { + eliminations.AddRange(GetEliminations(cell, sudokuPuzzle, i, j, k, m)); + } + } + } + } + } + } + } + + return eliminations; + } + } +} diff --git a/SimpleSudokuSolver/Strategy/HiddenTriple.cs b/SimpleSudokuSolver/Strategy/HiddenTriple.cs new file mode 100644 index 0000000..b8c6fb8 --- /dev/null +++ b/SimpleSudokuSolver/Strategy/HiddenTriple.cs @@ -0,0 +1,60 @@ +using SimpleSudokuSolver.Model; +using System.Collections.Generic; +using System.Linq; + +namespace SimpleSudokuSolver.Strategy +{ + /// + /// Strategy looks for three cells in the same row / column / block that have IN TOTAL three candidate values + /// that cannot be in any other cell of the same row / column / block. + /// If such three cells are found, all other candidate values from those three cells can be removed. + /// + /// + /// See also: + /// - https://sudoku9x9.com/hidden_pair.html + /// - http://www.sudokuwiki.org/Hidden_Candidates + /// + public class HiddenTriple : HiddenPairTripleQuadBase, ISudokuSolverStrategy + { + public string StrategyName => "Hidden Triple"; + + public SingleStepSolution SolveSingleStep(SudokuPuzzle sudokuPuzzle) + { + return GetSingleStepSolution(sudokuPuzzle, StrategyName); + } + + protected override IEnumerable GetHiddenEliminations( + IEnumerable cells, SudokuPuzzle sudokuPuzzle) + { + var cellsWithNoValue = cells.Where(x => !x.HasValue).ToArray(); + var hiddenCandidates = GetHiddenCandidates(cellsWithNoValue, sudokuPuzzle, 2, 3); + var eliminations = new List(); + + for (int i = 1; i <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle - 2; i++) + { + for (int j = i + 1; j <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle - 1; j++) + { + for (int k = j + 1; k <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle; k++) + { + if (hiddenCandidates.ContainsKey(i) && + hiddenCandidates.ContainsKey(j) && + hiddenCandidates.ContainsKey(k)) + { + var union = hiddenCandidates[i].Union(hiddenCandidates[j]). + Union(hiddenCandidates[k]).Distinct().ToArray(); + if (union.Length == 3) + { + foreach (var cell in union) + { + eliminations.AddRange(GetEliminations(cell, sudokuPuzzle, i, j, k)); + } + } + } + } + } + } + + return eliminations; + } + } +}