Skip to content

Commit a9d5e95

Browse files
committed
Implemented HiddenPair, HiddenTriple and HiddenQuad solving strategies + tests.
1 parent 0bf3093 commit a9d5e95

File tree

8 files changed

+476
-1
lines changed

8 files changed

+476
-1
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using NUnit.Framework;
2+
using SimpleSudokuSolver.Model;
3+
using SimpleSudokuSolver.Strategy;
4+
5+
namespace SimpleSudokuSolver.Tests.Strategy
6+
{
7+
public class HiddenPairTests : BaseStrategyTest
8+
{
9+
private readonly ISudokuSolverStrategy _strategy = new HiddenPair();
10+
11+
[Test]
12+
public void HiddenPairTest1()
13+
{
14+
var sudoku = new int[,]
15+
{
16+
// From: http://www.sudokuwiki.org/Hidden_Candidates
17+
{ 0,0,0,0,0,0,0,0,0 },
18+
{ 9,0,4,6,0,7,0,0,0 },
19+
{ 0,7,6,8,0,4,1,0,0 },
20+
{ 3,0,9,7,0,1,0,8,0 },
21+
{ 0,0,8,0,0,0,3,0,0 },
22+
{ 0,5,0,3,0,8,7,0,2 },
23+
{ 0,0,7,5,0,2,6,1,0 },
24+
{ 0,0,0,4,0,3,2,0,8 },
25+
{ 0,0,0,0,0,0,0,0,0 }
26+
};
27+
28+
var sudokuPuzzle = new SudokuPuzzle(sudoku);
29+
SolveUsingStrategy(sudokuPuzzle, _strategy);
30+
31+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 2);
32+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 3);
33+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 4);
34+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 5);
35+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 7].CanBe, 9);
36+
37+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 3);
38+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 4);
39+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 5);
40+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 9);
41+
}
42+
43+
[Test]
44+
public void HiddenPairTest2()
45+
{
46+
var sudoku = new int[,]
47+
{
48+
// From: http://www.sudokuwiki.org/Hidden_Candidates
49+
{ 7,2,0,4,0,8,0,3,0 },
50+
{ 0,8,0,0,0,0,0,4,7 },
51+
{ 4,0,1,0,7,6,8,0,2 },
52+
{ 8,1,0,7,3,9,0,0,0 },
53+
{ 0,0,0,8,5,1,0,0,0 },
54+
{ 0,0,0,2,6,4,0,8,0 },
55+
{ 2,0,9,6,8,0,4,1,3 },
56+
{ 3,4,0,0,0,0,0,0,8 },
57+
{ 1,6,8,9,4,3,2,7,5 }
58+
};
59+
60+
var sudokuPuzzle = new SudokuPuzzle(sudoku);
61+
SolveUsingStrategy(sudokuPuzzle, _strategy);
62+
63+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 2].CanBe, 5);
64+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 2].CanBe, 6);
65+
66+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 1].CanBe, 9);
67+
68+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 2].CanBe, 3);
69+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 2].CanBe, 6);
70+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 2].CanBe, 7);
71+
72+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 6].CanBe, 6);
73+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 6].CanBe, 9);
74+
75+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 6].CanBe, 1);
76+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 6].CanBe, 5);
77+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 6].CanBe, 9);
78+
}
79+
}
80+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using NUnit.Framework;
2+
using SimpleSudokuSolver.Model;
3+
using SimpleSudokuSolver.Strategy;
4+
5+
namespace SimpleSudokuSolver.Tests.Strategy
6+
{
7+
public class HiddenQuadTests : BaseStrategyTest
8+
{
9+
private readonly ISudokuSolverStrategy _strategy = new HiddenQuad();
10+
11+
[Test]
12+
public void HiddenQuadTest1()
13+
{
14+
var sudoku = new int[,]
15+
{
16+
// From: http://www.sudokuwiki.org/Hidden_Candidates
17+
{ 6,5,0,0,8,7,0,2,4 },
18+
{ 0,0,0,6,4,9,0,5,0 },
19+
{ 0,4,0,0,2,5,0,0,0 },
20+
{ 5,7,0,4,3,8,0,6,1 },
21+
{ 0,0,0,5,0,1,0,0,0 },
22+
{ 3,1,0,9,0,2,0,8,5 },
23+
{ 0,0,0,8,9,0,0,1,0 },
24+
{ 0,0,0,2,1,3,0,0,0 },
25+
{ 1,3,0,7,5,0,0,9,8 }
26+
};
27+
28+
var sudokuPuzzle = new SudokuPuzzle(sudoku);
29+
SolveUsingStrategy(sudokuPuzzle, _strategy);
30+
31+
// manual adaptation of CanBe so HiddenQuad can be applied
32+
sudokuPuzzle.Cells[2, 6].CanBe.Remove(9);
33+
sudokuPuzzle.Cells[4, 6].CanBe.Remove(2);
34+
sudokuPuzzle.Cells[4, 6].CanBe.Remove(9);
35+
sudokuPuzzle.Cells[6, 6].CanBe.Remove(2);
36+
sudokuPuzzle.Cells[6, 6].CanBe.Remove(4);
37+
sudokuPuzzle.Cells[8, 6].CanBe.Remove(4);
38+
39+
SolveUsingStrategy(sudokuPuzzle, _strategy);
40+
41+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[6, 6].CanBe, 6);
42+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[7, 6].CanBe, 6);
43+
}
44+
45+
[Test]
46+
public void HiddenQuadTest2()
47+
{
48+
var sudoku = new int[,]
49+
{
50+
// From: http://www.sudokuwiki.org/Hidden_Candidates
51+
{ 9,0,1,5,0,0,0,4,6 },
52+
{ 4,2,5,0,9,0,0,8,1 },
53+
{ 8,6,0,0,1,0,0,2,0 },
54+
{ 5,0,2,0,0,0,0,0,0 },
55+
{ 0,1,9,0,0,0,4,6,0 },
56+
{ 6,0,0,0,0,0,0,0,2 },
57+
{ 1,9,6,0,4,0,2,5,3 },
58+
{ 2,0,0,0,6,0,8,1,7 },
59+
{ 0,0,0,0,0,1,6,9,4 }
60+
};
61+
62+
var sudokuPuzzle = new SudokuPuzzle(sudoku);
63+
SolveUsingStrategy(sudokuPuzzle, _strategy);
64+
65+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 3].CanBe, 3);
66+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 3].CanBe, 7);
67+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 3].CanBe, 8);
68+
69+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 5].CanBe, 3);
70+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 5].CanBe, 7);
71+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 5].CanBe, 8);
72+
73+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 3].CanBe, 3);
74+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 3].CanBe, 7);
75+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 3].CanBe, 8);
76+
77+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 5].CanBe, 3);
78+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 5].CanBe, 5);
79+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 5].CanBe, 7);
80+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 5].CanBe, 8);
81+
}
82+
}
83+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using NUnit.Framework;
2+
using SimpleSudokuSolver.Model;
3+
using SimpleSudokuSolver.Strategy;
4+
5+
namespace SimpleSudokuSolver.Tests.Strategy
6+
{
7+
public class HiddenTripleTests : BaseStrategyTest
8+
{
9+
private readonly ISudokuSolverStrategy _strategy = new HiddenTriple();
10+
11+
[Test]
12+
public void HiddenTripleTest1()
13+
{
14+
var sudoku = new int[,]
15+
{
16+
// From: http://www.sudokuwiki.org/Hidden_Candidates
17+
{ 0,0,0,0,0,1,0,3,0 },
18+
{ 2,3,1,0,9,0,0,0,0 },
19+
{ 0,6,5,0,0,3,1,0,0 },
20+
{ 6,7,8,9,2,4,3,0,0 },
21+
{ 1,0,3,0,5,0,0,0,6 },
22+
{ 0,0,0,1,3,6,7,0,0 },
23+
{ 0,0,9,3,6,0,5,7,0 },
24+
{ 0,0,6,0,1,9,8,4,3 },
25+
{ 3,0,0,0,0,0,0,0,0 }
26+
};
27+
28+
var sudokuPuzzle = new SudokuPuzzle(sudoku);
29+
SolveUsingStrategy(sudokuPuzzle, _strategy);
30+
31+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 3].CanBe, 4);
32+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 3].CanBe, 7);
33+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 3].CanBe, 8);
34+
35+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 6].CanBe, 4);
36+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 6].CanBe, 9);
37+
38+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 4);
39+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 7);
40+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 8);
41+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 8].CanBe, 9);
42+
43+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[1, 8].CanBe, 5);
44+
45+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[2, 8].CanBe, 2);
46+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[2, 8].CanBe, 9);
47+
48+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 8].CanBe, 2);
49+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 8].CanBe, 9);
50+
}
51+
}
52+
}

