Skip to content

Commit d6b4c71

Browse files
committed
Implemented X-Wing solving strategy + tests.
1 parent 9c8999c commit d6b4c71

File tree

4 files changed

+314
-1
lines changed

4 files changed

+314
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ Out of the well known [Sudoku Solving Techniques](https://sudoku9x9.com/sudoku_s
2727
* Hidden Pair
2828
* Hidden Triple
2929
* Hidden Quad
30+
* X-Wing
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using NUnit.Framework;
2+
using SimpleSudokuSolver.Model;
3+
using SimpleSudokuSolver.Strategy;
4+
5+
namespace SimpleSudokuSolver.Tests.Strategy
6+
{
7+
public class XWingTests : BaseStrategyTest
8+
{
9+
private readonly ISudokuSolverStrategy _strategy = new XWing();
10+
11+
[Test]
12+
public void XWingTest1()
13+
{
14+
var sudoku = new[,]
15+
{
16+
// From: http://www.sudokuwiki.org/X_Wing_Strategy
17+
{ 1,0,0,0,0,0,5,6,9 },
18+
{ 4,9,2,0,5,6,1,0,8 },
19+
{ 0,5,6,1,0,9,2,4,0 },
20+
{ 0,0,9,6,4,0,8,0,1 },
21+
{ 0,6,4,0,1,0,0,0,0 },
22+
{ 2,1,8,0,3,5,6,0,4 },
23+
{ 0,4,0,5,0,0,0,1,6 },
24+
{ 9,0,5,0,6,1,4,0,2 },
25+
{ 6,2,1,0,0,0,0,0,5 }
26+
};
27+
28+
var sudokuPuzzle = new SudokuPuzzle(sudoku);
29+
SolveUsingStrategy(sudokuPuzzle, _strategy);
30+
31+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[0, 3].CanBe, 7);
32+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[7, 3].CanBe, 7);
33+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 3].CanBe, 7);
34+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[7, 3].CanBe, 7);
35+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 3].CanBe, 7);
36+
37+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[7, 7].CanBe, 7);
38+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 7].CanBe, 7);
39+
}
40+
41+
[Test]
42+
public void XWingTest2()
43+
{
44+
var sudoku = new[,]
45+
{
46+
// From: http://www.sudokuwiki.org/X_Wing_Strategy
47+
{ 0,0,0,0,0,0,0,9,4 },
48+
{ 7,6,0,9,1,0,0,5,0 },
49+
{ 0,9,0,0,0,2,0,8,1 },
50+
{ 0,7,0,0,5,0,0,1,0 },
51+
{ 0,0,0,7,0,9,0,0,0 },
52+
{ 0,8,0,0,3,1,0,6,7 },
53+
{ 2,4,0,1,0,0,0,7,0 },
54+
{ 0,1,0,0,9,0,0,4,5 },
55+
{ 9,0,0,0,0,0,1,0,0 }
56+
};
57+
58+
// part 1
59+
var sudokuPuzzle = new SudokuPuzzle(sudoku);
60+
SolveUsingStrategy(sudokuPuzzle, _strategy);
61+
62+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 1].CanBe, 2);
63+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 2].CanBe, 2);
64+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 6].CanBe, 2);
65+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 8].CanBe, 2);
66+
67+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 3].CanBe, 2);
68+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 8].CanBe, 2);
69+
70+
// part 2
71+
sudokuPuzzle.Cells[0, 1].Value = 2;
72+
73+
SolveUsingStrategy(sudokuPuzzle, _strategy);
74+
75+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 0].CanBe, 3);
76+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 2].CanBe, 3);
77+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 6].CanBe, 3);
78+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 8].CanBe, 3);
79+
80+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 2].CanBe, 3);
81+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 3].CanBe, 3);
82+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 5].CanBe, 3);
83+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 8].CanBe, 3);
84+
}
85+
}
86+
}