SimpleSudokuSolver/DefaultSolver.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ public DefaultSolver(params ISudokuSolverStrategy[] strategies)
3636
new LockedCandidates(),
3737
new NakedPair(),
3838
new NakedTriple(),
39-
new NakedQuad()
39+
new NakedQuad(),
40+
new HiddenPair(),
41+
new HiddenTriple(),
42+
new HiddenQuad()
4043
};
4144
}
4245
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using SimpleSudokuSolver.Model;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace SimpleSudokuSolver.Strategy
6+
{
7+
/// <summary>
8+
/// Strategy looks for two cells in the same row / column / block that have two candidate values that cannot
9+
/// be in any other cell of the same row / column / block.
10+
/// If such two cells are found, all other candidate values from those two cells can be removed.
11+
/// </summary>
12+
/// <remarks>
13+
/// See also:
14+
/// - https://sudoku9x9.com/hidden_pair.html
15+
/// - http://www.sudokuwiki.org/Hidden_Candidates
16+
/// </remarks>
17+
public class HiddenPair : HiddenPairTripleQuadBase, ISudokuSolverStrategy
18+
{
19+
public string StrategyName => "Hidden Pair";
20+
21+
public SingleStepSolution SolveSingleStep(SudokuPuzzle sudokuPuzzle)
22+
{
23+
return GetSingleStepSolution(sudokuPuzzle, StrategyName);
24+
}
25+
26+
protected override IEnumerable<SingleStepSolution.Candidate> GetHiddenEliminations(
27+
IEnumerable<Cell> cells, SudokuPuzzle sudokuPuzzle)
28+
{
29+
var cellsWithNoValue = cells.Where(x => !x.HasValue).ToArray();
30+
var hiddenCandidates = GetHiddenCandidates(cellsWithNoValue, sudokuPuzzle, 2);
31+
var eliminations = new List<SingleStepSolution.Candidate>();
32+
33+
for (int i = 1; i <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle - 1; i++)
34+
{
35+
for (int j = i + 1; j <= sudokuPuzzle.NumberOfRowsOrColumnsInPuzzle; j++)
36+
{
37+
if (hiddenCandidates.ContainsKey(i) &&
38+
hiddenCandidates.ContainsKey(j) &&
39+
hiddenCandidates[i].SequenceEqual(hiddenCandidates[j]))
40+
{
41+
eliminations.AddRange(GetEliminations(hiddenCandidates[i][0], sudokuPuzzle, i, j));
42+
eliminations.AddRange(GetEliminations(hiddenCandidates[i][1], sudokuPuzzle, i, j));
43+
}
44+
}
45+
}
46+
47+
return eliminations;
48+
}
49+
}
50+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using SimpleSudokuSolver.Model;
4+
5+
namespace SimpleSudokuSolver.Strategy
6+
{
7+
public abstract class HiddenPairTripleQuadBase
8+
{
9+
protected abstract IEnumerable<SingleStepSolution.Candidate> GetHiddenEliminations(
10+
IEnumerable<Cell> cells, SudokuPuzzle sudokuPuzzle);
11+
12+
protected SingleStepSolution GetSingleStepSolution(SudokuPuzzle sudokuPuzzle, string strategyName)
13+
{
14+
var eliminations = new List<SingleStepSolution.Candidate>();
15+
16+
foreach (var row in sudokuPuzzle.Rows)
17+
{
18+
eliminations.AddRange(GetHiddenEliminations(row.Cells, sudokuPuzzle));
19+
}
20+
21+
foreach (var column in sudokuPuzzle.Columns)
22+
{
23+
eliminations.AddRange(GetHiddenEliminations(column.Cells, sudokuPuzzle));
24+
}
25+
26+
foreach (var block in sudokuPuzzle.Blocks)
27+
{
28+
eliminations.AddRange(GetHiddenEliminations(block.Cells.OfType<Cell>(), sudokuPuzzle));
29+
}
30+
31+
return eliminations.Count > 0 ?
32+
new SingleStepSolution(eliminations.Distinct().ToArray(), strategyName) :
33+
null;
34+
}
35+
36+
/// <summary>
37+
/// Returns a dictionary where key is one of <see cref="SudokuPuzzle.PossibleCellValues"/>
38+
/// and value is the collection of cells containing that value, but only if the <paramref name="cellsWithNoValue"/>
39+
/// contains a certain number of such cells (<paramref name="numberOfCellsContainingValue"/>).
40+
/// </summary>
41+
/// <param name="cellsWithNoValue">Empty cells of a single row/column/block.</param>
42+
/// <param name="sudokuPuzzle">Sudoku puzzle.</param>
43+
/// <param name="numberOfCellsContainingValue">Tells how many cells containing a value we are looking for.</param>
44+
/// <returns>See summary.</returns>
45+
protected IDictionary<int, Cell[]> GetHiddenCandidates(Cell[] cellsWithNoValue, SudokuPuzzle sudokuPuzzle,
46+
params int[] numberOfCellsContainingValue)
47+
{
48+
var candidates = new Dictionary<int, Cell[]>();
49+
50+
foreach (var cellValue in sudokuPuzzle.PossibleCellValues)
51+
{
52+
var valueInCells = cellsWithNoValue.Where(x => x.CanBe.Contains(cellValue)).ToArray();
53+
if (numberOfCellsContainingValue.Contains(valueInCells.Length))
54+
candidates.Add(cellValue, valueInCells);
55+
}
56+
57+
return candidates;
58+
}
59+
60+
/// <summary>
61+
/// For each member of<paramref name="cell"/>'s <see cref="Cell.CanBe"/>:
62+
/// - if member is part of <paramref name="valuesToExclude"/>, ignore it
63+
/// - if member is not part of <paramref name="valuesToExclude"/> return it as an elimination
64+
/// </summary>
65+
/// <param name="cell">Cell which is analyzed for eliminations.</param>
66+
/// <param name="sudokuPuzzle">Sudoku puzzle to which the <paramref name="cell"/> belongs.</param>
67+
/// <param name="valuesToExclude">Values which are not elimination candidates.</param>
68+
/// <returns>See summary.</returns>
69+
protected IEnumerable<SingleStepSolution.Candidate> GetEliminations(
70+
Cell cell, SudokuPuzzle sudokuPuzzle, params int[] valuesToExclude)
71+
{
72+
var eliminations = new List<SingleStepSolution.Candidate>();
73+
var cellIndex = sudokuPuzzle.GetCellIndex(cell);
74+
var eliminatedValues = cell.CanBe.Except(valuesToExclude);
75+
foreach (var eliminatedValue in eliminatedValues)
76+
{
77+
eliminations.Add(new SingleStepSolution.Candidate(cellIndex.RowIndex, cellIndex.ColumnIndex, eliminatedValue));
78+
}
79+
80+
return eliminations;
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)