SimpleSudokuSolver/DefaultSolver.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public DefaultSolver(params ISudokuSolverStrategy[] strategies)
4040
new NakedQuad(),
4141
new HiddenPair(),
4242
new HiddenTriple(),
43-
new HiddenQuad()
43+
new HiddenQuad(),
44+
new XWing()
4445
};
4546
}
4647
}
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
using SimpleSudokuSolver.Model;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace SimpleSudokuSolver.Strategy
7+
{
8+
/// <summary>
9+
/// Strategy is iterating through each row:
10+
/// - it is looking for a value which can be present only in two cells of the row
11+
/// If such a value is found, and there is another row where situation is the same (same value, same two columns),
12+
/// then all other candidates for this value in the two columns can be eliminated.
13+
/// Same is true when iterating through columns instead of rows (then we are eliminating candidates in rows).
14+
/// </summary>
15+
/// <remarks>
16+
/// See also:
17+
/// - https://sudoku9x9.com/x_wing.html
18+
/// - http://www.sudokuwiki.org/X_Wing_Strategy
19+
/// </remarks>
20+
public class XWing : ISudokuSolverStrategy
21+
{
22+
public string StrategyName => "X-Wing";
23+
24+
public SingleStepSolution SolveSingleStep(SudokuPuzzle sudokuPuzzle)
25+
{
26+
var eliminations = new List<SingleStepSolution.Candidate>();
27+
28+
var xWingMembersPerRow = GetXWingMembers(sudokuPuzzle, true);
29+
eliminations.AddRange(GetEliminations(sudokuPuzzle, xWingMembersPerRow, true));
30+
31+
var xWingMembersPerColumn = GetXWingMembers(sudokuPuzzle, false);
32+
eliminations.AddRange(GetEliminations(sudokuPuzzle, xWingMembersPerColumn, false));
33+
34+
return eliminations.Count > 0 ?
35+
new SingleStepSolution(eliminations.Distinct().ToArray(), StrategyName) :
36+
null;
37+
}
38+
39+
/// <summary>
40+
/// Iterates the entire puzzle either per row or per columns and returns all found XWings.
41+
/// </summary>
42+
/// <param name="sudokuPuzzle">Sudoku puzzle.</param>
43+
/// <param name="perRow">Determines if the method is iterating per row or per column.</param>
44+
/// <returns>
45+
/// Tuple where:
46+
/// - Item1 is the value which is in the XWing
47+
/// - Item2, Item3, Item4, Item5 are four cells that are members of the XWing
48+
/// </returns>
49+
private Tuple<int, Cell, Cell, Cell, Cell>[] GetXWingMembers(SudokuPuzzle sudokuPuzzle, bool perRow)
50+
{
51+
var candidatesPerRow = new List<Tuple<int, int, int>[]>();
52+
var candidatesPerColumn = new List<Tuple<int, int, int>[]>();
53+
54+
Tuple<int, int, int>[] ToCandidatesWithRowOrCellIndex(Tuple<int, Cell, Cell>[] candidates)
55+
{
56+
return candidates.Select(
57+
x =>
58+
{
59+
var cell1Index = sudokuPuzzle.GetCellIndex(x.Item2);
60+
var cell2Index = sudokuPuzzle.GetCellIndex(x.Item3);
61+
return new Tuple<int, int, int>(x.Item1,
62+
perRow ? cell1Index.ColumnIndex : cell1Index.RowIndex,
63+
perRow ? cell2Index.ColumnIndex : cell2Index.RowIndex);
64+
}
65+
).ToArray();
66+
}
67+
68+
var xWingMembers = new List<Tuple<int, Cell, Cell, Cell, Cell>>();
69+
70+
if (perRow)
71+
{
72+
for (int i = 0; i < sudokuPuzzle.Rows.Length; i++)
73+
{
74+
var candidates = GetCandidates(sudokuPuzzle.Rows[i].Cells, sudokuPuzzle.PossibleCellValues);
75+
var candidatesWithColumnIndex = ToCandidatesWithRowOrCellIndex(candidates);
76+
candidatesPerRow.Add(candidatesWithColumnIndex);
77+
}
78+
79+
for (int i = 0; i < sudokuPuzzle.Rows.Length - 1; i++)
80+
{
81+
var row1 = candidatesPerRow[i];
82+
83+
for (int j = i + 1; j < sudokuPuzzle.Rows.Length; j++)
84+
{
85+
var row2 = candidatesPerRow[j];
86+
87+
var intersect = row1.Intersect(row2).ToArray();
88+
if (intersect.Length > 0)
89+
{
90+
foreach (var intersectInstance in intersect)
91+
{
92+
var value = intersectInstance.Item1;
93+
var cell1 = sudokuPuzzle.Cells[i, intersectInstance.Item2];
94+
var cell2 = sudokuPuzzle.Cells[i, intersectInstance.Item3];
95+
var cell3 = sudokuPuzzle.Cells[j, intersectInstance.Item2];
96+
var cell4 = sudokuPuzzle.Cells[j, intersectInstance.Item3];
97+
xWingMembers.Add(new Tuple<int, Cell, Cell, Cell, Cell>(value, cell1, cell2, cell3, cell4));
98+
}
99+
}
100+
}
101+
}
102+
}
103+
else
104+
{
105+
for (int i = 0; i < sudokuPuzzle.Columns.Length; i++)
106+
{
107+
var candidates = GetCandidates(sudokuPuzzle.Columns[i].Cells, sudokuPuzzle.PossibleCellValues);
108+
var candidatesWithRowIndex = ToCandidatesWithRowOrCellIndex(candidates);
109+
candidatesPerColumn.Add(candidatesWithRowIndex);
110+
}
111+
112+
for (int i = 0; i < sudokuPuzzle.Columns.Length - 1; i++)
113+
{
114+
var column1 = candidatesPerColumn[i];
115+
116+
for (int j = i + 1; j < sudokuPuzzle.Columns.Length; j++)
117+
{
118+
var column2 = candidatesPerColumn[j];
119+
120+
var intersect = column1.Intersect(column2).ToArray();
121+
if (intersect.Length > 0)
122+
{
123+
foreach (var intersectInstance in intersect)
124+
{
125+
var value = intersectInstance.Item1;
126+
var cell1 = sudokuPuzzle.Cells[intersectInstance.Item2, i];
127+
var cell2 = sudokuPuzzle.Cells[intersectInstance.Item3, i];
128+
var cell3 = sudokuPuzzle.Cells[intersectInstance.Item2, j];
129+
var cell4 = sudokuPuzzle.Cells[intersectInstance.Item3, j];
130+
xWingMembers.Add(new Tuple<int, Cell, Cell, Cell, Cell>(value, cell1, cell2, cell3, cell4));
131+
}
132+
}
133+
}
134+
}
135+
}
136+
137+
return xWingMembers.ToArray();
138+
}
139+
140+
/// <summary>
141+
/// Returns all the candidates that can be eliminated using the XWings.
142+
/// </summary>
143+
private SingleStepSolution.Candidate[] GetEliminations(SudokuPuzzle sudokuPuzzle, Tuple<int, Cell, Cell, Cell, Cell>[] xWingMembers, bool perRow)
144+
{
145+
var eliminations = new List<SingleStepSolution.Candidate>();
146+
147+
foreach (var xWingMember in xWingMembers)
148+
{
149+
var value = xWingMember.Item1;
150+
151+
// diagonal cells
152+
var firstCellIndex = sudokuPuzzle.GetCellIndex(xWingMember.Item2);
153+
var secondCellIndex = sudokuPuzzle.GetCellIndex(xWingMember.Item5);
154+
155+
if (perRow)
156+
{
157+
var column1 = sudokuPuzzle.Columns[firstCellIndex.ColumnIndex];
158+
var column2 = sudokuPuzzle.Columns[secondCellIndex.ColumnIndex];
159+
var row1Index = firstCellIndex.RowIndex;
160+
var row2Index = secondCellIndex.RowIndex;
161+
162+
// eliminations are those cell that:
163+
// - are in 'column1' or 'column2'
164+
// - 'value' is a possible value for that cell
165+
// - are not in row1Index or row2Index
166+
foreach (var cell in column1.Cells.Union(column2.Cells))
167+
{
168+
var cellIndex = sudokuPuzzle.GetCellIndex(cell);
169+
if (cell.CanBe.Contains(value) && cellIndex.RowIndex != row1Index && cellIndex.RowIndex != row2Index)
170+
eliminations.Add(new SingleStepSolution.Candidate(cellIndex.RowIndex, cellIndex.ColumnIndex, value));
171+
}
172+
}
173+
else
174+
{
175+
var row1 = sudokuPuzzle.Rows[firstCellIndex.RowIndex];
176+
var row2 = sudokuPuzzle.Rows[secondCellIndex.RowIndex];
177+
var column1Index = firstCellIndex.ColumnIndex;
178+
var column2Index = secondCellIndex.ColumnIndex;
179+
180+
// eliminations are those cell that:
181+
// - are in 'row1' or 'row2'
182+
// - 'value' is a possible value for that cell
183+
// - are not in column1Index or column2Index
184+
foreach (var cell in row1.Cells.Union(row2.Cells))
185+
{
186+
var cellIndex = sudokuPuzzle.GetCellIndex(cell);
187+
if (cell.CanBe.Contains(value) && cellIndex.ColumnIndex != column1Index && cellIndex.ColumnIndex != column2Index)
188+
eliminations.Add(new SingleStepSolution.Candidate(cellIndex.RowIndex, cellIndex.ColumnIndex, value));
189+
}
190+
}
191+
}
192+
193+
194+
return eliminations.ToArray();
195+
}
196+
197+
/// <summary>
198+
/// For each value in <paramref name="possibleCellValues"/> method iterates through all the <paramref name="cells"/>
199+
/// and is looking for a pair of cells that are the only cells that can contain the possible value.
200+
/// </summary>
201+
/// <param name="cells">Cells of a row or column.</param>
202+
/// <param name="possibleCellValues"><see cref="SudokuPuzzle.PossibleCellValues"/></param>
203+
/// <returns>
204+
/// Tuple where:
205+
/// - Item1 is possible value
206+
/// - Item2 is first cell containing possible value
207+
/// - Item3 is second cell containing possible value
208+
/// </returns>
209+
private Tuple<int, Cell, Cell>[] GetCandidates(Cell[] cells, int[] possibleCellValues)
210+
{
211+
var result = new List<Tuple<int, Cell, Cell>>();
212+
213+
foreach (var possibleCellValue in possibleCellValues)
214+
{
215+
var cellsContaingValue = cells.Where(x => x.CanBe.Contains(possibleCellValue)).ToArray();
216+
if (cellsContaingValue.Length == 2)
217+
{
218+
result.Add(new Tuple<int, Cell, Cell>(possibleCellValue, cellsContaingValue[0], cellsContaingValue[1]));
219+
}
220+
}
221+
222+
return result.ToArray();
223+
}
224+
}
225+
}

0 commit comments

Comments
 (0)